Skip to content

Commit

Permalink
Start frontend design
Browse files Browse the repository at this point in the history
  • Loading branch information
CRBl69 committed Jan 21, 2025
1 parent 463707b commit 5786707
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 85 deletions.
41 changes: 31 additions & 10 deletions src/typescript/frontend/src/app/arena/page.tsx
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!} />;
}
63 changes: 63 additions & 0 deletions src/typescript/frontend/src/components/Countdown.tsx
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 src/typescript/frontend/src/components/pages/arena/ArenaClient.tsx
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} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ import Link from "next/link";
import { ROUTES } from "router/routes";
import { Emoji } from "utils/emoji";
import "./module.css";
import { useInterval } from "react-use";
import { useMemo, useState } from "react";
import darkTheme from "theme/dark";
import { useMatchBreakpoints } from "@hooks/index";
import { getEmojisInString } from "@sdk/emoji_data";
import { Countdown } from "components/Countdown";

type ArenaCardProps = {
market0Symbol: string;
Expand All @@ -23,66 +21,6 @@ type ArenaCardProps = {
duration: bigint;
};

const TimerNumber = ({ 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>
);

const Timer = ({ 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.round(remaining) % 60, 0)
.toString()
.padStart(2, "0"),
[remaining]
);
const minutes = useMemo(
() =>
Math.max(Math.round(remaining / 60) % 60, 0)
.toString()
.padStart(2, "0"),
[remaining]
);
const hours = useMemo(
() =>
Math.max(Math.round(remaining / 60 / 60), 0)
.toString()
.padStart(2, "0"),
[remaining]
);

return (
<div className="text-light-gray flex flex-row pixel-clock w-min">
<TimerNumber n={hours.split("")[0]} />
<TimerNumber n={hours.split("")[1]} />
<div className="my-auto w-[1ch] text-center">:</div>
<TimerNumber n={minutes.split("")[0]} />
<TimerNumber n={minutes.split("")[1]} />
<div className="my-auto w-[1ch] text-center">:</div>
<TimerNumber n={seconds.split("")[0]} />
<TimerNumber n={seconds.split("")[1]} />
</div>
);
};

const GlowingEmoji = ({ emoji }: { emoji: string }) => (
<div
className={`relative grid items-center place-items-center symbol-${getEmojisInString(emoji).length}`}
Expand Down Expand Up @@ -147,7 +85,7 @@ export const ArenaCard = ({
</Link>
<div className={`flex flex-col gap-[2em] max-w-full ${isMobile ? "items-center" : ""}`}>
{!isMobile && headerText}
<Timer duration={duration} startTime={startTime} />
<Countdown duration={duration} startTime={startTime} />

<div className="flex flex-col gap-[.4em] arena-market-data-text">
<FlexGap gap="8px">
Expand Down

0 comments on commit 5786707

Please sign in to comment.