Skip to content

Commit

Permalink
Integrate Leaderboad Backend (#1858)
Browse files Browse the repository at this point in the history
  • Loading branch information
AtelyPham authored Nov 28, 2023
1 parent 79891dd commit 40a2142
Show file tree
Hide file tree
Showing 27 changed files with 1,044 additions and 13 deletions.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,10 @@ BRIDGE_DAPP_TANGLE_MULTICALL3_DEPLOYMENT_BLOCK=0

VANCHOR_ADDRESSES='["<VANCHOR_ADDRESS>"]'
ACTIVE_SUBGRAPHS="local"

# Testnet Leaderboard
# Format yyyy-mm-dd
TESTNET_LEADERBOARD_END_DATE="2023-12-31"
TESTNET_LEADERBOARD_GUIDELINES_URL=""
TESTNET_LEADERBOARD_REQUEST_POINTS_URL=""
TESTNET_LEADERBOARD_BACKEND_URL=""
4 changes: 3 additions & 1 deletion apps/testnet-leaderboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ export const metadata: Metadata = {
description: 'Welcome to Testnet Leaderboard!',
metadataBase: process.env.URL
? new URL(process.env.URL)
: new URL(`http://localhost:${process.env.PORT}`),
: process.env.PORT
? new URL(`http://localhost:${process.env.PORT}`)
: null,
openGraph: {
title: 'Testnet Leaderboard',
description: 'Welcome to Testnet Leaderboard!',
Expand Down
34 changes: 32 additions & 2 deletions apps/testnet-leaderboard/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
export default async function Index() {
return <h1 className="text-mono-80 h1">Welcome to testnet leaderboard</h1>;
import cx from 'classnames';

import CountdownSection from '../components/CountdownSection';
import FAQSection from '../components/FAQSection';
import RankingTableSection from '../components/RankingTableSection';

export default function Index() {
return (
<div
className={cx(
'bg-body bg-repeat-y bg-center',
'py-[60px] md:py-[90px] px-2 lg:px-0'
)}
>
<div className="lg:max-w-[1440px] lg:mx-auto">
<div
className={cx(
'lg:w-[77.5%] lg:mx-auto py-[48px] px-2 lg:px-[192px]',
'border-2 border-mono-0 rounded-2xl',
'flex flex-col gap-[64px]',
'bg-[linear-gradient(180deg,rgba(255,255,255,0.2)_0%,rgba(255,255,255,0)_100%)]'
)}
>
<CountdownSection />
<hr className="border-mono-200" />
<RankingTableSection />
<hr className="border-mono-200" />
<FAQSection />
</div>
</div>
</div>
);
}
70 changes: 70 additions & 0 deletions apps/testnet-leaderboard/components/CountdownSection/Badges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
import capitalize from 'lodash/capitalize';
import type { FC } from 'react';

import { BADGE_ICON_RECORD } from '../../constants';
import { BadgeEnum } from '../../types';

const BADGE_TO_NAME = {
[BadgeEnum.CREATOR]: 'Content Creator',
[BadgeEnum.GOVERNANCE]: 'Governance Guardian',
[BadgeEnum.SPECIALIST]: 'Tx Specialist',
} as const satisfies Partial<{
[key in BadgeEnum]: string;
}>;

const getBadgeName = (badge: string) => {
if (badge in BADGE_TO_NAME) {
return BADGE_TO_NAME[badge as keyof typeof BADGE_TO_NAME];
}

return null;
};

const Badges = () => {
return (
<div className="flex flex-wrap justify-center gap-1">
{Object.entries(BADGE_ICON_RECORD).map(([badge, icon], idx) => {
const name = getBadgeName(badge) ?? badge;
const fmtName = name.split(' ').map(capitalize).join(' ');

return (
<Badge
key={`${badge}-${idx}`}
icon={icon}
name={fmtName}
shorthand={capitalize(badge)}
/>
);
})}
</div>
);
};

export default Badges;

/** @internal */
const Badge: FC<{ icon: string; name: string; shorthand?: string }> = ({
icon,
name,
shorthand,
}) => {
return (
<>
<Typography
variant="mkt-body2"
fw="bold"
className="hidden md:block bg-[rgba(31,29,43,0.1)] px-2 py-[2px] rounded-full mt-1"
>
{icon} {name}
</Typography>
<Typography
variant="mkt-body2"
fw="bold"
className="block md:hidden bg-[rgba(31,29,43,0.1)] px-2 py-[2px] rounded-full mt-1"
>
{icon} {shorthand || name}
</Typography>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use client';

import Button from '@webb-tools/webb-ui-components/components/buttons/Button';

import { GUIDELINES_URL, REQUEST_POINTS_URL } from '../../constants';

const CTAButtons = () => {
return (
<div className="flex items-center justify-center gap-4">
<Button
variant="secondary"
href={GUIDELINES_URL}
target="_blank"
className="px-5 md:px-9"
>
View Guidelines
</Button>
<Button
href={REQUEST_POINTS_URL}
target="_blank"
className="px-5 md:px-9"
>
Request Points
</Button>
</div>
);
};

export default CTAButtons;
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client';

import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
import cx from 'classnames';
import type { FC } from 'react';

import { END_DATE } from '../../constants';
import useTimeRemaining from '../../hooks/useTimeRemaining';

const TimeRemaining = () => {
const { days, hours, minutes, seconds } = useTimeRemaining(END_DATE);

return (
<div className="space-y-[32px]">
<Typography
variant="mkt-small-caps"
fw="bold"
className="text-center text-mono-200"
>
Time Remaining
</Typography>
<div className="flex justify-between gap-6 px-4 md:justify-center md:gap-12">
<CountdownItem
timeUnit="Days"
value={typeof days === 'number' ? days.toString() : null}
/>
<CountdownItem
timeUnit="Hours"
value={typeof hours === 'number' ? hours.toString() : null}
/>
<CountdownItem
timeUnit="Minutes"
value={typeof minutes === 'number' ? minutes.toString() : null}
/>
<CountdownItem
timeUnit="Seconds"
value={typeof seconds === 'number' ? seconds.toString() : null}
/>
</div>
</div>
);
};

export default TimeRemaining;

/** @internal */
const CountdownItem: FC<{ timeUnit: string; value: string | null }> = ({
timeUnit,
value,
}) => {
const convertedValue = !value
? '--'
: value.length === 1
? `0${value}`
: value;

return (
<div className="flex flex-col items-center w-full gap-3 md:w-auto">
<div className="flex w-full gap-3 md:w-auto md:gap-4">
{convertedValue.split('').map((char, idx) => (
<div
className={cx(
'w-full aspect-[5/8] md:aspect-auto md:w-[55px] md:h-[92px]',
'border border-mono-0 rounded-xl',
'flex justify-center items-center',
'bg-[linear-gradient(180deg,#FFFFFF_0%,rgba(255,255,255,0.5)_100%)]',
'shadow-[0_4px_4px_rgba(0,0,0,0.25)]'
)}
key={idx}
>
<Typography
variant="mkt-h3"
fw="black"
className="text-center text-mono-200"
>
{char}
</Typography>
</div>
))}
</div>
<Typography
variant="mkt-small-caps"
fw="black"
className="text-center text-mono-200"
>
{timeUnit}
</Typography>
</div>
);
};
56 changes: 56 additions & 0 deletions apps/testnet-leaderboard/components/CountdownSection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';

import Badges from './Badges';
import CTAButtons from './CTAButtons';
import TimeRemaining from './TimeRemaining';

const CountdownSection = () => {
return (
<div className="space-y-9">
<div className="space-y-4">
<Typography
variant="mkt-small-caps"
fw="black"
className="text-center text-purple-70"
>
NOW LIVE
</Typography>
<Typography
variant="mkt-h3"
fw="black"
className="text-center text-mono-200"
>
Tangle Testnet Leaderboard
</Typography>
<Typography
variant="mkt-body2"
fw="medium"
className="text-center text-mono-140"
>
Our Tangle testnet leaderboard highlights top contributors, ranked by
points earned through activities such as running validators, engaging
in governance, crafting protocol extensions, transacting, and more!
</Typography>
</div>

<TimeRemaining />

<div className="space-y-4">
<Typography
variant="mkt-body2"
fw="medium"
className="text-center text-mono-140"
>
Unlock points and badges as you engage in the Tangle Network. Explore
the guidelines or request points now!
</Typography>

<CTAButtons />
</div>

<Badges />
</div>
);
};

export default CountdownSection;
54 changes: 54 additions & 0 deletions apps/testnet-leaderboard/components/FAQSection/FAQAccordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use client';

import {
Accordion,
AccordionButton,
AccordionContent,
AccordionItem,
} from '@webb-tools/webb-ui-components/components/Accordion';
import { Typography } from '@webb-tools/webb-ui-components/typography/Typography';
import cx from 'classnames';
import type { FC, ReactNode } from 'react';

interface FAQAccordionProps {
items: Array<{ question: string; answer: string | ReactNode }>;
className?: string;
itemClassName?: string;
}

const FAQAccordion: FC<FAQAccordionProps> = ({
items,
className,
itemClassName,
}) => {
return (
<Accordion collapsible type="single" className={className}>
{items.map((item, index) => (
<AccordionItem
key={index}
value={item.question}
className={cx('p-0', itemClassName)}
>
<AccordionButton className="gap-2 px-0 py-4">
<Typography
variant="mkt-body2"
className="font-black text-mono-200"
>
{item.question}
</Typography>
</AccordionButton>
<AccordionContent className="px-0">
<Typography
variant="mkt-body2"
className="font-medium whitespace-pre-wrap text-mono-160"
>
{item.answer}
</Typography>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
);
};

export default FAQAccordion;
Loading

0 comments on commit 40a2142

Please sign in to comment.