diff --git a/public/locales/en/sos.json b/public/locales/en/sos.json index 5a58d4ed1..f159e29b0 100644 --- a/public/locales/en/sos.json +++ b/public/locales/en/sos.json @@ -13,5 +13,43 @@ "linkCopied": "Link copied!", "growCrew": "grow your crew" } + }, + "superCharger": { + "title": "Super Charger", + "placeholder": "Enter Your Code", + "success": "Time bonus added {{time}}.", + "error": "Invalid code.", + "submit": "Go" + }, + "mainTimer": { + "title": "Your Timer", + "days": "DAYS", + "hours": "HOURS", + "minutes": "MINUTES", + "seconds": "SECONDS" + }, + "progress": { + "message": "You’re {{hours}} hours away from the next rank. Keep mining!" + }, + "leaserboardEntry": { + "mining": "Mining now", + "idle": "Last mined {{time}} ago" + }, + "leaderboard": { + "title": "Leaderboard", + "viewFull": "VIEW full LEADERBOARD" + }, + "member": { + "new": "New", + "nudge": "Nudge", + "minHr": "min/hr" + }, + "crewMining": { + "title": "Crew mining", + "rate": "+{{rate}}min/hr", + "mining": "{{current}}/{{total}} Mining" + }, + "crewList": { + "placeholder": "Invite your first crew member" } -} \ No newline at end of file +} diff --git a/src/components/AdminUI/groups/OtherUIGroup.tsx b/src/components/AdminUI/groups/OtherUIGroup.tsx index 873dc3037..bc7ef3033 100644 --- a/src/components/AdminUI/groups/OtherUIGroup.tsx +++ b/src/components/AdminUI/groups/OtherUIGroup.tsx @@ -7,8 +7,9 @@ import { useAirdropStore } from '@app/store/useAirdropStore.ts'; export function OtherUIGroup() { const setAdminShow = useUIStore((s) => s.setAdminShow); // prevent messing up the actual setup progress value const adminShow = useUIStore((s) => s.adminShow); - const { showWidget, setShowWidget } = useShellOfSecretsStore(); + const { showWidget, setShowWidget, showMainModal, setShowMainModal } = useShellOfSecretsStore(); const setFlare = useAirdropStore((s) => s.setFlareAnimationType); + return ( <> Other UI @@ -19,6 +20,9 @@ export function OtherUIGroup() { + + + + {/* */} + + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/LeaderboardList.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/LeaderboardList.tsx new file mode 100644 index 000000000..42bc90b2e --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/LeaderboardList.tsx @@ -0,0 +1,48 @@ +import { LeaderboardPlaceholder, Wrapper } from './styles'; +import LeaserboardEntry from './LeaserboardEntry/LeaserboardEntry'; +import { useCallback, useEffect, useState } from 'react'; +import { useAirdropRequest } from '@app/hooks/airdrop/utils/useHandleRequest'; +import { useAirdropStore } from '@app/store/useAirdropStore'; +import { LeaderboardResponse } from '@app/types/sosTypes'; +import { useShellOfSecretsStore } from '@app/store/useShellOfSecretsStore'; + +export default function LeaderboardList() { + const fetchHandler = useAirdropRequest(); + const userId = useAirdropStore((state) => state.userDetails?.user.id); + const setTotalTimeBonus = useShellOfSecretsStore((s) => s.setTotalTimeBonus); + const [leaderboardData, setLeaderboardData] = useState(); + + const handleLeaderboardData = useCallback(() => { + fetchHandler({ + path: '/sos/leaderboard/all', + method: 'GET', + }) + .catch((e) => { + console.error('Error fetching leaderboard data: ', e); + }) + .then((data) => { + if (!data?.top100) return; + setLeaderboardData(data); + if (data.userRank) { + setTotalTimeBonus(data.userRank.total_time_bonus); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetchHandler]); + + useEffect(() => { + handleLeaderboardData(); + const interval = setInterval(handleLeaderboardData, 1000 * 60); + return () => clearInterval(interval); + }, [handleLeaderboardData]); + + return ( + + {leaderboardData?.top100 + ? leaderboardData.top100.map((entry) => ( + + )) + : Array.from({ length: 100 }).map((_, index) => )} + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/LeaserboardEntry/LeaserboardEntry.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/LeaserboardEntry/LeaserboardEntry.tsx new file mode 100644 index 000000000..24e65a4a3 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/LeaserboardEntry/LeaserboardEntry.tsx @@ -0,0 +1,56 @@ +import { useTranslation } from 'react-i18next'; +import { Wrapper, Avatar, Handle, Status, LeftSide, RightSide, Rank, Dot, Duration } from './styles'; +import { LeaderboardEntry } from '@app/types/sosTypes'; +import { sosFormatAwardedBonusTime } from '@app/utils'; + +function getTimeDifference(dateString: string) { + const date = new Date(dateString); + // Calculate the difference in milliseconds + const diffInMs = Math.abs(date.getTime() - Date.now()); + // Convert to time units + const days = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diffInMs / (1000 * 60 * 60)) % 24); + const minutes = Math.floor((diffInMs / (1000 * 60)) % 60); + const seconds = Math.floor((diffInMs / 1000) % 60); + return sosFormatAwardedBonusTime({ days, hours, minutes, seconds }); +} + +interface Props { + entry: LeaderboardEntry; + isCurrentUser: boolean; +} + +export default function LeaserboardEntry({ entry, isCurrentUser }: Props) { + const { t } = useTranslation('sos', { useSuspense: false }); + + const getDuration = () => { + return `${sosFormatAwardedBonusTime({ ...entry.total_time_bonus })}`; + }; + const isMining = entry.last_mined_at && new Date(entry.last_mined_at).getTime() > Date.now() - 1000 * 60 * 5; + + return ( + + + + {entry.rank} + + {entry.name} + + + {entry.last_mined_at && isMining && ( + + {t('leaserboardEntry.mining')} + + )} + {entry.last_mined_at && !isMining && ( + + {' '} + {t('leaserboardEntry.idle', { time: getTimeDifference(entry.last_mined_at) })} + + )} + + {getDuration()} + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/LeaserboardEntry/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/LeaserboardEntry/styles.ts new file mode 100644 index 000000000..6b75b2544 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/LeaserboardEntry/styles.ts @@ -0,0 +1,151 @@ +import styled, { css } from 'styled-components'; + +export const Wrapper = styled('div')<{ $current?: boolean }>` + display: flex; + align-items: center; + + padding: 13px; + + border-radius: 9px; + border: 2px solid #2d2d2d; + background: rgba(255, 255, 255, 0.05); + box-shadow: 0px 3.625px 12.686px 0px rgba(0, 0, 0, 0.1); + + ${({ $current }) => + $current && + css` + border: 2px solid #85892a; + background: #364031; + box-shadow: 0px 3.698px 40.677px 0px rgba(0, 0, 0, 0.5); + `} +`; + +export const Avatar = styled('div')<{ $image: string; $current?: boolean }>` + width: 27px; + height: 27px; + border-radius: 100%; + background-color: rgba(255, 255, 255, 0.2); + + background-image: url(${(props) => props.$image}); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + + position: relative; + + ${({ $current }) => + $current && + css` + width: 45px; + height: 45px; + `} +`; + +export const Rank = styled('div')<{ $current?: boolean }>` + width: 15px; + height: 15px; + background-color: #e6ff47; + border-radius: 100%; + + position: absolute; + top: -4px; + left: -4px; + + display: flex; + justify-content: center; + align-items: center; + + color: #000; + font-family: 'Poppins', sans-serif; + font-size: 8px; + font-style: normal; + font-weight: 600; + line-height: 100%; + letter-spacing: 0.254px; + + ${({ $current }) => + $current && + css` + width: 26px; + height: 26px; + font-size: 13px; + top: -6px; + left: -8px; + `} +`; + +export const Handle = styled('span')<{ $current?: boolean }>` + color: #fff; + font-family: 'Poppins', sans-serif; + font-size: 13px; + font-style: normal; + font-weight: 600; + line-height: 100%; + letter-spacing: 0.254px; + + ${({ $current }) => + $current && + css` + font-size: 21.179px; + `} +`; + +export const LeftSide = styled('div')` + display: flex; + align-items: center; + flex-grow: 1; + gap: 10px; +`; + +export const RightSide = styled('div')` + display: flex; + align-items: center; + justify-content: center; + gap: 30px; +`; + +export const Dot = styled('div')<{ $isRed?: string }>` + width: 9px; + height: 9px; + border-radius: 100%; + background-color: #e6ff47; + + ${({ $isRed }) => + $isRed && + css` + background-color: #e2855d; + `} +`; + +export const Status = styled('div')<{ $isRed?: string }>` + color: #e6ff47; + font-size: 13px; + font-style: normal; + font-weight: 700; + line-height: 139.451%; + letter-spacing: -0.924px; + text-transform: uppercase; + + display: flex; + align-items: center; + gap: 5px; + + ${({ $isRed }) => + $isRed && + css` + color: #e2855d; + `} +`; + +export const Duration = styled('div')` + color: #e6ff47; + text-align: center; + font-size: 10px; + font-style: normal; + font-weight: 600; + line-height: 100%; + letter-spacing: 0.203px; + + padding: 6px 9px; + background: #000; +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/data.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/data.ts new file mode 100644 index 000000000..d37b63796 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/data.ts @@ -0,0 +1,164 @@ +export const data = { + entries: [ + { + handle: '@X_Miner_Pro', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 1, + image: 'https://robohash.org/user1', + }, + { + handle: '@CryptoKing', + status: 'idle', + last_mined: '2024-12-08T10:08:00Z', + duration: '52d 21h 32m 15s', + rank: 2, + image: 'https://robohash.org/user2', + }, + { + handle: '@BlockMaster', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 3, + image: 'https://robohash.org/user3', + }, + { + handle: '@HashRateHero', + status: 'idle', + last_mined: '2024-12-08T10:08:00Z', + duration: '52d 21h 32m 15s', + rank: 4, + image: 'https://robohash.org/user4', + }, + { + handle: '@MiningPro', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 5, + image: 'https://robohash.org/user5', + }, + { + handle: '@CoinCollector', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 6, + image: 'https://robohash.org/user6', + }, + { + handle: '@BlockchainBoss', + status: 'idle', + last_mined: '2024-12-08T10:08:00Z', + duration: '52d 21h 32m 15s', + rank: 7, + image: 'https://robohash.org/user7', + }, + { + handle: '@HashMaster', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 8, + image: 'https://robohash.org/user8', + }, + { + handle: '@CryptoChamp', + status: 'idle', + last_mined: '2024-12-08T10:08:00Z', + duration: '52d 21h 32m 15s', + rank: 9, + image: 'https://robohash.org/user9', + }, + { + handle: '@MineKing', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 10, + image: 'https://robohash.org/user10', + }, + { + handle: '@BlockWarrior', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 11, + image: 'https://robohash.org/user11', + }, + { + handle: '@HashHunter', + status: 'idle', + last_mined: '2024-12-08T10:08:00Z', + duration: '52d 21h 32m 15s', + rank: 12, + image: 'https://robohash.org/user12', + }, + { + handle: '@CryptoNinja', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 13, + image: 'https://robohash.org/user13', + }, + { + handle: '@BlockExplorer', + status: 'idle', + last_mined: '2024-12-08T10:08:00Z', + duration: '52d 21h 32m 15s', + rank: 14, + image: 'https://robohash.org/user14', + }, + { + handle: '@MiningMaster', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 15, + image: 'https://robohash.org/user15', + }, + { + handle: '@CryptoElite', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 16, + image: 'https://robohash.org/user16', + }, + { + handle: '@BlockGenius', + status: 'idle', + last_mined: '2024-12-08T10:08:00Z', + duration: '52d 21h 32m 15s', + rank: 17, + image: 'https://robohash.org/user17', + }, + { + handle: '@HashLegend', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 18, + image: 'https://robohash.org/user18', + }, + { + handle: '@CryptoWizard', + status: 'idle', + last_mined: '2024-12-08T10:08:00Z', + duration: '52d 21h 32m 15s', + rank: 19, + image: 'https://robohash.org/user19', + }, + { + handle: '@MiningLegend', + status: 'mining', + last_mined: null, + duration: '52d 21h 32m 15s', + rank: 20, + image: 'https://robohash.org/user20', + }, + ], +}; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/styles.ts new file mode 100644 index 000000000..9b60ec94e --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/LeaderboardList/styles.ts @@ -0,0 +1,45 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + gap: 6px; + + overflow: hidden; + overflow-y: auto; + + height: 100px; + flex-grow: 1; + + position: relative; + padding-top: 10px; + padding-bottom: 100px; + + mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 0px, + rgba(0, 0, 0, 1) 10px, + rgba(0, 0, 0, 1) 70%, + rgba(0, 0, 0, 0.5) 85%, + rgba(0, 0, 0, 0) 100% + ); + -webkit-mask-image: linear-gradient( + to bottom, + rgba(0, 0, 0, 0) 0px, + rgba(0, 0, 0, 1) 10px, + rgba(0, 0, 0, 1) 70%, + rgba(0, 0, 0, 0.5) 85%, + rgba(0, 0, 0, 0) 100% + ); +`; + +export const LeaderboardPlaceholder = styled('div')` + border-radius: 9px; + border: 2px solid rgba(255, 255, 255, 0.02); + background: rgba(255, 255, 255, 0.02); + box-shadow: 0px 3.625px 12.686px 0px rgba(0, 0, 0, 0.1); + + width: 100%; + height: 54px; + flex-shrink: 0; +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/Progress/Progress.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/Progress/Progress.tsx new file mode 100644 index 000000000..22db72be7 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/Progress/Progress.tsx @@ -0,0 +1,49 @@ +import { useTranslation, Trans } from 'react-i18next'; +import { + Wrapper, + TopLabel, + Line, + Text, + ProgressBar, + PercentWrapper, + PercentClip, + PercentText, + PercentTextOverlap, + Bar, + Inside, +} from './styles'; + +export default function Progress() { + const { t } = useTranslation('sos', { useSuspense: false }); + + const percent = 50; + + return ( + + + + + }} /> + + + + + + + + + + {percent}% + + {percent}% + + + + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/Progress/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/Progress/styles.ts new file mode 100644 index 000000000..45f83ff0a --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/Progress/styles.ts @@ -0,0 +1,115 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 10px; +`; + +export const TopLabel = styled('div')` + display: flex; + align-items: center; + gap: 40px; + width: calc(100% + 10px); + position: relative; + transform: translateX(-5px); + + &::before { + content: ''; + width: 2px; + height: 8px; + background-color: #e6ff47; + + position: absolute; + top: 7px; + right: 100%; + } + + &::after { + content: ''; + width: 2px; + height: 8px; + background-color: #e6ff47; + + position: absolute; + top: 7px; + left: 100%; + } +`; + +export const Line = styled('div')` + width: 100%; + height: 2px; + background-color: #e6ff47; +`; + +export const Text = styled('div')` + color: rgba(255, 255, 255, 0.5); + font-size: 13px; + font-style: normal; + font-weight: 700; + line-height: 129.623%; + text-transform: uppercase; + white-space: nowrap; + + span { + color: #e6ff47; + } +`; + +export const ProgressBar = styled('div')` + height: 41px; + + display: flex; + border-radius: 4px; + background-color: #e6ff47; + padding: 4px; + position: relative; +`; + +export const Bar = styled('div')` + height: 100%; + background-color: #0a1200; + border-radius: 4px; + transition: width 0.5s ease; +`; + +export const Inside = styled('div')` + position: relative; + width: 100%; +`; + +export const PercentWrapper = styled('div')` + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +`; + +export const PercentClip = styled('div')<{ $percent: number }>` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + clip-path: ${({ $percent }) => `inset(0 ${100 - $percent}% 0 0)`}; + transition: clip-path 0.5s ease; +`; + +export const PercentText = styled('div')` + color: #000; + font-size: 20px; + font-weight: 600; + line-height: 100%; + letter-spacing: 0.407px; +`; + +export const PercentTextOverlap = styled(PercentText)` + color: #fff; +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/styles.ts new file mode 100644 index 000000000..08949f2f8 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/Leaderboard/styles.ts @@ -0,0 +1,49 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + display: flex; + flex-direction: column; + flex-grow: 1; +`; + +export const TopRow = styled('div')` + display: flex; + align-items: center; + justify-content: space-between; + + margin-bottom: 20px; +`; + +export const SectionTitle = styled('div')` + color: #fff; + font-size: 22px; + font-weight: 700; + line-height: 129.623%; + text-transform: uppercase; +`; + +export const Button = styled('div')` + color: #fff; + font-size: 15px; + font-weight: 700; + line-height: normal; + text-transform: uppercase; + + border-radius: 9px; + background: rgba(217, 217, 217, 0.1); + + height: 35px; + padding: 12px 16px; + + display: flex; + align-items: center; + justify-content: center; + + cursor: pointer; + + transition: background 0.3s ease; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/MacbookDrop.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/MacbookDrop.tsx new file mode 100644 index 000000000..46d77ec0b --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/MacbookDrop.tsx @@ -0,0 +1,25 @@ +/* eslint-disable i18next/no-literal-string */ +import Progress from '../Progress/Progress'; +import { DropWrapper, Eyebrow, TextGroup, Title, Text, MacbookImage } from '../styles'; +import gemImage from './images/gem.png'; +import mackbookImage from './images/macbook.png'; + +export default function MacbookDrop() { + return ( + + + + LIMITED TIME + + + + WIN A MAC BOOK PRO + Be the first of 100 people to remove 1 Day from your clock and win a Macbook Pro + + + + + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/UpcomingDrop.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/UpcomingDrop.tsx new file mode 100644 index 000000000..6746e7059 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/UpcomingDrop.tsx @@ -0,0 +1,25 @@ +/* eslint-disable i18next/no-literal-string */ +import Progress from '../Progress/Progress'; +import { Eyebrow, TextGroup, Title, Text, DropWrapper, RuneImage } from '../styles'; +import cookieImage from './images/cookie.png'; +import runeImage from './images/rune.png'; + +export default function UpcomingDrop() { + return ( + + + + UPCOMING DROP + + + + 12D 8H 42M 21S + Invite 10 new crew members and unlock classified information about the next drop + + + + + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/cookie.png b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/cookie.png new file mode 100644 index 000000000..99993a428 Binary files /dev/null and b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/cookie.png differ diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/gem.png b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/gem.png new file mode 100644 index 000000000..eaec1b939 Binary files /dev/null and b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/gem.png differ diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/macbook.png b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/macbook.png new file mode 100644 index 000000000..ea0881c7a Binary files /dev/null and b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/macbook.png differ diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/rune.png b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/rune.png new file mode 100644 index 000000000..c9bc4ba0b Binary files /dev/null and b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Drops/images/rune.png differ diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/PrizeDrops.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/PrizeDrops.tsx new file mode 100644 index 000000000..3d1175355 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/PrizeDrops.tsx @@ -0,0 +1,12 @@ +import MacbookDrop from './Drops/MacbookDrop'; +import UpcomingDrop from './Drops/UpcomingDrop'; +import { Wrapper } from './styles'; + +export default function PrizeDrops() { + return ( + + + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Progress/Progress.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Progress/Progress.tsx new file mode 100644 index 000000000..e45d0b842 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Progress/Progress.tsx @@ -0,0 +1,46 @@ +import { Wrapper, PercentWrapper, PercentClip, PercentText, PercentTextOverlap, Bar, Inside } from './styles'; + +interface Props { + percent?: number; + count?: number; + total?: number; +} + +export default function Progress({ percent, count, total }: Props) { + const getDisplayValue = () => { + if (percent !== undefined) { + return `${percent}%`; + } + if (count !== undefined && total !== undefined) { + return `${count} / ${total}`; + } + return '0%'; + }; + + const getBarWidth = () => { + if (percent !== undefined) { + return percent; + } + if (count !== undefined && total !== undefined) { + return (count / total) * 100; + } + return 0; + }; + + const displayValue = getDisplayValue(); + const barWidth = getBarWidth(); + + return ( + + + + + {displayValue} + + {displayValue} + + + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Progress/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Progress/styles.ts new file mode 100644 index 000000000..d18b3eb27 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/Progress/styles.ts @@ -0,0 +1,59 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + height: 18px; + + display: flex; + border-radius: 4px; + background-color: #e6ff47; + padding: 2px; + position: relative; +`; + +export const Bar = styled('div')` + height: 100%; + background-color: #0a1200; + border-radius: 4px; + transition: width 0.5s ease; +`; + +export const Inside = styled('div')` + position: relative; + width: 100%; +`; + +export const PercentWrapper = styled('div')` + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +`; + +export const PercentClip = styled('div')<{ $percent: number }>` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + clip-path: ${({ $percent }) => `inset(0 ${100 - $percent}% 0 0)`}; + transition: clip-path 0.5s ease; +`; + +export const PercentText = styled('div')` + color: #000; + font-family: 'Poppins', sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 600; + line-height: 100%; + letter-spacing: 0.222px; +`; + +export const PercentTextOverlap = styled(PercentText)` + color: #fff; +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/images/background.png b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/images/background.png new file mode 100644 index 000000000..8ec616c76 Binary files /dev/null and b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/images/background.png differ diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/styles.ts new file mode 100644 index 000000000..0927bcaf2 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/PrizeDrops/styles.ts @@ -0,0 +1,89 @@ +import styled from 'styled-components'; +import backgroundImage from './images/background.png'; + +export const Wrapper = styled('div')` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; +`; + +export const DropWrapper = styled('div')` + width: 100%; + background-color: #171d15; + position: relative; + + padding: 35px 25px 18px 25px; + + display: flex; + flex-direction: column; + gap: 20px; + + background-image: url(${backgroundImage}); + background-size: cover; + background-position: right; +`; + +export const Eyebrow = styled('div')` + background: #e6ff47; + + width: 150px; + height: 24px; + padding: 0px 10px; + + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + flex-shrink: 0; + + position: absolute; + top: -12px; + + color: #000; + font-family: 'Poppins', sans-serif; + font-size: 10px; + font-style: normal; + font-weight: 700; + line-height: 100%; /* 10.104px */ + letter-spacing: -0.202px; +`; + +export const TextGroup = styled('div')` + display: flex; + flex-direction: column; + gap: 10px; + padding-right: 100px; + flex-grow: 1; +`; + +export const Title = styled('div')` + color: #e6ff47; + font-size: 28px; + font-weight: 700; + line-height: 105.333%; + letter-spacing: -0.924px; + max-width: 345px; +`; + +export const Text = styled('div')` + color: #cfe640; + font-size: 13px; + font-style: normal; + font-weight: 500; + line-height: 139.451%; + letter-spacing: -0.924px; + text-transform: uppercase; + max-width: 345px; +`; + +export const MacbookImage = styled('img')` + position: absolute; + top: -20px; + right: -20px; +`; + +export const RuneImage = styled('img')` + position: absolute; + top: -20px; + right: 10px; +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/SoSMainContent.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/SoSMainContent.tsx new file mode 100644 index 000000000..1a1b85ab2 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/SoSMainContent.tsx @@ -0,0 +1,16 @@ +import CrewMining from './CrewMining/CrewMining'; +import PrizeDrops from './PrizeDrops/PrizeDrops'; +import Leaderboard from './Leaderboard/Leaderboard'; +import { Wrapper } from './styles'; +import BottomElement from './BottomElement/BottomElement'; + +export default function SoSMainContent() { + return ( + + + + + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/styles.ts new file mode 100644 index 000000000..7847bc3a6 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainContent/styles.ts @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + gap: 20px; + + padding-top: 20px; +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/MainTimer/MainTimer.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/MainTimer/MainTimer.tsx new file mode 100644 index 000000000..6fcc9fd47 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/MainTimer/MainTimer.tsx @@ -0,0 +1,56 @@ +import { useTranslation } from 'react-i18next'; +import { Label, Number, NumberGroup, SectionLabel, TimerColumn, TopBar, Wrapper } from './styles'; +import { useShellOfSecretsStore } from '@app/store/useShellOfSecretsStore'; +import { useEffect, useState } from 'react'; + +const padTime = (time: number) => String(time).padStart(2, '0'); + +export default function MainTimer() { + const { t } = useTranslation('sos', { useSuspense: false }); + + const { getTimeRemaining } = useShellOfSecretsStore(); + const [reminingTime, setRemainingTime] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 }); + + useEffect(() => { + const intervalId = setInterval(() => { + setRemainingTime(getTimeRemaining()); + }, 1000); + return () => { + clearInterval(intervalId); + }; + }, [getTimeRemaining]); + + return ( + + + {t('mainTimer.title')} + + + + + {padTime(reminingTime.days)} + + + + + {padTime(reminingTime.hours)} + + + + + {padTime(reminingTime.minutes)} + + + + + {padTime(reminingTime.seconds)} + + + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/MainTimer/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/MainTimer/styles.ts new file mode 100644 index 000000000..a497d97e1 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/MainTimer/styles.ts @@ -0,0 +1,108 @@ +import styled from 'styled-components'; + +export const Wrapper = styled('div')``; + +export const SectionLabel = styled('div')` + font-size: 23px; + font-weight: 700; + line-height: 129.623%; + text-transform: uppercase; +`; + +export const TopBar = styled('div')` + display: flex; + align-items: center; + gap: 16px; + + &::before { + content: ''; + height: 2px; + width: 21px; + background-color: #e6ff47; + } + + &::after { + content: ''; + height: 2px; + width: 71px; + background-color: #e6ff47; + } +`; + +export const TimerColumn = styled('div')` + position: relative; + + display: flex; + flex-direction: column; + gap: 5px; + + border-left: 2px solid #e6ff47; + + padding: 24px 26px 18px 26px; + margin-top: -13px; + + &::after { + content: ''; + height: 2px; + width: 21px; + background-color: #e6ff47; + + position: absolute; + bottom: 0; + left: 0; + } +`; + +export const NumberGroup = styled('div')` + display: flex; + gap: 2px; + align-items: flex-end; +`; + +export const Number = styled('div')` + font-size: 153px; + line-height: 80%; + text-transform: uppercase; + + @media (max-height: 1024px) { + font-size: 120px; + } + + @media (max-height: 900px) { + font-size: 100px; + } + + @media (max-width: 1580px) { + font-size: 120px; + } + + @media (max-width: 1200px) { + font-size: 100px; + } + + @media (max-height: 800px) { + font-size: 80px; + } +`; + +export const Label = styled('div')` + font-size: 38px; + line-height: 100%; + text-transform: uppercase; + + @media (max-height: 1024px) { + font-size: 30px; + } + + @media (max-height: 900px) { + font-size: 25px; + } + + @media (max-width: 1580px) { + font-size: 30px; + } + + @media (max-width: 1200px) { + font-size: 25px; + } +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SoSMainSidebar.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SoSMainSidebar.tsx new file mode 100644 index 000000000..2c55be4c8 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SoSMainSidebar.tsx @@ -0,0 +1,14 @@ +import MainTimer from './MainTimer/MainTimer'; +import SuperCharger from './SuperCharger/SuperCharger'; +import { ContentLayer, Wrapper } from './styles'; + +export default function SoSMainSidebar() { + return ( + + + + + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/SuperCharger.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/SuperCharger.tsx new file mode 100644 index 000000000..c6eee869a --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/SuperCharger.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import KeyIcon from './icons/KeyIcon'; +import { + Wrapper, + TopBar, + SectionLabel, + FormWrapper, + InputField, + SubmitButton, + SuccessMessage, + ErrorMessage, +} from './styles'; +import { useAirdropRequest } from '@app/hooks/airdrop/utils/useHandleRequest'; +import { SosCoomieClaimResponse } from '@app/types/sosTypes'; +import { sosFormatAwardedBonusTime } from '@app/utils'; + +export default function SuperCharger() { + const { t } = useTranslation('sos', { useSuspense: false }); + const fetchHandler = useAirdropRequest(); + const [code, setCode] = useState(''); + const [successMessage, setSuccessMessage] = useState(''); + const [error, setError] = useState(''); + + const handleChange = (e: React.ChangeEvent) => { + setCode(e.target.value); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!code) return; + setSuccessMessage(''); + setError(''); + + fetchHandler({ + path: '/sos/cookie/claim', + method: 'POST', + body: { + cookieCode: code, + }, + }).then((response) => { + setCode(''); + // TODO add some feedback + if (response?.success) { + const time = sosFormatAwardedBonusTime(response.addedTimeBonus); + setSuccessMessage(t('superCharger.success', { time })); + } else { + setError(t('superCharger.error')); + } + }); + // TODO: handle loading and response states + }; + + useEffect(() => { + if (successMessage) { + const timeout = setTimeout(() => setSuccessMessage(''), 3000); + return () => clearTimeout(timeout); + } + }, [successMessage]); + + return ( + + + {t('superCharger.title')} + + + + + + + + {t('superCharger.submit')} + {successMessage} + {error} + + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/icons/KeyIcon.tsx b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/icons/KeyIcon.tsx new file mode 100644 index 000000000..3aeac82a3 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/icons/KeyIcon.tsx @@ -0,0 +1,39 @@ +const KeyIcon = () => ( + + + + + + +); + +export default KeyIcon; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/images/background.png b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/images/background.png new file mode 100644 index 000000000..a9fee67ec Binary files /dev/null and b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/images/background.png differ diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/styles.ts new file mode 100644 index 000000000..f595b1fc3 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/SuperCharger/styles.ts @@ -0,0 +1,167 @@ +import styled, { css } from 'styled-components'; +import backgroundImage from './images/background.png'; + +export const Wrapper = styled('div')` + width: 100%; + + display: flex; + flex-direction: column; + gap: 30px; +`; + +export const TopBar = styled('div')` + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 30px; + + &::before { + content: ''; + width: 100%; + height: 2px; + background-color: #e6ff47; + } + + &::after { + content: ''; + width: 100%; + height: 2px; + background-color: #e6ff47; + } +`; + +export const SectionLabel = styled('div')` + color: #e6ff47; + font-size: 23px; + font-weight: 500; + line-height: 129.623%; + text-transform: uppercase; + white-space: nowrap; + + @media (max-width: 1300px) { + font-size: 18px; + } +`; + +export const FormWrapper = styled('form')` + position: relative; + width: 100%; + min-height: 101px; + border: 1px solid #95a663; + background-color: #314627; + background-image: url(${backgroundImage}); + background-size: cover; + + padding: 0 30px 0px 10px; + + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + + svg { + flex-shrink: 0; + } + + @media (max-width: 1580px) { + padding: 0 30px; + + svg { + display: none; + } + } + + @media (max-width: 1220px) { + padding: 20px; + flex-wrap: wrap; + } +`; + +export const InputField = styled('input')` + width: 100%; + height: auto; + min-width: 180px; + padding: 2px 0 10px 0; + + background-color: transparent; + border: none; + + color: #fff; + font-size: 16px; + font-weight: 700; + line-height: 100%; + + border-bottom: 1px solid #fff; + + &::placeholder { + color: #fff; + opacity: 0.4; + } + + &:focus { + outline: none; + border-bottom: 1px solid #e6ff47; + + &::placeholder { + opacity: 0.6; + } + } +`; + +export const SubmitButton = styled('button')` + width: 81px; + + background: #e6ff47; + + display: flex; + width: 81px; + height: 32px; + padding: 12px; + justify-content: center; + align-items: center; + flex-shrink: 0; + + color: #161e0b; + font-size: 14px; + font-weight: 700; + line-height: 150%; + text-transform: uppercase; + + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.05); + } +`; + +export const SuccessMessage = styled('span')<{ $visible?: boolean }>` + opacity: 0; + bottom: 5px; + left: 50%; + transform: translateX(-50%); + position: absolute; + color: #e6ff47; + transition: opacity 0.2s ease-in-out; + ${({ $visible }) => + $visible && + css` + opacity: 1; + `} +`; + +export const ErrorMessage = styled('span')<{ $visible?: boolean }>` + opacity: 0; + position: absolute; + bottom: 5px; + height: 20px; + left: 50%; + transform: translateX(-50%); + color: red; + transition: opacity 0.2s ease-in-out; + ${({ $visible }) => + $visible && + css` + opacity: 1; + `} +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/images/sidebar_bg_1.png b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/images/sidebar_bg_1.png new file mode 100644 index 000000000..a64c78b75 Binary files /dev/null and b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/images/sidebar_bg_1.png differ diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/images/sidebar_bg_2.png b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/images/sidebar_bg_2.png new file mode 100644 index 000000000..69076ec83 Binary files /dev/null and b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/images/sidebar_bg_2.png differ diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/styles.ts new file mode 100644 index 000000000..1a4539c36 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/sections/SoSMainSidebar/styles.ts @@ -0,0 +1,55 @@ +import styled from 'styled-components'; +import bgImage1 from './images/sidebar_bg_1.png'; +import bgImage2 from './images/sidebar_bg_2.png'; + +export const Wrapper = styled('div')` + width: 100%; + + background: rgba(230, 255, 71, 0.1); + backdrop-filter: blur(1px); + + padding: 32px 26px; + position: relative; + + overflow: hidden; + overflow-y: auto; + + &::before { + content: ''; + position: absolute; + top: 0; + right: 0; + z-index: 0; + background-image: url(${bgImage1}); + background-repeat: no-repeat; + background-position: top right; + width: 100%; + height: 100%; + } + + &::after { + content: ''; + position: absolute; + bottom: 0; + right: 0; + z-index: 0; + background-image: url(${bgImage2}); + background-repeat: no-repeat; + background-position: bottom center; + width: 100%; + height: 100%; + } +`; + +export const ContentLayer = styled('div')` + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 20px; + + position: relative; + z-index: 1; + + width: 100%; + height: 100%; +`; diff --git a/src/containers/main/ShellOfSecrets/SoSMainModal/styles.ts b/src/containers/main/ShellOfSecrets/SoSMainModal/styles.ts new file mode 100644 index 000000000..512901179 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSMainModal/styles.ts @@ -0,0 +1,122 @@ +import { m } from 'framer-motion'; +import styled from 'styled-components'; + +export const Wrapper = styled('div')` + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 99999; + + display: flex; + justify-content: center; + align-items: center; + + pointer-events: all; + + overflow: hidden; + overflow-y: auto; + + padding: 41px 64px; + + @media (max-width: 1400px) { + padding: 30px; + } +`; + +export const Cover = styled(m.div)` + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.8); + z-index: 0; + cursor: pointer; + + backdrop-filter: blur(6px); +`; + +export const BoxWrapper = styled(m.div)` + width: 100%; + height: 100%; + flex-shrink: 0; + max-width: 1700px; + + border-radius: 30px; + box-shadow: 0px 4px 50px 0px rgba(0, 0, 0, 0.5); + + background-color: #111212; + + position: relative; + z-index: 1; +`; + +export const BoxContent = styled('div')` + display: flex; + flex-direction: column; + gap: 20px; + + padding: 34px; + + width: 100%; + height: 100%; + + overflow: hidden; + border-radius: 30px; + position: relative; + + overflow: hidden; + overflow-y: auto; +`; + +export const CloseButton = styled('button')` + cursor: pointer; + position: absolute; + top: -20px; + right: -20px; + z-index: 4; + transition: transform 0.2s ease; + color: #fff; + + display: flex; + justify-content: center; + align-items: center; + + width: 49px; + height: 49px; + border-radius: 50%; + + background-color: #36373a; + + &:hover { + transform: scale(1.1); + } +`; + +export const ContentLayer = styled('div')` + position: relative; + z-index: 3; + + display: grid; + grid-template-columns: 532px 1fr; + gap: 40px; + + width: 100%; + height: 100%; + + color: #e6ff47; + font-family: 'IBM Plex Mono', sans-serif; + font-weight: 700; + + transition: grid-template-columns 0.3s ease; + + @media (max-width: 1580px) { + grid-template-columns: 400px 1fr; + } + + @media (max-width: 1200px) { + grid-template-columns: 350px 1fr; + } +`; diff --git a/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/Friends.tsx b/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/Friends.tsx index d30b37041..be281a9c3 100644 --- a/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/Friends.tsx +++ b/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/Friends.tsx @@ -1,37 +1,15 @@ -import { - Wrapper, - FriendsWrapper, - Friend, - Text, - Buttons, - FriendCount, - CopyButtton, - GrowButton, - Copied, - PositionArrows, -} from './styles'; +import { Wrapper, FriendsWrapper, Friend, Text, FriendCount, PositionArrows } from './styles'; import friendImage1 from '../../images/friend1.png'; import friendImage2 from '../../images/friend2.png'; import friendImage3 from '../../images/friend3.png'; -import { useState } from 'react'; -import { AnimatePresence } from 'framer-motion'; import AnimatedArrows from './AnimatedArrows/AnimatedArrows'; import { useTranslation } from 'react-i18next'; +import GrowCrew from './GrowCrew/GrowCrew'; export default function Friends() { const { t } = useTranslation('sos', { useSuspense: false }); - const [copied, setCopied] = useState(false); - - const shareLink = 'https://universe.tari.com'; - - const handleCopy = () => { - navigator.clipboard.writeText(shareLink).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - }; return ( @@ -44,27 +22,7 @@ export default function Friends() { {t('widget.friends.reduceTimer')} - - - - - - - - - {copied && ( - - {t('widget.friends.linkCopied')} - - )} - - - {t('widget.friends.growCrew')} - + diff --git a/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/GrowCrew/GrowCrew.tsx b/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/GrowCrew/GrowCrew.tsx new file mode 100644 index 000000000..412c04412 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/GrowCrew/GrowCrew.tsx @@ -0,0 +1,43 @@ +import { Wrapper, CopyButtton, GrowButton, Copied } from './styles'; + +import { useState } from 'react'; +import { AnimatePresence } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; + +export default function GrowCrew() { + const { t } = useTranslation('sos', { useSuspense: false }); + const [copied, setCopied] = useState(false); + + const shareLink = 'https://universe.tari.com'; + + const handleCopy = () => { + navigator.clipboard.writeText(shareLink).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + + return ( + + + + + + + + + {copied && ( + + {t('widget.friends.linkCopied')} + + )} + + + {t('widget.friends.growCrew')} + + ); +} diff --git a/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/GrowCrew/styles.ts b/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/GrowCrew/styles.ts new file mode 100644 index 000000000..d3e735604 --- /dev/null +++ b/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/GrowCrew/styles.ts @@ -0,0 +1,82 @@ +import styled from 'styled-components'; +import { m } from 'framer-motion'; + +export const Wrapper = styled('div')` + display: flex; + gap: 4px; + align-items: center; + justify-content: center; +`; + +export const CopyButtton = styled('div')` + color: #fff; + border-radius: 7px; + background: rgba(217, 217, 217, 0.1); + backdrop-filter: blur(1px); + + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + + width: 28px; + height: 28px; + + cursor: pointer; + transition: background 0.3s ease; + position: relative; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } +`; + +export const Copied = styled(m.div)` + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + + background: #fff; + border-radius: 7px; + padding: 4px 8px; + border: 2px solid #000; + + color: #000; + font-family: 'IBM Plex Mono', sans-serif; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + pointer-events: none; + white-space: nowrap; + + margin-bottom: 5px; +`; + +export const GrowButton = styled('div')` + border-radius: 7px; + background: rgba(217, 217, 217, 0.1); + backdrop-filter: blur(1px); + + display: flex; + justify-content: center; + align-items: center; + gap: 10px; + + height: 28px; + padding: 0px 13px; + + color: #fff; + font-family: 'IBM Plex Mono', sans-serif; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + white-space: nowrap; + + cursor: pointer; + transition: background 0.3s ease; + + &:hover { + background: rgba(255, 255, 255, 0.2); + } +`; diff --git a/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/styles.ts b/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/styles.ts index c3a9dd09a..e4a61c072 100644 --- a/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/styles.ts +++ b/src/containers/main/ShellOfSecrets/SoSWidget/segments/Friends/styles.ts @@ -1,5 +1,4 @@ import styled from 'styled-components'; -import { m } from 'framer-motion'; export const Wrapper = styled('div')` display: flex; @@ -57,86 +56,6 @@ export const Text = styled('div')` max-width: 164px; `; -export const Buttons = styled('div')` - display: flex; - gap: 4px; - align-items: center; - justify-content: center; -`; - -export const CopyButtton = styled('div')` - color: #fff; - border-radius: 7px; - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(1px); - - display: flex; - justify-content: center; - align-items: center; - gap: 10px; - - width: 28px; - height: 28px; - - cursor: pointer; - transition: background 0.3s ease; - position: relative; - - &:hover { - background: rgba(255, 255, 255, 0.2); - } -`; - -export const Copied = styled(m.div)` - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - - background: #fff; - border-radius: 7px; - padding: 4px 8px; - border: 2px solid #000; - - color: #000; - font-family: 'IBM Plex Mono', sans-serif; - font-size: 10px; - font-weight: 700; - text-transform: uppercase; - pointer-events: none; - white-space: nowrap; - - margin-bottom: 5px; -`; - -export const GrowButton = styled('div')` - border-radius: 7px; - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(1px); - - display: flex; - justify-content: center; - align-items: center; - gap: 10px; - - height: 28px; - padding: 0px 13px; - - color: #fff; - font-family: 'IBM Plex Mono', sans-serif; - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - white-space: nowrap; - - cursor: pointer; - transition: background 0.3s ease; - - &:hover { - background: rgba(255, 255, 255, 0.2); - } -`; - export const PositionArrows = styled('div')` position: absolute; top: -20px; diff --git a/src/containers/main/ShellOfSecrets/components/Scanlines/Scanlines.tsx b/src/containers/main/ShellOfSecrets/components/Scanlines/Scanlines.tsx index ee9f1062b..c28f7b5bf 100644 --- a/src/containers/main/ShellOfSecrets/components/Scanlines/Scanlines.tsx +++ b/src/containers/main/ShellOfSecrets/components/Scanlines/Scanlines.tsx @@ -1,18 +1,31 @@ import { useEffect, useRef } from 'react'; import { Canvas, Vignette } from './styles'; -const Scanlines = () => { +interface ScanlinesProps { + scaleToWindow?: boolean; +} + +const Scanlines: React.FC = ({ scaleToWindow = false }) => { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; + if (scaleToWindow) { + canvas.width = window.innerWidth / 3; + canvas.height = window.innerHeight / 3; + } + const ctx = canvas.getContext('2d'); if (!ctx) return; let animationFrameId: number; - let scanlineOffset = 0; + let scanlineOffset = 0.2; // Adjusted initial scanlineOffset + + // Create ImageData once and reuse it + const imageData = ctx.createImageData(canvas.width, canvas.height); + const data = imageData.data; const drawScanlines = () => { if (!ctx || !canvas) return; @@ -20,24 +33,22 @@ const Scanlines = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); // Draw TV static - const imageData = ctx.createImageData(canvas.width, canvas.height); - const data = imageData.data; for (let i = 0; i < data.length; i += 4) { - const noise = Math.random() * 255; + const noise = Math.random() < 0.5 ? 19 : 28; // Updated noise colors data[i] = noise; data[i + 1] = noise; data[i + 2] = noise; - data[i + 3] = 255 * 0.08; + data[i + 3] = 255; } ctx.putImageData(imageData, 0, 0); // Draw scanlines for (let y = scanlineOffset; y < canvas.height; y += 3) { - ctx.fillStyle = 'rgba(255, 255, 255, 0.08)'; + ctx.fillStyle = '#1c212b'; // Lighter scanline color ctx.fillRect(0, y, canvas.width, 1); } - scanlineOffset = (scanlineOffset + 0.1) % 4; + scanlineOffset = (scanlineOffset + 0.1) % 3; animationFrameId = requestAnimationFrame(drawScanlines); }; @@ -47,11 +58,11 @@ const Scanlines = () => { return () => { cancelAnimationFrame(animationFrameId); }; - }, []); + }, [scaleToWindow]); return ( <> - + ); diff --git a/src/containers/main/ShellOfSecrets/components/Scanlines/styles.ts b/src/containers/main/ShellOfSecrets/components/Scanlines/styles.ts index 06b3d17d9..f290ab0b3 100644 --- a/src/containers/main/ShellOfSecrets/components/Scanlines/styles.ts +++ b/src/containers/main/ShellOfSecrets/components/Scanlines/styles.ts @@ -4,7 +4,7 @@ export const Canvas = styled('canvas')` position: absolute; inset: 0; pointer-events: none; - z-index: 2; + z-index: 1; width: 100%; height: 100%; `; @@ -13,7 +13,7 @@ export const Vignette = styled.div` position: absolute; inset: 0; pointer-events: none; - z-index: 1; - background: radial-gradient(circle at center, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%); + z-index: 2; + background: radial-gradient(circle, rgba(0, 0, 0, 0.3) 0%, rgba(18, 18, 19, 0.8) 100%); mix-blend-mode: multiply; `; diff --git a/src/hooks/airdrop/ws/useHandleWsUserIdEvent.ts b/src/hooks/airdrop/ws/useHandleWsUserIdEvent.ts index 416e0a2f5..5cbbc4776 100644 --- a/src/hooks/airdrop/ws/useHandleWsUserIdEvent.ts +++ b/src/hooks/airdrop/ws/useHandleWsUserIdEvent.ts @@ -44,6 +44,9 @@ export const useHandleWsUserIdEvent = () => { case WebsocketEventNames.MINING_STATUS_USER_UPDATE: setTotalBonusTimeMs(eventParsed.data.totalTimeBonusMs); break; + case WebsocketEventNames.COOKIE_CLAIMED: + setTotalBonusTimeMs(eventParsed.data.totalTimeBonusMs); + break; default: // eslint-disable-next-line no-console console.log('Unknown event', eventParsed); diff --git a/src/store/useShellOfSecretsStore.ts b/src/store/useShellOfSecretsStore.ts index 1a8bfcecb..eb1a6f8f4 100644 --- a/src/store/useShellOfSecretsStore.ts +++ b/src/store/useShellOfSecretsStore.ts @@ -1,5 +1,6 @@ import { CrewMember } from '@app/types/ws.ts'; import { create } from './create.ts'; +import { AwardedTimeBonus } from '@app/types/sosTypes.ts'; const SOS_GAME_ENDING_DATE = new Date('2025-01-30'); export const MINING_EVENT_INTERVAL_MS = 15000; @@ -33,6 +34,7 @@ interface State { showWidget: boolean; totalBonusTimeMs: number; revealDate: Date; + showMainModal: boolean; wsConnectionState: WsConnectionState; } @@ -40,8 +42,10 @@ interface Actions { setReferrals: (referrals: ReferralsResponse) => void; setShowWidget: (showWidget: boolean) => void; setTotalBonusTimeMs: (totalTimeBonusUpdate: number) => void; + setTotalTimeBonus: (totalTimeBonusUpdate: AwardedTimeBonus) => void; + getTimeRemaining: () => { days: number; hours: number; totalRemainingMs: number; minutes: number; seconds: number }; + setShowMainModal: (showMainModal: boolean) => void; registerWsConnectionEvent: (event: WsConnectionEvent) => void; - getTimeRemaining: () => { days: number; hours: number; totalRemainingMs: number }; } const initialState: State = { @@ -49,6 +53,7 @@ const initialState: State = { showWidget: false, totalBonusTimeMs: 0, revealDate: SOS_GAME_ENDING_DATE, + showMainModal: false, wsConnectionState: { state: 'off', }, @@ -59,6 +64,24 @@ export const useShellOfSecretsStore = create()((set, get) => ({ setReferrals: (referrals) => set({ referrals }), setShowWidget: (showWidget) => set({ showWidget }), setTotalBonusTimeMs: (totalTimeBonusUpdate: number) => set({ totalBonusTimeMs: totalTimeBonusUpdate }), + setTotalTimeBonus: (totalTimeBonusUpdate: AwardedTimeBonus) => { + let newTotalTimeBonus = 0; + if (totalTimeBonusUpdate.days) { + newTotalTimeBonus += totalTimeBonusUpdate.days * 24 * 60 * 60 * 1000; + } + if (totalTimeBonusUpdate.hours) { + newTotalTimeBonus += totalTimeBonusUpdate.hours * 60 * 60 * 1000; + } + if (totalTimeBonusUpdate.minutes) { + newTotalTimeBonus += totalTimeBonusUpdate.minutes * 60 * 1000; + } + if (totalTimeBonusUpdate.seconds) { + newTotalTimeBonus += totalTimeBonusUpdate.seconds * 1000; + } + if (newTotalTimeBonus !== get().totalBonusTimeMs) { + set({ totalBonusTimeMs: newTotalTimeBonus }); + } + }, registerWsConnectionEvent: (event: WsConnectionEvent) => set(({ wsConnectionState }) => { if (event.state === 'off' || event.state === 'up') { @@ -91,6 +114,9 @@ export const useShellOfSecretsStore = create()((set, get) => ({ const days = Math.floor(remainingMs / (1000 * 60 * 60 * 24)); const hours = Math.floor((remainingMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); - return { days, hours, totalRemainingMs: remainingMs }; + const minutes = Math.floor((remainingMs % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((remainingMs % (1000 * 60)) / 1000); + return { days, hours, totalRemainingMs: remainingMs, minutes, seconds }; }, + setShowMainModal: (showMainModal) => set({ showMainModal }), })); diff --git a/src/types/sosTypes.ts b/src/types/sosTypes.ts new file mode 100644 index 000000000..42e39cbc5 --- /dev/null +++ b/src/types/sosTypes.ts @@ -0,0 +1,36 @@ +export interface AwardedTimeBonus { + days?: number; + hours?: number; + minutes?: number; + seconds?: number; +} + +export interface LeaderboardEntry { + created_at: string; + updated_at: string; + id: string; + user_id: string; + name: string; + photo: string; + total_time_bonus: AwardedTimeBonus; + rank: string; + last_mined_at: string; +} + +export interface LeaderboardResponse { + top100: LeaderboardEntry[]; + userRank: LeaderboardEntry; +} + +export interface SosCoomieClaimResponse { + success: boolean; + message: string; + addedTimeBonus: AwardedTimeBonus; + newBalance: AwardedTimeBonus; + cookie: { + claimCode: string; + color: string; + fortune: string; + timeBonus: AwardedTimeBonus; + }; +} diff --git a/src/types/ws.ts b/src/types/ws.ts index b76306d0e..649433214 100644 --- a/src/types/ws.ts +++ b/src/types/ws.ts @@ -1,5 +1,6 @@ export enum WebsocketEventNames { COMPLETED_QUEST = 'completed_quest', + COOKIE_CLAIMED = 'cookie_claimed', MINING_STATUS_CREW_UPDATE = 'mining_status_crew_update', MINING_STATUS_CREW_DISCONNECTED = 'mining_status_crew_disconnected', MINING_STATUS_USER_UPDATE = 'mining_status_user_update', @@ -51,8 +52,16 @@ export interface MiningStatusCrewDisconnectedEvent { }; } +export interface CookieClaimedEvent { + name: WebsocketEventNames.COOKIE_CLAIMED; + data: { + totalTimeBonusMs: number; + }; +} + export type WebsocketUserEvent = | QuestCompletedEvent + | CookieClaimedEvent | MiningStatusCrewUpdateEvent | MiningStatusUserUpdateEvent | MiningStatusCrewDisconnectedEvent; diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index 776abf349..fc6a089fd 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -1,3 +1,4 @@ +import { AwardedTimeBonus } from '@app/types/sosTypes'; import i18n from 'i18next'; export enum FormatPreset { @@ -64,3 +65,13 @@ export function formatHashrate(hashrate: number, joinUnit = true): string { return (hashrate / 1000000000000000).toFixed(2) + (joinUnit ? ' PH/s' : 'P'); } } + +export const sosFormatAwardedBonusTime = (props: AwardedTimeBonus) => { + const units: string[] = []; + if (props.days) units.push(`${props.days}d`); + if (props.hours) units.push(`${props.hours}h`); + if (props.minutes) units.push(`${props.minutes}m`); + if (props.seconds) units.push(`${props.seconds}s`); + if (units.length === 0) units.push('0s'); + return units.join(' '); +};