-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
262 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,47 @@ | ||
import { fetchMarketStateByAddress, fetchMelee } from "@/queries/arena"; | ||
import { type fetchArenaInfo, fetchMarketStateByAddress } from "@/queries/arena"; | ||
import { ArenaClient } from "components/pages/arena/ArenaClient"; | ||
import { redirect } from "next/navigation"; | ||
|
||
export const revalidate = 2; | ||
|
||
export default async function Arena() { | ||
let melee: Awaited<ReturnType<typeof fetchMelee>> = null; | ||
let arenaInfo: Awaited<ReturnType<typeof fetchArenaInfo>> = null; | ||
try { | ||
melee = await fetchMelee({}); | ||
// arenaInfo = await fetchArenaInfo({}); | ||
} catch (e) { | ||
console.warn("Could not get melee data. This probably means that the backend is running an outdated version of the processor, without the arena processing. Please update.") | ||
console.warn( | ||
"Could not get melee data. This probably means that the backend is running an outdated version of the processor, without the arena processing. Please update." | ||
); | ||
redirect("/home"); | ||
} | ||
|
||
if (!melee) { | ||
redirect("/home"); | ||
} | ||
//if (!melee) { | ||
// redirect("/home"); | ||
//} | ||
|
||
arenaInfo = { | ||
arenaInfo: { | ||
duration: 120n * 1000n * 1000n, | ||
startTime: BigInt(new Date().getTime() * 1000 - 1000 * 1000 * 60), | ||
volume: 123n * 10n ** 8n, | ||
meleeId: 2n, | ||
aptLocked: 12n * 10n ** 8n, | ||
maxMatchAmount: 5n * 10n ** 8n, | ||
rewardsRemaining: 12345n * 10n ** 6n, | ||
maxMatchPercentage: 50n, | ||
emojicoin0MarketAddress: "0x43dcf02dcc0f3759d00486052585bf1694acf85c7e3e7c4b4770c5216d58eb67", | ||
emojicoin1MarketAddress: "0x43dcf02dcc0f3759d00486052585bf1694acf85c7e3e7c4b4770c5216d58eb67", | ||
}, | ||
}; | ||
|
||
const [market0, market1] = await Promise.all([ | ||
fetchMarketStateByAddress({ address: melee.arenaMelee.emojicoin0MarketAddress }), | ||
fetchMarketStateByAddress({ address: melee.arenaMelee.emojicoin1MarketAddress }), | ||
fetchMarketStateByAddress({ | ||
address: "0x43dcf02dcc0f3759d00486052585bf1694acf85c7e3e7c4b4770c5216d58eb67", | ||
}), | ||
fetchMarketStateByAddress({ | ||
address: "0x43dcf02dcc0f3759d00486052585bf1694acf85c7e3e7c4b4770c5216d58eb67", | ||
}), | ||
]); | ||
|
||
return <ArenaClient melee={melee} market0={market0!} market1={market1!} />; | ||
return <ArenaClient arenaInfo={arenaInfo} market0={market0!} market1={market1!} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { useMemo, useState } from "react"; | ||
import { useInterval } from "react-use"; | ||
import darkTheme from "theme/dark"; | ||
|
||
const CountdownNumber = ({ n }: { n: string }) => ( | ||
<div | ||
style={{ | ||
border: `1px solid ${darkTheme.colors.darkGray}`, | ||
borderRadius: "5px", | ||
background: "black", | ||
width: "1.7ch", | ||
textAlign: "center", | ||
margin: ".1em", | ||
paddingLeft: ".05em", | ||
color: darkTheme.colors.econiaBlue, | ||
}} | ||
> | ||
{n} | ||
</div> | ||
); | ||
|
||
export const Countdown = ({ startTime, duration }: { startTime: bigint; duration: bigint }) => { | ||
const getRemaining = () => Number(duration) - (new Date().getTime() / 1000 - Number(startTime)); | ||
const [remaining, setRemaining] = useState<number>(getRemaining()); | ||
useInterval(() => { | ||
setRemaining(getRemaining()); | ||
}, 1000); | ||
|
||
const seconds = useMemo( | ||
() => | ||
Math.max(Math.floor(remaining) % 60, 0) | ||
.toString() | ||
.padStart(2, "0"), | ||
[remaining] | ||
); | ||
const minutes = useMemo( | ||
() => | ||
Math.max(Math.floor(remaining / 60) % 60, 0) | ||
.toString() | ||
.padStart(2, "0"), | ||
[remaining] | ||
); | ||
const hours = useMemo( | ||
() => | ||
Math.max(Math.floor(remaining / 60 / 60), 0) | ||
.toString() | ||
.padStart(2, "0"), | ||
[remaining] | ||
); | ||
|
||
return ( | ||
<div className="text-light-gray flex flex-row pixel-clock w-min"> | ||
<CountdownNumber n={hours.split("")[0]} /> | ||
<CountdownNumber n={hours.split("")[1]} /> | ||
<div className="my-auto w-[1ch] text-center">:</div> | ||
<CountdownNumber n={minutes.split("")[0]} /> | ||
<CountdownNumber n={minutes.split("")[1]} /> | ||
<div className="my-auto w-[1ch] text-center">:</div> | ||
<CountdownNumber n={seconds.split("")[0]} /> | ||
<CountdownNumber n={seconds.split("")[1]} /> | ||
</div> | ||
); | ||
}; |
177 changes: 166 additions & 11 deletions
177
src/typescript/frontend/src/components/pages/arena/ArenaClient.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,177 @@ | ||
"use client"; | ||
|
||
import type { ArenaMeleeModel, MarketStateModel } from "@sdk/indexer-v2/types"; | ||
import { useMatchBreakpoints } from "@hooks/index"; | ||
import type { ArenaInfoModel, MarketStateModel } from "@sdk/indexer-v2/types"; | ||
import { Countdown } from "components/Countdown"; | ||
import { FormattedNumber } from "components/FormattedNumber"; | ||
import type { CSSProperties } from "react"; | ||
import { createPortal } from "react-dom"; | ||
import { emoji } from "utils"; | ||
import { Emoji } from "utils/emoji"; | ||
|
||
export const ArenaClient = ({ | ||
melee, | ||
market0, | ||
market1, | ||
}: { | ||
melee: ArenaMeleeModel; | ||
type Props = { | ||
arenaInfo: ArenaInfoModel; | ||
market0: MarketStateModel; | ||
market1: MarketStateModel; | ||
}; | ||
|
||
const Box: React.FC<React.PropsWithChildren<{ className?: string; style?: CSSProperties }>> = ({ | ||
children, | ||
className, | ||
style, | ||
}) => { | ||
return ( | ||
<div | ||
className={`border-solid border-[1px] border-dark-gray rounded-[3px] bg-black/50 ${className}`} | ||
style={style} | ||
> | ||
{children} | ||
</div> | ||
); | ||
}; | ||
|
||
const EmojiTitle = ({ | ||
market0Symbols, | ||
market1Symbols, | ||
}: { | ||
market0Symbols: string; | ||
market1Symbols: string; | ||
}) => { | ||
const { isTablet, isLaptop } = useMatchBreakpoints(); | ||
const emojiCount = market0Symbols.length + market1Symbols.length; | ||
// Formula to get a good font size for the amount of emojis to display. | ||
// Works great for up to 6 emojis (not tested for more, but might work as well). | ||
const baseFontSize = | ||
72 * (isTablet ? 0.6 : 1) * (isLaptop ? 0.69 : 1) * (1 - (emojiCount * 5) / 6 / 10); | ||
return ( | ||
<div className="flex flex-col gap-[1em] text-ec-blue"> | ||
<div>ID: {melee.arenaMelee.meleeId.toString()}</div> | ||
<div> | ||
{market0.market.symbolEmojis.join("")} vs {market1.market.symbolEmojis.join("")} | ||
<div className="text-center uppercase" style={{ fontSize: baseFontSize + "px" }}> | ||
<Emoji emojis={market0Symbols} />{" "} | ||
<span style={{ fontSize: baseFontSize * 1.2 + "px" }} className="text-light-gray"> | ||
vs | ||
</span>{" "} | ||
<Emoji emojis={market1Symbols} /> | ||
</div> | ||
); | ||
}; | ||
|
||
const RewardsRemainingBox = ({ rewardsRemaining }: { rewardsRemaining: bigint }) => { | ||
const { isMobile } = useMatchBreakpoints(); | ||
return ( | ||
<Box | ||
style={{ | ||
display: "grid", | ||
gridTemplateRows: "auto 1fr", | ||
placeItems: "center", | ||
padding: "1em", | ||
}} | ||
> | ||
<div | ||
className={`uppercase ${isMobile ? "text-2xl" : "text-3xl"} text-light-gray tracking-widest text-center`} | ||
> | ||
Rewards remaining | ||
</div> | ||
<div | ||
className={`uppercase font-forma ${isMobile ? "text-4xl" : "text-6xl lg:text-7xl xl:text-8xl"} text-white`} | ||
> | ||
<FormattedNumber value={rewardsRemaining} nominalize /> | ||
</div> | ||
</Box> | ||
); | ||
}; | ||
|
||
const Desktop: React.FC<Props> = ({ arenaInfo, market0, market1 }) => { | ||
return ( | ||
<div | ||
style={{ | ||
display: "grid", | ||
gridTemplateRows: "auto 1fr", | ||
gridTemplateColumns: "1fr 0.65fr 0.85fr 1fr", | ||
height: "100%", | ||
width: "100%", | ||
padding: "2em", | ||
gap: "2em", | ||
}} | ||
> | ||
<Box className="grid place-items-center"> | ||
<EmojiTitle | ||
market0Symbols={market0.market.symbolEmojis.join("")} | ||
market1Symbols={market1.market.symbolEmojis.join("")} | ||
/> | ||
</Box> | ||
<Box className="col-start-2 col-end-4 text-5xl lg:text-6xl xl:text-7xl grid place-items-center"> | ||
<Countdown | ||
startTime={arenaInfo.arenaInfo.startTime / 1000n / 1000n} | ||
duration={arenaInfo.arenaInfo.duration / 1000n / 1000n} | ||
/> | ||
</Box> | ||
<RewardsRemainingBox rewardsRemaining={arenaInfo.arenaInfo.rewardsRemaining} /> | ||
<Box className="col-start-1 col-end-3"></Box> | ||
<Box className="col-start-3 col-end-5"></Box> | ||
</div> | ||
); | ||
}; | ||
|
||
const BottomNavigationItem = ({ | ||
emoji, | ||
text, | ||
onClick, | ||
}: { | ||
emoji: string; | ||
text: string; | ||
onClick?: () => void; | ||
}) => { | ||
return ( | ||
<div onClick={onClick} className="flex flex-col place-items-center"> | ||
<Emoji emojis={emoji} /> | ||
<div className="uppercase tracking-widest text-light-gray">{text}</div> | ||
</div> | ||
); | ||
}; | ||
|
||
const BottomNavigation = () => { | ||
return ( | ||
<div | ||
className="fixed bottom-0 w-[100%] border-solid border-t-[1px] border-dark-gray h-[4em] bg-black/50" | ||
style={{ | ||
display: "grid", | ||
gridTemplateColumns: "1fr 1fr 1fr 1fr", | ||
placeItems: "center", | ||
}} | ||
> | ||
<BottomNavigationItem emoji={emoji("smiling face with horns")} text="enter" /> | ||
<BottomNavigationItem emoji={emoji("ninja")} text="profile" /> | ||
<BottomNavigationItem emoji={emoji("left speech bubble")} text="chat" /> | ||
<BottomNavigationItem emoji={emoji("books")} text="info" /> | ||
</div> | ||
); | ||
}; | ||
|
||
const Mobile: React.FC<Props> = ({ arenaInfo, market0, market1 }) => { | ||
return ( | ||
<> | ||
<div className="flex flex-col gap-[1em] h-[100%] w-[100%] p-[1em]"> | ||
<Box className="grid place-items-center gap-[1em] py-[1em]"> | ||
<EmojiTitle | ||
market0Symbols={market0.market.symbolEmojis.join("")} | ||
market1Symbols={market1.market.symbolEmojis.join("")} | ||
/> | ||
<div className="text-4xl"> | ||
<Countdown | ||
startTime={arenaInfo.arenaInfo.startTime / 1000n / 1000n} | ||
duration={arenaInfo.arenaInfo.duration / 1000n / 1000n} | ||
/> | ||
</div> | ||
</Box> | ||
<RewardsRemainingBox rewardsRemaining={arenaInfo.arenaInfo.rewardsRemaining} /> | ||
<Box className="h-[500px]"></Box> | ||
<Box className="h-[500px]"></Box> | ||
</div> | ||
{createPortal(<BottomNavigation />, document.body)} | ||
</> | ||
); | ||
}; | ||
|
||
export const ArenaClient = (props: Props) => { | ||
const { isMobile } = useMatchBreakpoints(); | ||
return isMobile ? <Mobile {...props} /> : <Desktop {...props} />; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters