diff --git a/src/components/index.ts b/src/components/index.ts index 65e8364a..5fdc6ca9 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,7 +10,13 @@ const Accordion = dynamic(async () => await import('@/components/accordion/Accor const Podium = dynamic(async () => await import('@/components/podium/Podium')) -const LeaderboardTable = dynamic(async () => await import('@/components/table/LeaderboardTable')) +const ValidatorsLeaderboardTable = dynamic( + async () => await import('@/components/table/validators/LeaderboardTable') +) + +const BuildersLeaderboardTable = dynamic( + async () => await import('@/components/table/builders/LeaderboardTable') +) const Countdown = dynamic(async () => await import('@/components/countdown/Countdown')) @@ -36,7 +42,8 @@ export { Accordion, BaseCard, Podium, - LeaderboardTable, + ValidatorsLeaderboardTable, + BuildersLeaderboardTable, Countdown, Snackbar, GoBackButton, diff --git a/src/components/table/builders/LeaderboardTable.tsx b/src/components/table/builders/LeaderboardTable.tsx new file mode 100644 index 00000000..a2d86cfb --- /dev/null +++ b/src/components/table/builders/LeaderboardTable.tsx @@ -0,0 +1,142 @@ +import React, { useCallback, useState, useMemo } from 'react' +import classNames from 'classnames' +import Snackbar from '@/components/snackbar/Snackbar' +import { Search } from '@/components/search/Search' +import { Copy } from '@/components/copy/Copy' +import { useMediaType } from '@/hook/useMediaType' +import type { Column } from '@/components/table/table.type' +import type { BuilderDescriptor, BuildersDescriptor } from '@/entity/builder' + +export type LeaderboardTable = { + data: BuildersDescriptor + onSearchChange: (value: string) => void +} + +const LeaderboardTable: React.FC = ({ data, onSearchChange }) => { + const [address, setAddress] = useState('') + const isMobileScreen = useMediaType('(max-width: 580px)') + + const handleCopyAddress = useCallback( + (address: string) => { + setAddress(address) + }, + [setAddress] + ) + + const columns: Column[] = useMemo( + () => + [ + { + label: 'Rank', + renderCell: (druid: BuilderDescriptor) => {druid.rank.toLocaleString()}, + width: isMobileScreen ? '18%' : '10%' + }, + { + label: 'Address', + renderCell: (druid: BuilderDescriptor) => ( +
+ {druid.valoper} + +
+ ), + width: isMobileScreen ? '50%' : '25%' + }, + { + label: 'Awarded POAP', + renderCell: (druid: BuilderDescriptor) => ( +
    + {druid.earnings.poap.map(item => ( +
  • {item}
  • + ))} +
+ ), + width: '35%', + hidden: isMobileScreen + }, + { + label: 'Awarded know', + renderCell: (druid: BuilderDescriptor) => ( + {druid.earnings.know.toLocaleString()} + ), + width: isMobileScreen ? '32%' : '30%' + } + ].filter(column => !column.hidden), + [handleCopyAddress, isMobileScreen] + ) + + const handleSearchChange = useCallback( + (value: string) => { + onSearchChange(value) + }, + [onSearchChange] + ) + + const handleSnackbarClose = useCallback(() => { + setAddress('') + }, [setAddress]) + + return ( + <> +
+
+

RANKING

+ +
+
+ + + + {columns.map(({ label, width }, index) => ( + + ))} + + + + {data.length > 0 && ( + + {data.map((row, index) => { + const podiumClassname = classNames({ + gold: row.rank === 1, + silver: row.rank === 2, + bronze: row.rank === 3 + }) + return ( + + {columns.map((column, index) => { + const { renderCell } = column + return + })} + {!isMobileScreen && ( + + ) + })} + + )} + {!data.length && ( + + + + )} + +
+ {label} +
{renderCell(row)} + )} +
+ No results found... +
+
+
+ + + ) +} + +export default LeaderboardTable diff --git a/src/components/table/builders/leaderboardTable.scss b/src/components/table/builders/leaderboardTable.scss new file mode 100644 index 00000000..58e4fc83 --- /dev/null +++ b/src/components/table/builders/leaderboardTable.scss @@ -0,0 +1,171 @@ +$podium-colors: ('gold', 'silver', 'bronze'); + +.okp4-nemeton-web-results-table-main { + display: flex; + align-items: center; + flex-direction: column; + align-items: baseline; + + .okp4-nemeton-web-results-table-header-container { + display: flex; + align-items: center; + width: 100%; + + @media screen and (max-width: 1000px) { + flex-direction: column; + gap: 28px; + } + } + + .okp4-nemeton-web-results-table-content-container { + width: 100%; + margin-top: 20px; + min-height: 400px; + padding-left: 17px; + + table { + width: 100%; + border-spacing: 0 12px; + table-layout: fixed; + + tr { + height: 48px; + } + + th { + font-family: Gotham bold, sans-serif; + line-height: 24px; + font-size: 14px; + color: rgba(255, 255, 255, 0.6); + text-transform: uppercase; + text-align: start; + + &:first-child { + padding-left: 41px; + + @media screen and (max-width: 1000px) { + padding-left: unset; + } + } + } + + tbody { + tr { + font-family: Gotham light, sans-serif; + background: none; + border-radius: 6px; + position: relative; + transform: scale(1); + height: 48px; + + &.gold { + background: linear-gradient(90deg, $choco-100 0%, rgba(209, 193, 147, 0) 100%); + } + + &.silver { + background: linear-gradient(90deg, $white-200 0%, rgba(180, 184, 187, 0) 100%); + } + + &.bronze { + background: linear-gradient(90deg, $choco 0%, rgba(180, 150, 137, 0) 100%); + } + + &.gold, + &.silver, + &.bronze { + font-family: Gotham, sans-serif; + font-weight: 700; + background-attachment: fixed; + } + + .okp4-nemeton-web-results-table-podium-logo { + position: absolute; + width: 39px; + height: 39px; + background-repeat: no-repeat; + background-position: 0; + left: -14px; + top: -6px; + bottom: 0; + + @each $podium-color in $podium-colors { + &.#{$podium-color} { + background-image: url('/icons/#{$podium-color}-druid.svg'); + } + } + } + + .flex-cell, + a { + @include flex-rows-with-gap(8px); + align-items: center; + max-width: 95%; + + > img:first-of-type { + border-radius: 4px; + } + + .okp4-nemeton-web-copy-logo { + width: 100%; + } + } + + td { + font-size: 16px; + color: $white; + border: unset; + padding: 11px 0; + vertical-align: top; + + &:nth-child(2) { + font-family: Gotham light, sans-serif; + } + + &:nth-child(3) { + li { + font-family: 'Gotham light', sans-serif; + } + } + + &:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + padding-left: 41px; + + @media screen and (max-width: 1000px) { + padding-left: 15px; + } + } + + &:last-child { + border-bottom-right-radius: 6px; + border-top-right-radius: 6px; + } + + span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + ul { + list-style: none; + padding: 0; + margin: 0; + + li { + color: $white; + font-size: 16px; + font-family: Gotham; + line-height: 24px; + margin: 0; + padding: 0; + } + } + } + } + } + } +} + diff --git a/src/components/table/LeaderboardTable.tsx b/src/components/table/validators/LeaderboardTable.tsx similarity index 98% rename from src/components/table/LeaderboardTable.tsx rename to src/components/table/validators/LeaderboardTable.tsx index 6d3cd815..2992e07d 100644 --- a/src/components/table/LeaderboardTable.tsx +++ b/src/components/table/validators/LeaderboardTable.tsx @@ -2,7 +2,7 @@ import { Skeleton } from '@mui/material' import classNames from 'classnames' import Image from 'next/image' import Link from 'next/link' -import hatDruidAnimationData from '../../../public/json/hat-druid.json' +import hatDruidAnimationData from '../../../../public/json/hat-druid.json' import React, { useCallback, useMemo, useState } from 'react' import { useMediaType } from '@/hook/useMediaType' import type { DruidDescriptor } from '@/entity/druid' @@ -10,7 +10,7 @@ import { Search } from '@/components/search/Search' import Snackbar from '@/components/snackbar/Snackbar' import LottieLoader from '@/components/loader/LottieLoader' import { Copy } from '@/components/copy/Copy' -import type { Column } from './table.type' +import type { Column } from '../table.type' export type LeaderboardTableProps = { data: DruidDescriptor[] diff --git a/src/components/taskContentIcon/TaskContentIcon.tsx b/src/components/taskContentIcon/TaskContentIcon.tsx index 2e6fe2a0..f7365dd7 100644 --- a/src/components/taskContentIcon/TaskContentIcon.tsx +++ b/src/components/taskContentIcon/TaskContentIcon.tsx @@ -4,7 +4,7 @@ import ArticleIcon from '@mui/icons-material/Article' import GavelIcon from '@mui/icons-material/Gavel' import HelpIcon from '@mui/icons-material/Help' import MoneyIcon from '@mui/icons-material/Money' -import type { TaskContentId,ChallengeTaskContentId } from '@/data/phase/dto.type' +import type { TaskContentId, ChallengeTaskContentId } from '@/data/phase/dto.type' type TaskContentIconProps = { id: TaskContentId | ChallengeTaskContentId diff --git a/src/data/builder/builder.ts b/src/data/builder/builder.ts new file mode 100644 index 00000000..5f7c05b1 --- /dev/null +++ b/src/data/builder/builder.ts @@ -0,0 +1,203 @@ +import type { BuildersDTO } from './dto.type' + +export const buildersData: BuildersDTO = [ + { + rank: 1, + valoper: 'okp41u6vp630kpjpxqp2p6xwagtlkzq58tw3zadwrgu', + poap: [ + 'OKP4 Ontologist Apprentice', + 'OKP4 Governance Creator', + 'Dagda Servant', + 'OKP4 Law-Stone Builder', + 'OKP4 Objectarium Explorer', + 'OKP4 Young Druid' + ], + know: 30000 + }, + { + rank: 2, + valoper: 'okp41wy8ywt98sv5pmsg873rct4pxtw0ntxuwxdtzen', + poap: [ + 'OKP4 Ontologist Apprentice', + 'OKP4 Governance Creator', + 'OKP4 Law-Stone Builder', + 'OKP4 Objectarium Explorer', + 'OKP4 Young Druid' + ], + know: 30000 + }, + { + rank: 3, + valoper: 'okp41hkf8uj3yg9v2dwdq6ygapf7gyjnjvuamgz6m0g', + poap: ['OKP4 Law-Stone Builder', 'OKP4 Objectarium Explorer', 'OKP4 Young Druid'], + know: 0 + }, + { + rank: 3, + valoper: 'okp413pn0qmuxferwvjv0yzssedlkxff77ddtcwfefd', + poap: ['OKP4 Law-Stone Builder', 'OKP4 Objectarium Explorer', 'OKP4 Young Druid'], + know: 0 + }, + { + rank: 4, + valoper: 'okp41cvclz3u9zrpfvrhymds9pjp63x94wcujzlaaw0', + poap: ['OKP4 Objectarium Explorer', 'OKP4 Young Druid'], + know: 0 + }, + { + rank: 4, + valoper: 'okp413dgy7rtjjvvlkzu2upgtntdyzy4hnr6efarspq', + poap: ['OKP4 Objectarium Explorer', 'OKP4 Young Druid'], + know: 0 + }, + { + rank: 4, + valoper: 'okp41pa7ngqmqz5z4x88fd273kha89mtjkncnm5nwnx', + poap: ['OKP4 Objectarium Explorer', 'OKP4 Young Druid'], + know: 0 + }, + { + rank: 4, + valoper: 'okp41htsmzdnjf9zgrpnmyjzd04w3ql5mnq9xsaek64', + poap: ['OKP4 Objectarium Explorer', 'OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41d26y0n0y84yj2r9gzeyudzd6v5c8z6m7wmryer', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp414y9j2e7lqy0cr3nd5w73esuqtx07pse3ts5au4', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp410t2s8nzagk3lf9ydkn0t3ur5f0qcsy3fx6wdsh', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41wr7xfnm65keadtnu2asucqur2w54vdvder0du0', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41482yd7n04xk3l77wkaq7hntyq0xlr4sjjyjafz', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp414u09zxsxw752dfrzy7g4zxj6d4mprv4rmyv6lt', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp4102yjeeher64ttnu27fcvd0ah774ywsrp3qst4e', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp4127ey4un5zdfylq0a9fwgp8z27av5at0y8k0wrc', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41czkq2rg372tyrkdw2cpczgkd9k7x7zrgqcv8g6', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp416ve0fkf8ew8hezevhmyfg8ftkuzrx37chest4z', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41qaqzdzme4apm3d6f2lrg6espuay3wa0s5dap3x', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41vplplzclfmlg7pyutnau7p0w40gv0lp7nucdy3', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41srjjchn57narjr7gjxntrtdkegrx96mr495a77', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41hxlrt94n2nj6acpvk60wz8wdr4tgn84sk5a7ln', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41s7pz4rc7sq4hj88ljcruvrh4v7hk28paf6dm30', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41qcual5kgmw3gqc9q22hlp0aluc3t7rnsvlux5l', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41k5pyrxxd3usuneeeh4gssqh7sw0f73kv09dxql', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41wr6s62fgs7asqjt3yy566rlrtakujc8r7l7nwr', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41mde8kzhdc80qdxsx20rf4jfxhfnwxk4a7u29mh', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41hy5f3vftpj8lm9y8f5k4p38w73xu73rdeg0y3m', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41m8f8qnr2h5mdwu8ntjjrtyjcac74tjs62xahqz', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41vmv0tle77ag7r4v2myktnlj6a94uhel0jaf5af', + poap: ['OKP4 Young Druid'], + know: 0 + }, + { + rank: 5, + valoper: 'okp41wknzm45q885vyp7ynr6ld0u0h2ah4mpv60lq58', + poap: ['OKP4 Young Druid'], + know: 0 + } +] diff --git a/src/data/builder/dto.type.ts b/src/data/builder/dto.type.ts new file mode 100644 index 00000000..56d794ca --- /dev/null +++ b/src/data/builder/dto.type.ts @@ -0,0 +1,14 @@ +type Valoper = string + +type BuilderIdentity = Readonly<{ + valoper: Valoper +}> + +type Builder = Readonly<{ + rank: number + poap: string[] + know: number +}> + +export type BuilderDTO = BuilderIdentity & Builder +export type BuildersDTO = BuilderDTO[] diff --git a/src/data/builder/mapper.ts b/src/data/builder/mapper.ts new file mode 100644 index 00000000..1aef3c81 --- /dev/null +++ b/src/data/builder/mapper.ts @@ -0,0 +1,11 @@ +import type { BuilderDescriptor } from '@/entity/builder' +import type { BuilderDTO } from './dto.type' + +export const mapBuilderDTOtoBuilderDescriptor = (builder: BuilderDTO): BuilderDescriptor => ({ + rank: builder.rank, + valoper: builder.valoper, + earnings: { + poap: builder.poap, + know: builder.know + } +}) diff --git a/src/entity/builder.ts b/src/entity/builder.ts new file mode 100644 index 00000000..ffe42823 --- /dev/null +++ b/src/entity/builder.ts @@ -0,0 +1,12 @@ +type Valoper = string + +export type BuilderDescriptor = Readonly<{ + rank: number + valoper: Valoper + earnings: { + poap: string[] + know: number + } +}> + +export type BuildersDescriptor = BuilderDescriptor[] diff --git a/src/mixins.scss b/src/mixins.scss index 117fa837..8e6b30ad 100644 --- a/src/mixins.scss +++ b/src/mixins.scss @@ -44,4 +44,4 @@ @mixin rotation-animation($degrees) { transform: rotate($degrees); transition: transform 0.1s linear; -} \ No newline at end of file +} diff --git a/src/pages/builders/results.tsx b/src/pages/builders/results.tsx new file mode 100644 index 00000000..9daf1638 --- /dev/null +++ b/src/pages/builders/results.tsx @@ -0,0 +1,51 @@ +import React, { useMemo, useState, useCallback } from 'react' +import type { GetServerSideProps, NextPage } from 'next' +import type { Config } from '@/types/config.type' +import { config } from '@/lib/config' +import { BuildersLeaderboardTable } from '@/components/index' +import { buildersData } from '@/data/builder/builder' +import { mapBuilderDTOtoBuilderDescriptor } from '@/data/builder/mapper' + +export type ResultsProps = Pick + +const Results: NextPage = () => { + const [query, setQuery] = useState('') + const builders = useMemo( + () => + buildersData + .map(mapBuilderDTOtoBuilderDescriptor) + .filter( + row => + row.valoper.toLowerCase().includes(query.toLowerCase()) || + row.earnings.poap.find(item => item.toLowerCase().includes(query.toLowerCase())) + ), + [query] + ) + + const handleSearchChange = useCallback((value: string): void => setQuery(value), []) + + return ( +
+
+
+

Results

+
+

Here you can consult the marks allocated to builders for the Samhain challenges.

+
+
+ +
+
+
+
+
+ ) +} + +export const getServerSideProps: GetServerSideProps = async () => ({ + props: { + ...config + } +}) + +export default Results diff --git a/src/pages/validators/leaderboard.tsx b/src/pages/validators/leaderboard.tsx index bb10243a..c46c831b 100644 --- a/src/pages/validators/leaderboard.tsx +++ b/src/pages/validators/leaderboard.tsx @@ -2,7 +2,13 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import type { GetServerSideProps, NextPage } from 'next' import InfiniteScroll from 'react-infinite-scroll-component' import type { BaseCardProps } from '@/components/card/base/BaseCard' -import { BaseCard, Podium, LeaderboardTable, Countdown, Snackbar } from '@/components/index' +import { + BaseCard, + Podium, + ValidatorsLeaderboardTable, + Countdown, + Snackbar +} from '@/components/index' import type { PodiumStep } from '@/components/podium/Podium' import { config } from '@/lib/config' import type { Config } from '@/types/config.type' @@ -202,7 +208,7 @@ const Leaderboard: NextPage = props => { scrollThreshold={0.91} style={{ overflow: 'unset' }} > -