diff --git a/.eslintrc.json b/.eslintrc.json index 61cc8d2b..6a4aa2e6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -101,6 +101,8 @@ "!src/apps/deposit/**", "!src/apps/leaderboard/", "!src/apps/leaderboard/**", + "!src/apps/blitz/", + "!src/apps/blitz/**", "public/" // Ignore public folder ], "settings": { diff --git a/src/apps/blitz/components/RandomAvatar/RandomAvatar.tsx b/src/apps/blitz/components/RandomAvatar/RandomAvatar.tsx new file mode 100644 index 00000000..4244fa95 --- /dev/null +++ b/src/apps/blitz/components/RandomAvatar/RandomAvatar.tsx @@ -0,0 +1,50 @@ +import Avatar from 'boring-avatars'; + +// types +import { AvatarVariantType } from '../../../../types'; + +type RandomAvatarProps = { + name: string; + variant?: string; + isRandomVariant?: boolean; +}; + +const RandomAvatar = ({ + name, + variant, + isRandomVariant, +}: RandomAvatarProps) => { + const variants: AvatarVariantType[] = [ + 'marble', + 'beam', + 'pixel', + 'sunset', + 'ring', + 'bauhaus', + ]; + + const randomVariant: AvatarVariantType = + variants[Math.floor(Math.random() * variants.length)]; + + const avatarVariant = () => { + if (isRandomVariant && !variant) { + return randomVariant; + } + if (variant) { + return variant as AvatarVariantType; + } + return 'marble'; + }; + + return ( + + ); +}; + +export default RandomAvatar; diff --git a/src/apps/blitz/components/TimeClock/TimeClock.tsx b/src/apps/blitz/components/TimeClock/TimeClock.tsx new file mode 100644 index 00000000..56a4e7aa --- /dev/null +++ b/src/apps/blitz/components/TimeClock/TimeClock.tsx @@ -0,0 +1,84 @@ +/* eslint-disable react/no-unstable-nested-components */ +import { differenceInMinutes, differenceInSeconds, isPast } from 'date-fns'; +import { useEffect, useState } from 'react'; + +const DUMMY_END_DATE = new Date(2025, 1, 5, 18, 10, 0); // Move outside component + +type TimeClockProps = { classname?: string }; + +const TimeClock = ({ classname }: TimeClockProps) => { + const [isDesktop, setIsDesktop] = useState(window.innerWidth > 1023); + + const calculateTimeLeft = () => { + const now = new Date(); + const minutes = Math.max(0, differenceInMinutes(DUMMY_END_DATE, now)); + const seconds = Math.max(0, differenceInSeconds(DUMMY_END_DATE, now) % 60); + return { minutes, seconds }; + }; + + const [timeRemaining, setTimeRemaining] = useState(calculateTimeLeft); + + useEffect(() => { + const interval = setInterval(() => { + setTimeRemaining(calculateTimeLeft()); + }, 1000); + + return () => clearInterval(interval); + }, []); + + useEffect(() => { + const handleResize = () => { + setIsDesktop(window.innerWidth > 1023); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + const formattedMinutes = timeRemaining.minutes.toString().padStart(2, '0'); + const formattedSeconds = timeRemaining.seconds.toString().padStart(2, '0'); + + const timeMap = Array.from(formattedMinutes + formattedSeconds); + + const DigitComponent = ({ time }: { time: string }) => { + return ( +
+
+

+ {isPast(DUMMY_END_DATE) ? 0 : time} +

+
+
+ ); + }; + + return ( +
+
+
+ + +
+

+ {isDesktop ? 'minutes' : 'min'} +

+
+
+
+ + +
+

+ {isDesktop ? 'seconds' : 'sec'} +

+
+
+ ); +}; + +export default TimeClock; diff --git a/src/apps/blitz/components/TokenPriceTime/TokenPriceTime.tsx b/src/apps/blitz/components/TokenPriceTime/TokenPriceTime.tsx new file mode 100644 index 00000000..32a15ee9 --- /dev/null +++ b/src/apps/blitz/components/TokenPriceTime/TokenPriceTime.tsx @@ -0,0 +1,37 @@ +import TokenPotIcon from '../../images/token_pot_icon.svg'; +import TimeClock from '../TimeClock/TimeClock'; +import TokenPriceUpdate from '../TokenPriceUpdate/TokenPriceUpdate'; +import Body from '../Typography/Body'; + +const TokenPriceTime = () => { + const DUMMY_TOKEN_PRICE = '120,582.08'; + const DUMMY_TOKEN_AMOUNT = 3515; + const DUMMY_TOKEN_SYMBOL = 'RBTC'; + const DUMMY_TOKEN_WON_CLAIMABLE = 562; + + return ( +
+
+ +
+

+ ${DUMMY_TOKEN_PRICE} +

+
+ +
+
+ token-pot-icon + + {DUMMY_TOKEN_AMOUNT} {DUMMY_TOKEN_SYMBOL} in the Pot! + +
+ + {DUMMY_TOKEN_WON_CLAIMABLE} {DUMMY_TOKEN_SYMBOL} Won{' '} + Claim Now! + +
+ ); +}; + +export default TokenPriceTime; diff --git a/src/apps/blitz/components/TokenPriceUpdate/TokenPriceUpdate.tsx b/src/apps/blitz/components/TokenPriceUpdate/TokenPriceUpdate.tsx new file mode 100644 index 00000000..0c9444a4 --- /dev/null +++ b/src/apps/blitz/components/TokenPriceUpdate/TokenPriceUpdate.tsx @@ -0,0 +1,24 @@ +import RandomToken from '../../images/randomToken.png'; + +type TokenPriceUpdateProps = { classname?: string }; + +const TokenPriceUpdate = ({ classname }: TokenPriceUpdateProps) => { + const DUMMY_TIME = '3:15pm'; + return ( +
+ token-icon +

+ Token price at{' '} + {DUMMY_TIME} +

+
+ ); +}; + +export default TokenPriceUpdate; diff --git a/src/apps/blitz/components/Typography/Body.tsx b/src/apps/blitz/components/Typography/Body.tsx new file mode 100644 index 00000000..f510eb65 --- /dev/null +++ b/src/apps/blitz/components/Typography/Body.tsx @@ -0,0 +1,12 @@ +import { ReactNode } from 'react'; + +type BodyProps = { + children: ReactNode; + className?: string; +}; + +const Body = ({ children, className }: BodyProps) => { + return

{children}

; +}; + +export default Body; diff --git a/src/apps/blitz/components/Typography/BodySmall.tsx b/src/apps/blitz/components/Typography/BodySmall.tsx new file mode 100644 index 00000000..4c2105e5 --- /dev/null +++ b/src/apps/blitz/components/Typography/BodySmall.tsx @@ -0,0 +1,12 @@ +import { ReactNode } from 'react'; + +type BodySmallProps = { + children: ReactNode; + className?: string; +}; + +const BodySmall = ({ children, className }: BodySmallProps) => { + return

{children}

; +}; + +export default BodySmall; diff --git a/src/apps/blitz/components/Typography/tests/Body.test.tsx b/src/apps/blitz/components/Typography/tests/Body.test.tsx new file mode 100644 index 00000000..f12dfbee --- /dev/null +++ b/src/apps/blitz/components/Typography/tests/Body.test.tsx @@ -0,0 +1,30 @@ +import renderer, { ReactTestRendererJSON } from 'react-test-renderer'; + +// components +import Body from '../Body'; + +describe('', () => { + it('renders correctly', () => { + const tree = renderer + .create( + <> + Some regular text. + Some red text + Some text with font size 23px + + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); + + const treeElements = tree as ReactTestRendererJSON[]; + + expect(treeElements[0].children?.length).toBe(1); + expect(treeElements[0].children?.[0]).toBe('Some regular text.'); + expect(treeElements[0].type).toBe('p'); + expect(treeElements[0].props.className).toContain('text-base'); + expect(treeElements[0].props.className).toContain('font-medium'); + expect(treeElements[1].props.className).toContain('text-red-500'); + expect(treeElements[2].props.className).toContain('text-[23px]'); + }); +}); diff --git a/src/apps/blitz/components/Typography/tests/BodySmall.test.tsx b/src/apps/blitz/components/Typography/tests/BodySmall.test.tsx new file mode 100644 index 00000000..5cdee9ca --- /dev/null +++ b/src/apps/blitz/components/Typography/tests/BodySmall.test.tsx @@ -0,0 +1,32 @@ +import renderer, { ReactTestRendererJSON } from 'react-test-renderer'; + +// components +import BodySmall from '../BodySmall'; + +describe('', () => { + it('renders correctly', () => { + const tree = renderer + .create( + <> + Some small text. + Some red small text + + Some small text with font size 7px + + + ) + .toJSON(); + + expect(tree).toMatchSnapshot(); + + const treeElements = tree as ReactTestRendererJSON[]; + + expect(treeElements[0].children?.length).toBe(1); + expect(treeElements[0].children?.[0]).toBe('Some small text.'); + expect(treeElements[0].type).toBe('p'); + expect(treeElements[0].props.className).toContain('text-sm'); + expect(treeElements[0].props.className).toContain('font-medium'); + expect(treeElements[1].props.className).toContain('text-red-500'); + expect(treeElements[2].props.className).toContain('text-[7px]'); + }); +}); diff --git a/src/apps/blitz/components/Typography/tests/__snapshots__/Body.test.tsx.snap b/src/apps/blitz/components/Typography/tests/__snapshots__/Body.test.tsx.snap new file mode 100644 index 00000000..27f96970 --- /dev/null +++ b/src/apps/blitz/components/Typography/tests/__snapshots__/Body.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` +[ +

+ Some regular text. +

, +

+ Some red text +

, +

+ Some text with font size 23px +

, +] +`; diff --git a/src/apps/blitz/components/Typography/tests/__snapshots__/BodySmall.test.tsx.snap b/src/apps/blitz/components/Typography/tests/__snapshots__/BodySmall.test.tsx.snap new file mode 100644 index 00000000..a0bccf69 --- /dev/null +++ b/src/apps/blitz/components/Typography/tests/__snapshots__/BodySmall.test.tsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders correctly 1`] = ` +[ +

+ Some small text. +

, +

+ Some red small text +

, +

+ Some small text with font size 7px +

, +] +`; diff --git a/src/apps/blitz/components/UserInfo/UserInfo.tsx b/src/apps/blitz/components/UserInfo/UserInfo.tsx new file mode 100644 index 00000000..427eb650 --- /dev/null +++ b/src/apps/blitz/components/UserInfo/UserInfo.tsx @@ -0,0 +1,65 @@ +import { + Copy as CopyIcon, + CopySuccess as CopySuccessIcon, +} from 'iconsax-react'; +import { useCallback, useEffect, useState } from 'react'; +import CopyToClipboard from 'react-copy-to-clipboard'; + +// utils + +// components +import RandomAvatar from '../RandomAvatar/RandomAvatar'; +import Body from '../Typography/Body'; + +const UserInfo = () => { + const DUMMY_WALLET_ADDRESS = '0x4a5e60B1F60E1A23B45C67D89E0F123456789ABCD'; + const DUMMY_USERNAME = 'Tester124'; + + const [copied, setCopied] = useState(false); + + const onCopyCodeClick = useCallback(() => { + if (copied) { + setCopied(false); + return; + } + + setCopied(true); + }, [copied]); + + useEffect(() => { + const codeCopyActionTimeout = setTimeout(() => { + setCopied(false); + }, 500); + + return () => { + clearTimeout(codeCopyActionTimeout); + }; + }, [copied]); + + return ( +
+
+ +
+
+ {DUMMY_USERNAME ?? {DUMMY_USERNAME}} +
+

+ {DUMMY_WALLET_ADDRESS} +

+ +
+ {copied ? ( + + ) : ( + + )} +
+
+
+
+
+ ); +}; + +export default UserInfo; diff --git a/src/apps/blitz/components/VoteInfoButton/VoteInfoButton.tsx b/src/apps/blitz/components/VoteInfoButton/VoteInfoButton.tsx new file mode 100644 index 00000000..e25146af --- /dev/null +++ b/src/apps/blitz/components/VoteInfoButton/VoteInfoButton.tsx @@ -0,0 +1,63 @@ +import { FaArrowDown, FaArrowUp } from 'react-icons/fa'; +import { TbTriangleFilled, TbTriangleInvertedFilled } from 'react-icons/tb'; +import UsersVotesIcon from '../../images/users_votes_icon.svg'; + +type VoteInfoButtonProps = { + type: 'up' | 'down'; +}; + +const VoteInfoButton = ({ type }: VoteInfoButtonProps) => { + const DUMMY_VOTES_NUMBER = 15; + const DUMMY_VOTES_PERCENTAGE = 71.43982; + + return ( +
+
+
+

+ Voted +

+ {type === 'up' ? ( + + ) : ( + + )} +
+
+ token-pot-icon +

+ {DUMMY_VOTES_NUMBER} +

+
+
+

+ {DUMMY_VOTES_PERCENTAGE.toFixed(1)}% +

+ +
+ ); +}; + +export default VoteInfoButton; diff --git a/src/apps/blitz/components/VotersList/VotersList.tsx b/src/apps/blitz/components/VotersList/VotersList.tsx new file mode 100644 index 00000000..258b89d7 --- /dev/null +++ b/src/apps/blitz/components/VotersList/VotersList.tsx @@ -0,0 +1,84 @@ +import { useState } from 'react'; +import { TbTriangleFilled, TbTriangleInvertedFilled } from 'react-icons/tb'; +import UserInfo from '../UserInfo/UserInfo'; + +const Tab = ({ + value, + activeTab, + setActiveTab, +}: { + value: 'up' | 'down'; + activeTab: 'up' | 'down'; + setActiveTab: (value: 'up' | 'down') => void; +}) => { + const DUMMY_VOTES_PERCENTAGE = 71.43982; + return ( + + ); +}; + +const VotersList = () => { + const [activeTab, setActiveTab] = useState<'up' | 'down'>('up'); + + return ( +
+ {/* Desktop View */} +
+
+
+

Voted

+ +
+ {Array.from({ length: 10 }).map((_, index) => ( + + ))} +
+
+
+

Voted

+ +
+ {Array.from({ length: 4 }).map((_, index) => ( + + ))} +
+
+ + {/* Tablet and Mobile View with Tabs */} +
+
+ + +
+ {activeTab === 'up' && + Array.from({ length: 10 }).map((_, index) => ( + + ))} + {activeTab === 'down' && + Array.from({ length: 4 }).map((_, index) => )} +
+
+ ); +}; + +export default VotersList; diff --git a/src/apps/blitz/icon.png b/src/apps/blitz/icon.png new file mode 100644 index 00000000..c4da0a66 Binary files /dev/null and b/src/apps/blitz/icon.png differ diff --git a/src/apps/blitz/images/Blitz_Logo.png b/src/apps/blitz/images/Blitz_Logo.png new file mode 100644 index 00000000..cf0cedd8 Binary files /dev/null and b/src/apps/blitz/images/Blitz_Logo.png differ diff --git a/src/apps/blitz/images/pillarX_full_white.png b/src/apps/blitz/images/pillarX_full_white.png new file mode 100644 index 00000000..b0a05436 Binary files /dev/null and b/src/apps/blitz/images/pillarX_full_white.png differ diff --git a/src/apps/blitz/images/randomToken.png b/src/apps/blitz/images/randomToken.png new file mode 100644 index 00000000..9d12bb27 Binary files /dev/null and b/src/apps/blitz/images/randomToken.png differ diff --git a/src/apps/blitz/images/token_pot_icon.svg b/src/apps/blitz/images/token_pot_icon.svg new file mode 100644 index 00000000..2e0b2f49 --- /dev/null +++ b/src/apps/blitz/images/token_pot_icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/apps/blitz/images/users_votes_icon.svg b/src/apps/blitz/images/users_votes_icon.svg new file mode 100644 index 00000000..7049dc97 --- /dev/null +++ b/src/apps/blitz/images/users_votes_icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/apps/blitz/index.tsx b/src/apps/blitz/index.tsx new file mode 100644 index 00000000..49354c66 --- /dev/null +++ b/src/apps/blitz/index.tsx @@ -0,0 +1,78 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import { useEffect, useState } from 'react'; +import styled from 'styled-components'; +import TokenPriceTime from './components/TokenPriceTime/TokenPriceTime'; +import VoteInfoButton from './components/VoteInfoButton/VoteInfoButton'; +import VotersList from './components/VotersList/VotersList'; +import BlitzLogo from './images/Blitz_Logo.png'; +import PillarXLogo from './images/pillarX_full_white.png'; +import './styles/tailwindBlitz.css'; + +const App = () => { + const [isDesktop, setIsDesktop] = useState(window.innerWidth > 1023); + + useEffect(() => { + const handleResize = () => { + setIsDesktop(window.innerWidth > 1023); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return ( + + pillar-x-logo + blitz-logo + {isDesktop ? ( +
+ + + +
+ ) : ( +
+ +
+ + +
+
+ )} + +
+ ); +}; + +const Wrapper = styled.div` + display: flex; + width: 100%; + margin: 0 auto; + flex-direction: column; + max-width: 1248px; + + @media (min-width: 1024px) { + padding: 52px 62px; + } + + @media (max-width: 1024px) { + padding: 52px 32px; + } + + @media (max-width: 768px) { + padding: 32px 16px; + } +`; + +export default App; diff --git a/src/apps/blitz/manifest.json b/src/apps/blitz/manifest.json new file mode 100644 index 00000000..60246cc5 --- /dev/null +++ b/src/apps/blitz/manifest.json @@ -0,0 +1,9 @@ +{ + "title": "Blitz", + "description": "Blitz by PillarX", + "translations": { + "en": { + "title": "Blitz by PillarX" + } + } +} diff --git a/src/apps/blitz/styles/tailwindBlitz.css b/src/apps/blitz/styles/tailwindBlitz.css new file mode 100644 index 00000000..ecd10c43 --- /dev/null +++ b/src/apps/blitz/styles/tailwindBlitz.css @@ -0,0 +1,48 @@ +@config '../tailwind.blitz.config.js'; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +[type='text'], +input:where(:not([type])), +[type='email'], +[type='url'], +[type='password'], +[type='number'], +[type='date'], +[type='datetime-local'], +[type='month'], +[type='search'], +[type='tel'], +[type='time'], +[type='week'], +[multiple], +textarea, +select { + appearance: none; + background-color: transparent; + border-color: unset; + border-width: 0; + border-radius: unset; + padding: unset; + font-size: unset; + line-height: unset; +} + +/* Override default focus colors for tailwindcss-forms https://github.com/tailwindlabs/tailwindcss-forms */ +[type='text']:focus, +[type='email']:focus, +[type='url']:focus, +[type='password']:focus, +[type='number']:focus, +[type='date']:focus, +[type='search']:focus, +[type='checkbox']:focus, +[type='radio']:focus, +[multiple]:focus, +textarea:focus, +select:focus { + --tw-ring-color: '#5e00ff'; + border-color: '#5e00ff'; +} diff --git a/src/apps/blitz/tailwind.blitz.config.js b/src/apps/blitz/tailwind.blitz.config.js new file mode 100644 index 00000000..0f52a8d0 --- /dev/null +++ b/src/apps/blitz/tailwind.blitz.config.js @@ -0,0 +1,33 @@ +/** @type {import('tailwindcss').Config} */ +// eslint-disable-next-line no-undef +module.exports = { + content: [ + './src/apps/blitz/**/**/*.{js,ts,jsx,tsx,html,mdx}', + './src/apps/blitz/**/*.{js,ts,jsx,tsx,html,mdx}', + ], + darkMode: 'class', + theme: { + screens: { + desktop: { min: '1024px' }, + tablet: { max: '1023px' }, + mobile: { max: '768px' }, + xs: { max: '470px' }, + }, + extend: { + colors: { + deep_purple: { A700: '#5e00ff' }, + container_grey: '#27262F', + medium_grey: '#312F3A', + purple_light: '#E2DDFF', + purple_medium: '#8A77FF', + percentage_green: '#5DC787', + percentage_red: '#FF366C', + light_grey: '#A9BBD3', + }, + fontFamily: { + custom: ['Formular'], + }, + }, + }, + plugins: [], +};