diff --git a/.github/CONTRIBUTORS.md b/.github/CONTRIBUTORS.md index 8067927..44d057c 100644 --- a/.github/CONTRIBUTORS.md +++ b/.github/CONTRIBUTORS.md @@ -27,8 +27,11 @@ git clone https://github.com/irffanasiff/Superteam-Reputation.git cd Superteam-Reputation # install the dependencies using pnpm pnpm i +# create a new branch and start working +git checkout -b # run the development server pnpm run dev ``` -Once you have made the changes check commitlint.config.js for how to commit your code. If the build is succesfully you will be able to push the code and make the pr. Follow the pull request. \ No newline at end of file + +Once you have made the changes check ```commitlint.config.js``` for how to commit your code. If the build is succesfully you will be able to push the code and make the pr. Follow the pull request. diff --git a/.vscode/settings.json b/.vscode/settings.json index 76f57c3..87839f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,9 @@ { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": true - } - } \ No newline at end of file + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": true, + "source.organizeImports": true + }, + "cSpell.words": ["projectworkxp"] +} diff --git a/next.config.js b/next.config.js index 3d3bc99..81a2a58 100644 --- a/next.config.js +++ b/next.config.js @@ -2,6 +2,9 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, + experimental: { + swcPlugins: [['next-superjson-plugin', {}]], + }, }; module.exports = nextConfig; diff --git a/package.json b/package.json index fbcf18a..e4a2294 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "eslint-config-next": "13.0.3", "framer-motion": "^7.6.6", "next": "13.0.3", + "next-superjson-plugin": "^0.4.9", "react": "18.2.0", "react-apexcharts": "^1.4.0", "react-dom": "18.2.0", @@ -43,7 +44,8 @@ "@commitlint/config-conventional": "^17.2.0", "husky": "^8.0.2", "only-pnpm": "^1.0.5", - "prettier": "^2.7.1" + "prettier": "^2.7.1", + "simple-zustand-devtools": "^1.1.0" }, "packageManager": "pnpm@7.16.1" } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69784ea..aeb2490 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,12 +18,14 @@ specifiers: framer-motion: ^7.6.6 husky: ^8.0.2 next: 13.0.3 + next-superjson-plugin: ^0.4.9 only-pnpm: ^1.0.5 prettier: ^2.7.1 react: 18.2.0 react-apexcharts: ^1.4.0 react-dom: 18.2.0 react-icons: ^4.6.0 + simple-zustand-devtools: ^1.1.0 swr: ^1.3.0 typescript: 4.8.4 @@ -42,6 +44,7 @@ dependencies: eslint-config-next: 13.0.3_rmayb2veg2btbq6mbmnyivgasy framer-motion: 7.6.6_biqbaboplfbrettd7655fr4n2y next: 13.0.3_mqvh5p7ejg4taogoj6tpk3gd5a + next-superjson-plugin: 0.4.9_f6u5o4wezvqlz2l4c6obuika34 react: 18.2.0 react-apexcharts: 1.4.0_g3lvop6jrvmpqnlf3mxot4n2ym react-dom: 18.2.0_react@18.2.0 @@ -55,6 +58,7 @@ devDependencies: husky: 8.0.2 only-pnpm: 1.0.5 prettier: 2.7.1 + simple-zustand-devtools: 1.1.0_pwbbdtypzkdc2trxa6yuxzoe2i packages: @@ -1979,13 +1983,11 @@ packages: /@types/prop-types/15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - dev: false /@types/react-dom/18.0.9: resolution: {integrity: sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==} dependencies: '@types/react': 18.0.25 - dev: false /@types/react/18.0.25: resolution: {integrity: sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==} @@ -1993,11 +1995,9 @@ packages: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.2 csstype: 3.1.1 - dev: false /@types/scheduler/0.16.2: resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} - dev: false /@typescript-eslint/parser/5.43.0_rmayb2veg2btbq6mbmnyivgasy: resolution: {integrity: sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==} @@ -2471,6 +2471,13 @@ packages: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} dev: false + /copy-anything/3.0.2: + resolution: {integrity: sha512-CzATjGXzUQ0EvuvgOCI6A4BGOo2bcVx8B+eC2nF862iv9fopnPQwlrbACakNCHRIJbCSBj+J/9JeDf60k64MkA==} + engines: {node: '>=12.13'} + dependencies: + is-what: 4.1.7 + dev: false + /copy-to-clipboard/3.3.1: resolution: {integrity: sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw==} dependencies: @@ -2531,7 +2538,6 @@ packages: /csstype/3.1.1: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} - dev: false /damerau-levenshtein/1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -3586,6 +3592,11 @@ packages: call-bind: 1.0.2 dev: false + /is-what/4.1.7: + resolution: {integrity: sha512-DBVOQNiPKnGMxRMLIYSwERAS5MVY1B7xYiGnpgctsOFvVDz9f9PFXXxMcTOHuoqYp4NK9qFYQaIC1NRRxLMpBQ==} + engines: {node: '>=12.13'} + dev: false + /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3733,7 +3744,6 @@ packages: hasBin: true dependencies: js-tokens: 4.0.0 - dev: false /lru-cache/4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -3884,6 +3894,17 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: false + /next-superjson-plugin/0.4.9_f6u5o4wezvqlz2l4c6obuika34: + resolution: {integrity: sha512-MSj40zj+ZRmFnZgZxOgQFD6efk3A5OQAPhU9e018JthWPjiE/Xx3HyxOsuOycoBDSxurBa6xH+m78SRr60JTXw==} + peerDependencies: + next: '>12.3.0' + superjson: ^1 + dependencies: + hoist-non-react-statics: 3.3.2 + next: 13.0.3_mqvh5p7ejg4taogoj6tpk3gd5a + superjson: 1.11.0 + dev: false + /next/13.0.3_mqvh5p7ejg4taogoj6tpk3gd5a: resolution: {integrity: sha512-rFQeepcenRxKzeKlh1CsmEnxsJwhIERtbUjmYnKZyDInZsU06lvaGw5DT44rlNp1Rv2MT/e9vffZ8vK+ytwXHA==} engines: {node: '>=14.6.0'} @@ -4241,7 +4262,6 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false /react-fast-compare/3.2.0: resolution: {integrity: sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==} @@ -4335,7 +4355,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /read-pkg-up/7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} @@ -4479,7 +4498,6 @@ packages: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: false /semver/5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} @@ -4531,6 +4549,22 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: true + /simple-zustand-devtools/1.1.0_pwbbdtypzkdc2trxa6yuxzoe2i: + resolution: {integrity: sha512-Axfcfr9L3YL3kto7aschCQLY2VUlXXMnIVtaTe9Y0qWbNmPsX/y7KsNprmxBZoB0pww5ZGs1u/ohcrvQ3tE6jA==} + peerDependencies: + '@types/react': '>=18.0.0' + '@types/react-dom': '>=18.0.0' + react: '>=18.0.0' + react-dom: '>=18.0.0' + zustand: '>=1.0.2' + dependencies: + '@types/react': 18.0.25 + '@types/react-dom': 18.0.9 + react: 18.2.0 + react-dom: 18.2.0_react@18.2.0 + zustand: 4.1.4_react@18.2.0 + dev: true + /slash/3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -4704,6 +4738,13 @@ packages: resolution: {integrity: sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==} dev: false + /superjson/1.11.0: + resolution: {integrity: sha512-6PfAg1FKhqkwWvPb2uXhH4MkMttdc17eJ91+Aoz4s1XUEDZFmLfFx/xVA3wgkPxAGy5dpozgGdK6V/n20Wj9yg==} + engines: {node: '>=10'} + dependencies: + copy-anything: 3.0.2 + dev: false + /supports-color/2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} engines: {node: '>=0.8.0'} @@ -5001,7 +5042,6 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: react: 18.2.0 - dev: false /util-deprecate/1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5132,3 +5172,19 @@ packages: /yocto-queue/0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + + /zustand/4.1.4_react@18.2.0: + resolution: {integrity: sha512-k2jVOlWo8p4R83mQ+/uyB8ILPO2PCJOf+QVjcL+1PbMCk1w5OoPYpAIxy9zd93FSfmJqoH6lGdwzzjwqJIRU5A==} + engines: {node: '>=12.7.0'} + peerDependencies: + immer: '>=9.0' + react: '>=16.8' + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + dependencies: + react: 18.2.0 + use-sync-external-store: 1.2.0_react@18.2.0 + dev: true diff --git a/src/components/Dashboard/LeaderBoardWrapper.tsx b/src/components/Dashboard/LeaderBoardWrapper.tsx index d45cd70..95db89b 100644 --- a/src/components/Dashboard/LeaderBoardWrapper.tsx +++ b/src/components/Dashboard/LeaderBoardWrapper.tsx @@ -1,42 +1,89 @@ import { - Container, css, Flex, Heading, Icon, Input, InputGroup, - InputLeftElement, Tab, TabList, TabPanel, TabPanels, Tabs, VStack + Container, + css, + Flex, + Heading, + Icon, + Input, + InputGroup, + InputLeftElement, + Tab, + TabList, + TabPanel, + TabPanels, + Tabs, + VStack, } from '@chakra-ui/react'; import React from 'react'; import { HiSearch } from 'react-icons/hi'; -import { filters } from '../../interfaces/filters.enum'; -import { xpTableType } from '../../interfaces/xpTable'; import EnhancedTable from './Leaderboard'; +import { dashboardDataType } from './Row/interfaces/dashboardStore'; type propsType = { - xpData: xpTableType[]; + dashboardData: dashboardDataType[]; }; -const LeaderBoardWrapper = ({ xpData }: propsType) => { +const LeaderBoardWrapper = ({ dashboardData }: propsType) => { const [wordEntered, setWordEntered] = React.useState(''); - const [data, setData] = React.useState(xpData); + const [data, setData] = React.useState(dashboardData); const [searching, _setSearching] = React.useState(false); - const ContributorsData: xpTableType[] = data.filter( - (person) => person.person_type === 'Contributor' - ); + const ContributorsData = data.map((item) => { + if (item.personType === 'Contributor') { + return item.overallXP.details; + } + }); - const MembersData: xpTableType[] = data.filter( - (person) => person.person_type === 'Member' + const ContributorsDataFiltered = ContributorsData.filter( + (item) => item !== undefined ); - const Developers: xpTableType[] = data.filter((person) => person.dev_xp > 0); - const Designers: xpTableType[] = data.filter( - (person) => person.design_xp > 0 + const MembersData = data.map((item) => { + if (item.personType === 'Member') { + return item.overallXP.details; + } + }); + + const filteredMembersData = MembersData.filter((item) => item !== undefined); + + const ProjectWorkXPData = data.map((item) => { + if (item.projectWorkXP) { + return item.projectWorkXP; + } + }); + + // filter the undefined values from the projectworkxp array + const filteredProjectWorkXPData = ProjectWorkXPData.filter( + (item) => item !== undefined ); - const Strategies: xpTableType[] = data.filter( - (person) => person.strategy_xp > 0 + + const IndieWorkXPData = data.map((item) => { + if (item.indieWorkXP) { + return item.indieWorkXP; + } + }); + + // filter the undefined values from the indieWorkXP array + const filteredIndieWorkXPData = IndieWorkXPData.filter( + (item) => item !== undefined ); - const Operations: xpTableType[] = data.filter((person) => person.ops_xp > 0); - const Videography: xpTableType[] = data.filter( - (person) => person.video_xp > 0 + + const workingGroupXPData = data.map((item) => { + if (item.internalOps) { + return item.internalOps; + } + }); + + // filter the undefined values from the workingGroupXP array + const filteredWorkingGroupXPData = workingGroupXPData.filter( + (item) => item !== undefined ); - const Writers: xpTableType[] = data.filter((person) => person.writing_xp > 0); + + const allXPData = data.map((item) => { + if (item.overallXP.details) { + return item.overallXP.details; + } + }); const handleSearch = (event: { target: { value: any } }) => { const searchWord = event.target.value; @@ -46,7 +93,7 @@ const LeaderBoardWrapper = ({ xpData }: propsType) => { }); if (searchWord === '') { - setData(xpData); + setData(dashboardData); } else { setData(newFilter); } @@ -73,7 +120,7 @@ const LeaderBoardWrapper = ({ xpData }: propsType) => { css={css({ scrollbarWidth: 'none', '::-webkit-scrollbar': { display: 'none' }, - 'WebkitOverflowXScrolling': 'touch', // todo: here there is a bug remove the scrollbar in x direction + WebkitOverflowXScrolling: 'touch', // todo: here there is a bug remove the scrollbar in x direction })} overflowX="scroll" overflowY={'visible'} @@ -86,6 +133,7 @@ const LeaderBoardWrapper = ({ xpData }: propsType) => { py="0.9rem" fontSize={'14px'} fontWeight="400" + whiteSpace="nowrap" _active={{ color: 'superteamWhite', }} @@ -103,6 +151,7 @@ const LeaderBoardWrapper = ({ xpData }: propsType) => { py="0.9rem" fontSize={'14px'} fontWeight="400" + whiteSpace="nowrap" _active={{ color: 'superteamWhite', }} @@ -120,6 +169,7 @@ const LeaderBoardWrapper = ({ xpData }: propsType) => { py="0.9rem" fontSize={'14px'} fontWeight="400" + whiteSpace="nowrap" _active={{ color: 'superteamWhite', }} @@ -137,6 +187,7 @@ const LeaderBoardWrapper = ({ xpData }: propsType) => { py="0.9rem" fontSize={'14px'} fontWeight="400" + whiteSpace="nowrap" _active={{ color: 'superteamWhite', }} @@ -146,58 +197,7 @@ const LeaderBoardWrapper = ({ xpData }: propsType) => { borderColor: 'superteamBlue.900', }} > - Design - - - Development - - - Operations - - - Video + Project Work { py="0.9rem" fontSize={'14px'} fontWeight="400" + whiteSpace="nowrap" _active={{ color: 'superteamWhite', }} @@ -214,7 +215,7 @@ const LeaderBoardWrapper = ({ xpData }: propsType) => { borderColor: 'superteamBlue.900', }} > - Strategy + Indie Work { py="0.9rem" fontSize={'14px'} fontWeight="400" + whiteSpace="nowrap" _active={{ color: 'superteamWhite', }} @@ -231,7 +233,7 @@ const LeaderBoardWrapper = ({ xpData }: propsType) => { borderColor: 'superteamBlue.900', }} > - Writing + Internal Operations { - + - + {' '} {' '} - - - - - - {' '} - - - diff --git a/src/components/Dashboard/Leaderboard.tsx b/src/components/Dashboard/Leaderboard.tsx index 3c9afe7..6289bab 100644 --- a/src/components/Dashboard/Leaderboard.tsx +++ b/src/components/Dashboard/Leaderboard.tsx @@ -3,35 +3,34 @@ import { Container, Table, TableContainer, - Tbody, Text, Th, Thead, Tr, useMediaQuery + Tbody, + Text, + Th, + Thead, + Tr, + useMediaQuery, } from '@chakra-ui/react'; import * as React from 'react'; -import { filters } from '../../interfaces/filters.enum'; -import { xpTableType } from '../../interfaces/xpTable'; import Pagination from '../Pagination'; +import { xpType } from './Row/interfaces/xp'; import TableRow from './Row/TableRow'; import TableRowMobile from './Row/TableRowMobile'; //import XPGraph from './graph'; type propsType = { - _filter_by: filters; - row: xpTableType[]; + row: (xpType | undefined)[]; searching: boolean; }; //todo: for now the data which is received is sorted for the highest overall xp, we need to sort this as per filter_by value for each tab -function sortArrayAscending( - array: xpTableType[], - _filter_by: filters -): xpTableType[] { - return array.sort(({ dev_xp: a }, { dev_xp: b }) => a - b); -} +// function sortArrayAscending( +// array: xpType[] | undefined, +// _filter_by: filters +// ): xpTableType[] { +// return array.sort(({ development: a }, { development: b }) => a - b); +// } -export default function EnhancedTable({ - row, - _filter_by, - searching, -}: propsType) { +export default function EnhancedTable({ row, searching }: propsType) { const [currentPage, setCurrentPage] = React.useState(1); const [isSmallerThan990] = useMediaQuery('(max-width: 990px)'); @@ -41,9 +40,19 @@ export default function EnhancedTable({ const firstPageIndex = (currentPage - 1) * PageSize; const lastPageIndex = firstPageIndex + PageSize; const arr = row.slice(firstPageIndex, lastPageIndex); - return arr; + return arr as xpType[]; }, [currentPage, row]); + rows.sort((a: xpType, b: xpType) => { + if (a?.total_amount > b?.total_amount) { + return -1; + } + if (a?.total_amount < b?.total_amount) { + return 1; + } + return 0; + }); + return ( <> diff --git a/src/components/Dashboard/Row/Chart.tsx b/src/components/Dashboard/Row/Chart.tsx new file mode 100644 index 0000000..77ee2d7 --- /dev/null +++ b/src/components/Dashboard/Row/Chart.tsx @@ -0,0 +1,171 @@ +import { Center } from '@chakra-ui/react'; +import dynamic from 'next/dynamic'; +import React from 'react'; +const ReactApexChart = dynamic(() => import('react-apexcharts'), { + ssr: false, +}); + +export enum curveEnum { + SMOOTH = 'smooth', + STRAIGHT = 'straight', + STEPLINE = 'stepline', +} + +export enum xaxisType { + CATEGORY = 'category', + DATETIME = 'datetime', + NUMERIC = 'numeric', +} + +type propsType = { + lastSixMonths: { + xp: number; + monthYear: string; + }[]; + graphColor: string[]; +}; + +const Chart = ({ lastSixMonths, graphColor }: propsType) => { + const [hydrated, setHydrated] = React.useState(false); + + const [chartData, setChartData] = React.useState({ + series: [ + { + name: 'series1', + data: lastSixMonths.map((item) => item.xp).reverse(), + }, + ], + options: { + chart: { + height: 70, + width: 130, + toolbar: { + show: false, + tools: { + download: false, + selection: false, + zoom: false, + zoomin: false, + zoomout: false, + pan: false, + reset: false, + }, + }, + }, + stroke: { + colors: graphColor, + curve: curveEnum.SMOOTH, + width: 1, + }, + fill: { + colors: graphColor, + type: 'gradient', + gradient: { + shade: 'dark', + type: 'vertical', + shadeIntensity: 1, + gradientToColors: graphColor, + inverseColors: true, + opacityFrom: 0.4, + opacityTo: 0.1, + stops: [0, 100], + colorStops: [], + }, + }, + dataLabels: { + enabled: false, + }, + xaxis: { + show: false, + axisTicks: { + show: false, + }, + tooltip: { + enabled: false, + }, + crosshairs: { + show: false, + }, + labels: { + show: false, + }, + type: xaxisType.DATETIME, + categories: [ + '2018-09-19T06:30:00.000Z', + '2018-09-19T07:00:00.000Z', + '2018-09-19T08:30:00.000Z', + '2018-09-19T09:30:00.000Z', + '2018-09-19T10:30:00.000Z', + '2018-09-19T11:30:00.000Z', + ], + }, + grid: { show: false }, + yaxis: { + show: false, + labels: { + show: false, + }, + }, + tooltip: { + enabled: true, + theme: 'dark', + custom: function ({ series, seriesIndex, dataPointIndex, w }: any) { + return ( + '
' + + '' + + series[seriesIndex][dataPointIndex] + + '' + + '
' + ); + }, + style: { + fontSize: '12px', + shadow: '0px', + }, + x: { + show: false, + format: 'dd/MM/yy HH:mm', + }, + y: { + show: true, + formatter: function ( + value: any, + { series, seriesIndex, dataPointIndex, w }: any + ) { + return value; + }, + }, + marker: { + show: false, + }, + }, + }, + }); + + // to prevent hydration issue we remove content from initial render + React.useEffect(() => { + // This forces a rerender, so the date is rendered + // the second time but not the first + setHydrated(true); + }, []); + if (!hydrated) { + // Returns null on first render, so the client and server match + return null; + } + + return ( +
+ {typeof window !== 'undefined' ? ( + + ) : null} +
+ ); +}; + +export default Chart; diff --git a/src/components/Dashboard/Row/ExpandedRow.tsx b/src/components/Dashboard/Row/ExpandedRow.tsx index 7c7da2e..459d994 100644 --- a/src/components/Dashboard/Row/ExpandedRow.tsx +++ b/src/components/Dashboard/Row/ExpandedRow.tsx @@ -1,10 +1,11 @@ import { Flex, Td, Text, Tr } from '@chakra-ui/react'; -import { xpTableType } from '../../../interfaces/xpTable'; +import { skillKind } from '../../../enums/skill'; import CustomTag from '../../HOC/Tag.HOC'; +import { xpType } from './interfaces/xp'; type propsType = { expandRow: boolean; - row: xpTableType; + row: xpType; }; export const ExpandedRow = ({ expandRow, row }: propsType) => { @@ -20,64 +21,116 @@ export const ExpandedRow = ({ expandRow, row }: propsType) => { - {row.dev_xp > 0 && Development} - {row.design_xp > 0 && Design} - {row.strategy_xp > 0 && Strategy}{' '} - {row.video_xp > 0 && Videography}{' '} - {row.writing_xp > 0 && Writing}{' '} - {row.ops_xp > 0 && Operations} + {row?.skills.map((skill, key) => { + if (skill.skill === skillKind.DEV) { + return ( + + Development + + ); + } + if (skill.skill === skillKind.DESIGN) { + return ( + + Design + + ); + } + if (skill.skill === skillKind.STRATEGY) { + return ( + + Strategy + + ); + } + if (skill.skill === skillKind.VIDEO) { + return ( + + Videography + + ); + } + if (skill.skill === skillKind.WRITING) { + return ( + + Writing + + ); + } + if (skill.skill === skillKind.OPS) { + return ( + + Operations + + ); + } + })} - {row.dev_xp > 0 && ( - - - {Math.round(row.dev_xp)} - - - - )} - {row.design_xp > 0 && ( - - - {Math.round(row.design_xp)} - - - - )} - {row.strategy_xp > 0 && ( - - - {Math.round(row.strategy_xp)} - - - - )}{' '} - {row.video_xp > 0 && ( - - - {Math.round(row.video_xp)} - - - - )}{' '} - {row.writing_xp > 0 && ( - - - {Math.round(row.writing_xp)} - - - - )}{' '} - {row.ops_xp > 0 && ( - - - {Math.round(row.ops_xp)} - - - - )} + {row?.skills.map((skill, key) => { + if (skill.skill === skillKind.DEV) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.DESIGN) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.STRATEGY) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.VIDEO) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.WRITING) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.OPS) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + })} @@ -108,62 +161,114 @@ export const ExpandedRowMobile = ({ expandRow, row }: propsType) => { justify={'center'} > - {row.dev_xp > 0 && Development} - {row.design_xp > 0 && Design} - {row.strategy_xp > 0 && Strategy}{' '} - {row.video_xp > 0 && Videography}{' '} - {row.writing_xp > 0 && Writing}{' '} - {row.ops_xp > 0 && Operations} + {row?.skills.map((skill, key) => { + if (skill.skill === skillKind.DEV) { + return ( + + Development + + ); + } + if (skill.skill === skillKind.DESIGN) { + return ( + + Design + + ); + } + if (skill.skill === skillKind.STRATEGY) { + return ( + + Strategy + + ); + } + if (skill.skill === skillKind.VIDEO) { + return ( + + Videography + + ); + } + if (skill.skill === skillKind.WRITING) { + return ( + + Writing + + ); + } + if (skill.skill === skillKind.OPS) { + return ( + + Operations + + ); + } + })} - {row.dev_xp > 0 && ( - - - {Math.round(row.dev_xp)} - - - - )} - {row.design_xp > 0 && ( - - - {Math.round(row.design_xp)} - - - - )} - {row.strategy_xp > 0 && ( - - - {Math.round(row.strategy_xp)} - - - - )}{' '} - {row.video_xp > 0 && ( - - - {Math.round(row.video_xp)} - - - - )}{' '} - {row.writing_xp > 0 && ( - - - {Math.round(row.writing_xp)} - - - - )}{' '} - {row.ops_xp > 0 && ( - - - {Math.round(row.ops_xp)} - - - - )} + {row.skills.map((skill, key) => { + if (skill.skill === skillKind.DEV) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.DESIGN) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.STRATEGY) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.VIDEO) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.WRITING) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + if (skill.skill === skillKind.OPS) { + return ( + + + {Math.round(skill.amount)} + + + + ); + } + })} diff --git a/src/components/Dashboard/Row/GraphColumn.tsx b/src/components/Dashboard/Row/GraphColumn.tsx index 68e962e..e7a84c8 100644 --- a/src/components/Dashboard/Row/GraphColumn.tsx +++ b/src/components/Dashboard/Row/GraphColumn.tsx @@ -1,124 +1,91 @@ -import { Flex, Icon, Text } from '@chakra-ui/react'; +import { Flex, Icon, Text, Tooltip } from '@chakra-ui/react'; import { BsCaretDownFill } from 'react-icons/bs'; import CustomTag from '../../HOC/Tag.HOC'; -// const ReactApexChart = dynamic(() => import('react-apexcharts'), { -// ssr: false, -// }); +import Chart from './Chart'; +import { xpType } from './interfaces/xp'; -export enum curveEnum { - SMOOTH = 'smooth', - STRAIGHT = 'straight', - STEPLINE = 'stepline', -} +type propsType = { + row: xpType; +}; + +const GraphColumn = ({ row }: propsType) => { + const cumulativeData: { date: Date; xp: number }[] = row.xp.amount.map( + (amount, index) => { + return { + date: new Date(row.xp.dates[index]), + xp: amount, + }; + } + ); + + const monthlyData = cumulativeData.reduce((acc, curr) => { + const month = curr.date.getMonth(); + const year = curr.date.getFullYear(); + const monthYear = `${month}-${year}`; + const existingMonth = acc.find((item) => item.monthYear === monthYear); + if (existingMonth) { + existingMonth.xp += curr.xp; + } else { + acc.push({ + xp: curr.xp, + monthYear, + }); + } + return acc; + }, [] as { xp: number; monthYear: string }[]); -export enum xaxisType { - CATEGORY = 'category', - DATETIME = 'datetime', - NUMERIC = 'numeric', -} -function randomArray(length: number, max: number): [number[], number] { - const arr = [...new Array(length)].map(() => Math.round(Math.random() * max)); - const arrDiff = arr[length - 1] - arr[0]; - return [arr, arrDiff]; -} -const GraphColumn = () => { - const [_numArray, arrDiff] = randomArray(8, 100); + const lastSixMonths = [...new Array(6)].map((_, index) => { + const date = new Date(); + date.setMonth(date.getMonth() - index); + const month = date.getMonth(); + const year = date.getFullYear(); + const monthYear = `${month}-${year}`; + const existingMonth = monthlyData.find( + (item) => item.monthYear === monthYear + ); + return { + xp: existingMonth ? existingMonth.xp : 0, + monthYear, + }; + }); - const graphColor = arrDiff >= 0 ? ['#00A67E'] : ['#FF0B71']; + const calculateXpGrowth = () => { + const lastMonth = lastSixMonths[0].xp; + const secondLastMonth = lastSixMonths[1].xp; + const diff = lastMonth - secondLastMonth; + const growth = (diff / secondLastMonth) * 100; + if (growth > 0) { + return Math.round(growth); + } else if (growth < 0) { + return Math.round(growth); + } else { + return 0; + } + }; - // const [chartData, setChartData] = React.useState({ - // series: [ - // { - // name: 'series1', - // data: numArray, - // }, - // ], - // options: { - // chart: { - // height: 70, - // width: 130, - // toolbar: { - // show: false, - // tools: { - // download: false, - // selection: false, - // zoom: false, - // zoomin: false, - // zoomout: false, - // pan: false, - // reset: false, - // }, - // }, - // }, - // stroke: { - // colors: graphColor, - // curve: curveEnum.SMOOTH, - // width: 1, - // }, - // fill: { - // colors: graphColor, - // type: 'gradient', - // gradient: { - // shade: 'dark', - // type: 'vertical', - // shadeIntensity: 1, - // gradientToColors: graphColor, - // inverseColors: true, - // opacityFrom: 0.4, - // opacityTo: 0.1, - // stops: [0, 100], - // colorStops: [], - // }, - // }, - // dataLabels: { - // enabled: false, - // }, + const graphColor = calculateXpGrowth() > 0 ? ['#00A67E'] : ['#FF0B71']; - // xaxis: { - // show: false, - // axisTicks: { - // show: false, - // }, - // crosshairs: { - // show: false, - // }, - // labels: { - // show: false, - // }, - // type: xaxisType.DATETIME, - // categories: [ - // '2018-09-19T04:30:00.000Z', - // '2018-09-19T05:30:00.000Z', - // '2018-09-19T06:30:00.000Z', - // '2018-09-19T07:00:00.000Z', - // '2018-09-19T08:30:00.000Z', - // '2018-09-19T09:30:00.000Z', - // '2018-09-19T10:30:00.000Z', - // '2018-09-19T11:30:00.000Z', - // ], - // }, - // grid: { show: false }, - // yaxis: { - // show: false, - // labels: { - // show: false, - // }, - // }, - // tooltip: { - // enabled: false, - // x: { - // format: 'dd/MM/yy HH:mm', - // }, - // }, - // }, - // }); return ( - - + + - - 162 - + + + {lastSixMonths[0].xp} + + @@ -128,27 +95,39 @@ const GraphColumn = () => { w={3} h={3} color={graphColor} - transform={arrDiff >= 0 ? 'rotate(180deg)' : 'rotate(0deg)'} + transform={ + graphColor[0] === '#00A67E' ? 'rotate(180deg)' : 'rotate(0deg)' + } /> - - 31(13%) - + + + {Math.round(lastSixMonths[1].xp)} + + + + + ({calculateXpGrowth()}%) + + {' '} + ); }; -{ - /* typeof window !== 'undefined' ? ( -
- -
*/ -} export default GraphColumn; diff --git a/src/components/Dashboard/Row/RowCategories.tsx b/src/components/Dashboard/Row/RowCategories.tsx index 0335963..4270555 100644 --- a/src/components/Dashboard/Row/RowCategories.tsx +++ b/src/components/Dashboard/Row/RowCategories.tsx @@ -1,32 +1,55 @@ import { Wrap } from '@chakra-ui/react'; -import { xpTableType } from '../../../interfaces/xpTable'; +import { skillKind } from '../../../enums/skill'; +import { xpType } from './interfaces/xp'; import CustomChip from '../../HOC/Chip.HOC'; type propTypes = { - row: xpTableType; + row: xpType; }; const RowCategories = ({ row }: propTypes) => { return ( - {row.dev_xp > 0 && ( - - )} - {row.design_xp > 0 && ( - - )} - {row.video_xp > 0 && ( - - )} - {row.ops_xp > 0 && ( - - )}{' '} - {row.strategy_xp > 0 && ( - - )} - {row.writing_xp > 0 && ( - - )} + {row?.skills.map((skill, key) => { + if (skill.skill === skillKind.DEV) { + return ( + + ); + } + if (skill.skill === skillKind.DESIGN) { + return ( + + ); + } + if (skill.skill === skillKind.VIDEO) { + return ( + + ); + } + if (skill.skill === skillKind.OPS) { + return ( + + ); + } + if (skill.skill === skillKind.STRATEGY) { + return ( + + ); + } + if (skill.skill === skillKind.WRITING) { + return ( + + ); + } + })} ); }; diff --git a/src/components/Dashboard/Row/TableRow.tsx b/src/components/Dashboard/Row/TableRow.tsx index 843a0a2..c264c62 100644 --- a/src/components/Dashboard/Row/TableRow.tsx +++ b/src/components/Dashboard/Row/TableRow.tsx @@ -1,21 +1,22 @@ -import { Center, Flex, Icon, Td, Text, Tr } from '@chakra-ui/react'; +import { Box, Center, Flex, Icon, Td, Text, Tr } from '@chakra-ui/react'; import * as React from 'react'; import { FiChevronDown } from 'react-icons/fi'; -import { xpTableType } from '../../../interfaces/xpTable'; import CustomTag from '../../HOC/Tag.HOC'; import MedalSVG from '../../Logo/MedalSVG'; import { ExpandedRow } from './ExpandedRow'; import GraphColumn from './GraphColumn'; +import { xpType } from './interfaces/xp'; import RowCategories from './RowCategories'; type propTypes = { - row: xpTableType; + row: xpType; index: number; searching: boolean; }; const TableRow = ({ row, index, searching }: propTypes) => { const [expandRow, setExpandRow] = React.useState(false); + return ( <> { fontSize={'14px'} textTransform="capitalize" > - {row.name.split('#')[0]} + {row?.name.split('#')[0]} - {row.name} + {row?.name} - {Math.round(row.total_xp)} + {Math.round(row?.total_amount)} - + + + diff --git a/src/components/Dashboard/Row/TableRowMobile.tsx b/src/components/Dashboard/Row/TableRowMobile.tsx index e44eeae..12f2742 100644 --- a/src/components/Dashboard/Row/TableRowMobile.tsx +++ b/src/components/Dashboard/Row/TableRowMobile.tsx @@ -1,15 +1,15 @@ import { Center, Flex, Icon, Text, Tr } from '@chakra-ui/react'; import * as React from 'react'; import { FiChevronDown } from 'react-icons/fi'; -import { xpTableType } from '../../../interfaces/xpTable'; import CustomTag from '../../HOC/Tag.HOC'; import MedalSVG from '../../Logo/MedalSVG'; import { ExpandedRowMobile } from './ExpandedRow'; import GraphColumn from './GraphColumn'; +import { xpType } from './interfaces/xp'; import RowCategories from './RowCategories'; type propTypes = { - row: xpTableType; + row: xpType; index: number; searching: boolean; }; @@ -32,52 +32,50 @@ const TableRowMobile = ({ row, index, searching }: propTypes) => { background: 'superteamBlack.800', }} > - - - {index + 1 <= 3 ? ( - searching ? ( - ` ${index + 1}.` - ) : ( - - ) - ) : ( + + {index + 1 <= 3 ? ( + searching ? ( ` ${index + 1}.` - )} - - - {row.name.split('#')[0]} - - - {row.name} - - {' '} -
- -
+ ) : ( + + ) + ) : ( + ` ${index + 1}.` + )} + + + {row?.name.split('#')[0]} + + + {row?.name} + + {' '} +
+ +
+
+ + + + + {Math.round(row?.total_amount)} + + - - - - - {Math.round(row.total_xp)} - - - - - - + + - - + + ); }; diff --git a/src/components/Dashboard/Row/interfaces/airtableRecievedXP.d.ts b/src/components/Dashboard/Row/interfaces/airtableRecievedXP.d.ts new file mode 100644 index 0000000..7485c96 --- /dev/null +++ b/src/components/Dashboard/Row/interfaces/airtableRecievedXP.d.ts @@ -0,0 +1,6 @@ +import { allocated_xp } from './xp'; + +export type receivedXPFromAirtableType = { + name: string; + xp: allocated_xp; +}; diff --git a/src/components/Dashboard/Row/interfaces/airtableRecievedXPTotal.d.ts b/src/components/Dashboard/Row/interfaces/airtableRecievedXPTotal.d.ts new file mode 100644 index 0000000..7c217b2 --- /dev/null +++ b/src/components/Dashboard/Row/interfaces/airtableRecievedXPTotal.d.ts @@ -0,0 +1,6 @@ +import { skillKind } from '../../../../enums/skill'; + +export type receivedXPFromAirtableTotalType = { + name: string; + xp: { amount: number; date: Date; skill: skillKind }[]; +}; diff --git a/src/components/Dashboard/Row/interfaces/dashboardStore.d.ts b/src/components/Dashboard/Row/interfaces/dashboardStore.d.ts new file mode 100644 index 0000000..f3ee738 --- /dev/null +++ b/src/components/Dashboard/Row/interfaces/dashboardStore.d.ts @@ -0,0 +1,31 @@ +import { xpType } from './xp'; + +export type totalOverallXPType = { + total_xp: number; + development: number; + design: number; + operations: number; + videography: number; + strategy: number; + writing: number; +}; + +export type overallXP = { + total?: totalOverallXPType; + details: xpType; +}; +export type dashboardDataType = { + name: string; + personType: string; + overallXP: overallXP; + projectWorkXP?: xpType; + indieWorkXP?: xpType; + internalOps?: xpType; + bountiesXP?: xpType; + stackExchangeXP?: xpType; +}; + +export type dashboardStoreType = { + dashboardData?: dashboardDataType; + setDashboardData?: (data: dashboardDataType) => any; +}; diff --git a/src/components/Dashboard/Row/interfaces/filters.enum.ts b/src/components/Dashboard/Row/interfaces/filters.enum.ts new file mode 100644 index 0000000..244a6b3 --- /dev/null +++ b/src/components/Dashboard/Row/interfaces/filters.enum.ts @@ -0,0 +1,9 @@ +export enum filters { + TOTAL = 'total_xp', + DESIGN = 'design', + DEV = 'development', + OPS = 'operations', + STRATEGY = 'strategy', + WRITING = 'writing', + VIDEO = 'videography', +} diff --git a/src/components/Dashboard/Row/interfaces/xp.d.ts b/src/components/Dashboard/Row/interfaces/xp.d.ts new file mode 100644 index 0000000..1e6a4ac --- /dev/null +++ b/src/components/Dashboard/Row/interfaces/xp.d.ts @@ -0,0 +1,43 @@ +import { skillKind } from '../../../../enums/skill'; + +export type total_xp = { + total_xp: number; + development: number; + design: number; + operations: number; + videography: number; + strategy: number; + writing: number; +}; +export type allocated_xp = { + total_amount: number; + date: Date; + skill: skillKind; +}; + +export type xpType = { + name: string; + total_amount: number; + skills: { skill: skillKind; amount: number }[]; + xp: { + amount: number[]; + dates: Date[]; + }; +}; + +export type xp = { + total?: { + total_xp: number; + development: number; + design: number; + operations: number; + videography: number; + strategy: number; + writing: number; + }; + details?: { + total_xp: number; + skills: { skill: string; amount: number }[]; + xp: { amount: number[]; dates: Date[] }; + }; +}; diff --git a/src/enums/skill.ts b/src/enums/skill.ts new file mode 100644 index 0000000..9033266 --- /dev/null +++ b/src/enums/skill.ts @@ -0,0 +1,9 @@ +/* eslint-disable no-unused-vars */ +export enum skillKind { + DEV= 'development', + DESIGN = 'design', + OPS = 'operations', + VIDEO = 'videography', + STRATEGY = 'strategy', + WRITING = 'writing', +} \ No newline at end of file diff --git a/src/interfaces/filters.enum.ts b/src/interfaces/filters.enum.ts deleted file mode 100644 index b0cf8f2..0000000 --- a/src/interfaces/filters.enum.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum filters { - TOTAL = 'total_xp', - DESIGN = 'design_xp', - DEV = 'dev_xp', - OPS = 'ops_xp', - STRATEGY = 'strategy_xp', - WRITING = 'writing_xp', - VIDEO = 'video_xp', -} diff --git a/src/interfaces/xpTable.d.ts b/src/interfaces/xpTable.d.ts deleted file mode 100644 index ed19c00..0000000 --- a/src/interfaces/xpTable.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type xpTableType = { - name: string; - person_type: string; - total_xp: number; - design_xp: number; - dev_xp: number; - ops_xp: number; - strategy_xp: number; - writing_xp: number; - video_xp: number; -}; diff --git a/src/lib/airtable.ts b/src/lib/airtable.ts index 7153431..73400cd 100644 --- a/src/lib/airtable.ts +++ b/src/lib/airtable.ts @@ -1,11 +1,6 @@ +import { skillKind } from '../enums/skill'; + const Airtable = require('airtable'); -import { ProjectsXPType } from '../interfaces/projectsXP'; -import { xpTableType } from '../interfaces/xpTable'; -import { - getIndieWork_xp_view, - getProjectWork_xp_view, - getWorkingGroups_xp_view, -} from './getXPDistributions'; const getFilteredRecords = (records: any[]) => { // filter out the records where allocation is null and xp is not null @@ -320,7 +315,7 @@ const getAllTitleFunction = async () => { }; const getXPRecordFunction = async () => { - const xps: xpTableType[] | null = []; + const xps: any = []; try { var base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( process.env.BASE @@ -349,12 +344,12 @@ const getXPRecordFunction = async () => { name: name, person_type: personType, total_xp: total || 0, - design_xp: design || 0, - dev_xp: dev || 0, - ops_xp: ops || 0, - strategy_xp: strategy || 0, - writing_xp: writing || 0, - video_xp: video || 0, + design: design || 0, + development: dev || 0, + operations: ops || 0, + strategy: strategy || 0, + writing: writing || 0, + videography: video || 0, }); }); fetchNextPage(); @@ -373,19 +368,6 @@ const getXPRecordFunction = async () => { return xps; }; -// this function will get xp from all views -> Indie Work, Project Work, Working Groups(cab), Internal Ops, Bounties, Stack Exchange -const getXPFromAllViews = async () => { - // const bounties = await getBounties_xp_view(); - const indieWork = (await getIndieWork_xp_view()) as ProjectsXPType[]; - const projectWork = (await getProjectWork_xp_view()) as ProjectsXPType[]; - const workingGroups = (await getWorkingGroups_xp_view()) as ProjectsXPType[]; - // const stackExchange = await getStackExchange_xp_view(); - // const internalOperations = await getInternalOperations_xp_view(); - const totalXP = [...indieWork, ...projectWork, ...workingGroups]; - - return totalXP; - // now will combine all that into an array -}; export { getIndieRecordsFunction, getCommunityRecordsFunction, diff --git a/src/lib/getXPDistributions.ts b/src/lib/getXPDistributions.ts index c56f3f2..b6286db 100644 --- a/src/lib/getXPDistributions.ts +++ b/src/lib/getXPDistributions.ts @@ -1,5 +1,44 @@ const Airtable = require('airtable'); -import { ProjectsXPType } from '../interfaces/projectsXP'; +import { receivedXPFromAirtableType } from '../components/Dashboard/Row/interfaces/airtableRecievedXP'; +import { skillKind } from '../enums/skill'; + +// this function fetches xp distribution +async function getXPDistribution( + baseName: string +): Promise { + const xp_allocated_for_work: receivedXPFromAirtableType[] = []; + var base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( + process.env.BASE + ); + await base(baseName) + .select({ + maxRecords: 1000, + view: 'xp_view', + }) + .eachPage( + function page(records: any[], fetchNextPage: () => void) { + records.forEach((record) => { + const fields = record.fields; + const name = fields._Name as string; + const total_amount = fields._XP as number; + const date = fields._Date as Date; + const skill = fields._Skill as skillKind; + xp_allocated_for_work.push({ + name, + xp: { total_amount, date, skill }, + }); + }); + fetchNextPage(); + }, + function done(err: any) { + if (err) { + console.error('there was an error - ', err); + return; + } + } + ); + return xp_allocated_for_work; +} { /* * xp view are the views in airtable where all the xp allocated for that source is listed @@ -8,195 +47,45 @@ import { ProjectsXPType } from '../interfaces/projectsXP'; */ } -// this function fetches xp distributed for project work -const getProjectWork_xp_view = async () => { - const xp_allocated_for_projects: ProjectsXPType[] = []; - var base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( - process.env.BASE - ); - try { - base('Project Work') - .select({ - // Selecting the first 3 records in All XP Requests: - maxRecords: 1000, - view: 'xp_view', - }) - .eachPage( - function page(records: any[], fetchNextPage: () => void) { - records.forEach((record) => { - // console.log('records fetched - ', record.fields); - const fields = record.fields; - const name = fields._Name as string; - - const xp = fields._XP as number; - const date = fields._Date as Date; - xp_allocated_for_projects.push({ - name, - xp: { xp_allocated: xp, date: date }, - }); - // console.log('xp - ', xp_allocated_for_projects); - }); - - fetchNextPage(); - }, - function done(err: any) { - if (err) { - console.error(err); - return; - } - } - ); - return xp_allocated_for_projects; - } catch (error) { - console.log(error); - return error; - } -}; - -// this function fetches xp distributed for Indie work -const getIndieWork_xp_view = async () => { - const xp_allocated_for_indieWork: ProjectsXPType[] = []; - var base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( - process.env.BASE - ); - try { - base('Indie Work') - .select({ - // Selecting the first 3 records in All XP Requests: - maxRecords: 1000, - view: 'xp_view', - }) - .eachPage( - function page(records: any[], fetchNextPage: () => void) { - records.forEach((record) => { - // console.log('records fetched - ', record.fields); - const fields = record.fields; - const name = fields._Name as string; - - const xp = fields._XP as number; - const date = fields._Date as Date; - xp_allocated_for_indieWork.push({ - name, - xp: { xp_allocated: xp, date: date }, - }); - // console.log('xp - ', xp_allocated_for_projects); - }); - - fetchNextPage(); - }, - function done(err: any) { - if (err) { - console.error(err); - return; - } - } - ); - return xp_allocated_for_indieWork; - } catch (error) { - console.log(error); - return error; - } -}; -// this function fetches xp distributed for Working Groups -const getWorkingGroups_xp_view = async () => { - const xp_allocated_for_working_groups: ProjectsXPType[] = []; - var base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( - process.env.BASE - ); - try { - base('CAB/SubDAO XPs') - .select({ - // Selecting the first 3 records in All XP Requests: - maxRecords: 1000, - view: 'xp_view', - }) - .eachPage( - function page(records: any[], fetchNextPage: () => void) { - records.forEach((record) => { - // console.log('records fetched - ', record.fields); - const fields = record.fields; - const name = fields._Name as string; - - const xp = fields._XP as number; - const date = fields._Date as Date; - xp_allocated_for_working_groups.push({ - name, - xp: { xp_allocated: xp, date: date }, - }); - // console.log('xp - ', xp_allocated_for_projects); - }); - - fetchNextPage(); - }, - function done(err: any) { - if (err) { - console.error(err); - return; - } - } - ); - return xp_allocated_for_working_groups; - } catch (error) { - console.log(error); - return error; - } -}; - -// this function fetches xp distributed for Internal Operations -const getInternalOperations_xp_view = async () => { - const xp_allocated_for_projects: ProjectsXPType[] = []; - var base = new Airtable({ apiKey: process.env.AIRTABLE_API_KEY }).base( - process.env.BASE - ); - try { - base('Project Work') - .select({ - // Selecting the first 3 records in All XP Requests: - maxRecords: 1000, - view: 'xp_view', - }) - .eachPage( - function page(records: any[], fetchNextPage: () => void) { - records.forEach((record) => { - // console.log('records fetched - ', record.fields); - const fields = record.fields; - const name = fields._Name as string; +async function getProjectWork_xp_view(): Promise< + receivedXPFromAirtableType[] | undefined +> { + return await getXPDistribution('Project Work'); +} - const xp = fields._XP as number; - const date = fields._Date as Date; - xp_allocated_for_projects.push({ - name, - xp: { xp_allocated: xp, date: date }, - }); - // console.log('xp - ', xp_allocated_for_projects); - }); +async function getIndieWork_xp_view(): Promise< + receivedXPFromAirtableType[] | undefined +> { + return await getXPDistribution('Indie Work'); +} +async function getWorkingGroups_xp_view(): Promise< + receivedXPFromAirtableType[] | undefined +> { + return await getXPDistribution('CAB/SubDAO XPs'); +} - fetchNextPage(); - }, - function done(err: any) { - if (err) { - console.error(err); - return; - } - } - ); - return xp_allocated_for_projects; - } catch (error) { - console.log(error); - return error; - } -}; +async function getInternalOperations_xp_view(): Promise< + receivedXPFromAirtableType[] | undefined +> { + return await getXPDistribution('CAB/SubDAO XPs'); +} -// this function fetches xp distributed for Bounties -const getBounties_xp_view = async () => {}; -// this function fetches xp distributed for Stack Exchanges -const getStackExchange_xp_view = async () => {}; +async function getBounties_xp_view(): Promise< + receivedXPFromAirtableType[] | undefined +> { + return await getXPDistribution('Indie Work'); +} +async function getStackExchange_xp_view(): Promise< + receivedXPFromAirtableType[] | undefined +> { + return await getXPDistribution('Indie Work'); +} export { - getBounties_xp_view, - getIndieWork_xp_view, getProjectWork_xp_view, + getIndieWork_xp_view, getWorkingGroups_xp_view, + getBounties_xp_view, getStackExchange_xp_view, getInternalOperations_xp_view, }; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 624934b..a6429ed 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,40 +1,30 @@ import { Container } from '@chakra-ui/react'; import DashboardHeader from '../components/Dashboard/DashboardHeader'; import LeaderBoardWrapper from '../components/Dashboard/LeaderBoardWrapper'; -import { ProjectsXPType } from '../interfaces/projectsXP'; -import { xpTableType } from '../interfaces/xpTable'; -import { - getBountiesRecordsFunction, - getXPFromAllViews, - getXPRecordFunction, -} from '../lib/airtable'; +import { dashboardDataType } from '../components/Dashboard/Row/interfaces/dashboardStore'; +import { dataCalculator } from '../util/dataCalculator'; export default function Home(props: { + dashboardData: dashboardDataType[]; bountyDataJson: any; - xpDataJson: xpTableType[]; - xpDataWithDate: ProjectsXPType[]; }) { - const { bountyDataJson, xpDataJson, xpDataWithDate } = props; - console.log('total xp with date - ', xpDataWithDate.length); + const { dashboardData } = props; return (
- +
); } export async function getStaticProps() { - const xpDataWithDate = await getXPFromAllViews(); - const xpDataJson = await getXPRecordFunction(); - const bountyDataJson = await getBountiesRecordsFunction(); + const { personData, bountyDataJson } = await dataCalculator(); return { props: { bountyDataJson, - xpDataJson: xpDataJson, - xpDataWithDate, + dashboardData: personData, }, revalidate: 10, }; diff --git a/src/util/dataCalculator.tsx b/src/util/dataCalculator.tsx new file mode 100644 index 0000000..350cfcf --- /dev/null +++ b/src/util/dataCalculator.tsx @@ -0,0 +1,177 @@ +import { receivedXPFromAirtableType } from '../components/Dashboard/Row/interfaces/airtableRecievedXP'; +import { + dashboardDataType, + totalOverallXPType, +} from '../components/Dashboard/Row/interfaces/dashboardStore'; +import { xpType } from '../components/Dashboard/Row/interfaces/xp'; +import { + getBountiesRecordsFunction, + getXPRecordFunction, +} from '../lib/airtable'; +import { + getBounties_xp_view, + getIndieWork_xp_view, + getInternalOperations_xp_view, + getProjectWork_xp_view, + getStackExchange_xp_view, +} from '../lib/getXPDistributions'; +import { + overallXPDetails, + sourceTotalXPCalculator, + xpSumBySkillCalculator, + xpSumPerPersonCalculator, +} from './sourceTotalXpCalculator'; + +export async function dataCalculator() { + const xpDataJson: [ + { + name: string; + total_xp: any; + person_type: string; + development: any; + design: any; + videography: any; + writing: any; + strategy: any; + operations: any; + } + ] = await getXPRecordFunction(); + + const projectWorkXP = + (await getProjectWork_xp_view()) as receivedXPFromAirtableType[]; + const indieWorkXp = + (await getIndieWork_xp_view()) as receivedXPFromAirtableType[]; + const internalOpsXp = + (await getInternalOperations_xp_view()) as receivedXPFromAirtableType[]; + const bountiesXP = + (await getBounties_xp_view()) as receivedXPFromAirtableType[]; + const stackExchangeXP = + (await getStackExchange_xp_view()) as receivedXPFromAirtableType[]; + const bountyDataJson = await getBountiesRecordsFunction(); + + const personDetailsData: { name: string; personType: string }[] = + xpDataJson.map((person) => { + return { + name: person.name, + personType: person.person_type, + }; + }); + + const overallXP = xpDataJson.map((person) => { + return { + name: person.name, + total_xp: person.total_xp, + development: person.development, + design: person.design, + operations: person.operations, + videography: person.videography, + strategy: person.strategy, + writing: person.writing, + }; + }); + + const projectWorkCalculatedXP: xpType[] = sourceTotalXPCalculator( + projectWorkXP + ).map((person) => { + return { + name: person.name, + skills: xpSumBySkillCalculator(person), + total_amount: xpSumPerPersonCalculator(person).total_amount, + xp: xpSumPerPersonCalculator(person).xp, + }; + }); + + const indieWorkCalculatedXP: xpType[] = sourceTotalXPCalculator( + indieWorkXp + ).map((person) => { + return { + name: person.name, + skills: xpSumBySkillCalculator(person), + total_amount: xpSumPerPersonCalculator(person).total_amount, + xp: xpSumPerPersonCalculator(person).xp, + }; + }); + + const internalOpsCalculateXP: xpType[] = sourceTotalXPCalculator( + internalOpsXp + ).map((person) => { + return { + name: person.name, + skills: xpSumBySkillCalculator(person), + total_amount: xpSumPerPersonCalculator(person).total_amount, + xp: xpSumPerPersonCalculator(person).xp, + }; + }); + // console.log('internal ops per person - ', internalOpsCalculateXP[0]) + //todo: internal ops data is not working till here it works + + const bountiesCalculatedXP: xpType[] = sourceTotalXPCalculator( + bountiesXP + ).map((person) => { + return { + name: person.name, + skills: xpSumBySkillCalculator(person), + total_amount: xpSumPerPersonCalculator(person).total_amount, + xp: xpSumPerPersonCalculator(person).xp, + }; + }); + + const stackExchangeCalculatedXP: xpType[] = sourceTotalXPCalculator( + stackExchangeXP + ).map((person) => { + return { + name: person.name, + skills: xpSumBySkillCalculator(person), + total_amount: xpSumPerPersonCalculator(person).total_amount, + xp: xpSumPerPersonCalculator(person).xp, + }; + }); + + // add all the xps into one object called person: {name: string, personType: string, overallXP: , projectWorkXP: , indieWorkXP: , internalOpsXp: } + const personData: dashboardDataType[] = personDetailsData.map((person) => { + const xpSourcesSum = overallXPDetails( + [ + projectWorkCalculatedXP.find( + (personXP) => personXP.name === person.name + ), + indieWorkCalculatedXP.find((personXP) => personXP.name === person.name), + internalOpsCalculateXP.find( + (personXP) => personXP.name === person.name + ), + bountiesCalculatedXP.find((personXP) => personXP.name === person.name), + stackExchangeCalculatedXP.find( + (personXP) => personXP.name === person.name + ), + ] as xpType[], + person.name, + overallXP.find( + (personXP) => personXP.name === person.name + ) as totalOverallXPType + ); + return { + name: person.name, + personType: person.personType, + overallXP: { + total: overallXP.find((personXP) => personXP.name === person.name), + details: xpSourcesSum, + }, + projectWorkXP: projectWorkCalculatedXP.find( + (personXP) => personXP.name === person.name + ), + indieWorkXP: indieWorkCalculatedXP.find( + (personXP) => personXP.name === person.name + ), + internalOpsXp: internalOpsCalculateXP.find( + (personXP) => personXP.name === person.name + ), + bountiesXP: bountiesCalculatedXP.find( + (personXP) => personXP.name === person.name + ), + stackExchangeXP: stackExchangeCalculatedXP.find( + (personXP) => personXP.name === person.name + ), + }; + }); + + return { personData, bountyDataJson }; +} diff --git a/src/util/sourceTotalXpCalculator.ts b/src/util/sourceTotalXpCalculator.ts new file mode 100644 index 0000000..45f2b6e --- /dev/null +++ b/src/util/sourceTotalXpCalculator.ts @@ -0,0 +1,210 @@ +import { receivedXPFromAirtableType } from '../components/Dashboard/Row/interfaces/airtableRecievedXP'; +import { receivedXPFromAirtableTotalType } from '../components/Dashboard/Row/interfaces/airtableRecievedXPTotal'; +import { totalOverallXPType } from '../components/Dashboard/Row/interfaces/dashboardStore'; +import { allocated_xp, xpType } from '../components/Dashboard/Row/interfaces/xp'; +import { skillKind } from '../enums/skill'; + +function remove_duplicates_and_add_value( + arr: { skill: skillKind; amount: number }[] +): { skill: skillKind; amount: number }[] { + // remove if a duplicate skill is present but add the amount of similar skills and make one object + let s = new Set(arr.map((item) => item.skill)); + let it = s.values(); + let unique_skills = Array.from(it); + let unique_skills_with_amount = unique_skills.map((skill) => { + let amount = 0; + arr.forEach((item) => { + if (item.skill === skill) { + amount += item.amount; + } + }); + return { skill: skill, amount: amount }; + }); + return unique_skills_with_amount; +} +// calculate details for overallxp ( type: xpType ) using all the xps from the sources +{ + /* + * @params { xpSources: xpType[]} // xp sources include projectWorkXP, indieWorkXP, workingGroupXP, bountiesXP, stackExchangeXP + * return { xpType } + */ +} +export function overallXPDetails( + xpSources: xpType[], + name: string, + overallXP: totalOverallXPType +): xpType { + let total_xp = overallXP.total_xp; + let skills: { skill: skillKind; amount: number }[] = [ + { + skill: skillKind.DEV, + amount: overallXP.development, + }, + { + skill: skillKind.DESIGN, + amount: overallXP.design, + }, + { + skill: skillKind.OPS, + amount: overallXP.operations, + }, + { + skill: skillKind.VIDEO, + amount: overallXP.videography, + }, + { + skill: skillKind.STRATEGY, + amount: overallXP.strategy, + }, + { + skill: skillKind.WRITING, + amount: overallXP.writing, + }, + ]; + let xp: { amount: number[]; dates: Date[] } = { amount: [], dates: [] }; + xpSources.forEach((xpSource) => { + if ( + !xpSource?.name || + !xpSource?.total_amount || + !xpSource?.skills || + !xpSource?.xp + ) + return; + name = xpSource.name; + xp.amount.push(...xpSource.xp.amount); + xp.dates.push(...xpSource.xp.dates); + }); + // filtering skill by amount (removing skill with amount 0 ) + skills = skills.filter((skill) => skill.amount !== 0); + return { name: name, total_amount: total_xp, skills: skills, xp: xp }; +} + +export function xpSumBySkillCalculator(person: { + name: string; + xp: { amount: number; date: Date; skill: skillKind }[]; +}) { + const skills: { skill: string; amount: number }[] | undefined = []; + + // iterating over person.xp and if skill matches we add the xp and push the value to skills array + person.xp.forEach((xp) => { + if (skills.some((skill) => skill.skill === xp.skill)) { + skills.forEach((skill) => { + if (skill.skill === xp.skill) { + skill.amount += xp.amount; + } + }); + } else { + skills.push({ skill: xp.skill, amount: xp.amount }); + } + }); + // filtering skill by amount (removing skill with amount 0 ) + skills.filter((skill) => skill.amount !== 0); + + // itterate over skills and replace skill name with skilltype enum + skills.forEach((skill) => { + if (skill.skill === 'Development') { + skill.skill = skillKind.DEV; + } else if (skill.skill === 'Design') { + skill.skill = skillKind.DESIGN; + } else if (skill.skill === 'Ops') { + skill.skill = skillKind.OPS; + } else if (skill.skill === 'Videography') { + skill.skill = skillKind.VIDEO; + } else if (skill.skill === 'Strategy') { + skill.skill = skillKind.STRATEGY; + } else if (skill.skill === 'Writing') { + skill.skill = skillKind.WRITING; + } + }); + + return skills as { skill: skillKind; amount: number }[]; +} + +export function xpSumPerPersonCalculator(person: { + name: string; + xp: { amount: number; date: Date; skill: skillKind }[]; +}): xpType { + const name = person.name; + const allocated_xp: number[] = []; + const dates: Date[] = [new Date()]; + const skillsArr: { skill: skillKind; amount: number }[] = []; + const every_person_xp_sum = person.xp.reduce((prevValue, currentValue) => { + allocated_xp.push(currentValue.amount); + dates.push(new Date(currentValue.date) as Date); + skillsArr.push({ + skill: currentValue.skill as skillKind, + amount: currentValue.amount, + }); + return { + amount: (prevValue.amount + currentValue.amount) as number, + date: currentValue.date, + skill: currentValue.skill, + }; + }); + + dates.shift(); + + const skills: { skill: skillKind; amount: number }[] = + remove_duplicates_and_add_value(skillsArr); + const total_xp_amount: number = every_person_xp_sum.amount; + const newPerson: xpType = { + name: name, + total_amount: total_xp_amount, + skills: skills, + xp: { amount: allocated_xp, dates }, + }; + + return newPerson; +} + +export function skillTotalXPCalculator( + source_xp: allocated_xp[], + skill: string +) { + // sum all the allocated xp + const total_xp_for_provided_skill = source_xp.reduce( + (accumulator: any, currentValue: any) => { + if (currentValue.skill === skill) { + return accumulator.amount + currentValue.amount; + } + return accumulator; + } + ); + return total_xp_for_provided_skill; +} + +export function sourceTotalXPCalculator( + xp_source_data: receivedXPFromAirtableType[] +): receivedXPFromAirtableTotalType[] { + const sourceTotalXPArr: receivedXPFromAirtableTotalType[] = [ + { name: '', xp: [{ amount: 0, date: new Date(), skill: skillKind.DEV }] }, + ]; + + xp_source_data?.forEach((xp_source) => { + const index = sourceTotalXPArr.findIndex( + (item) => item.name === xp_source.name + ); + if (index === -1) { + sourceTotalXPArr.push({ + name: xp_source.name, + xp: [ + { + amount: xp_source.xp.total_amount, + date: xp_source.xp.date, + skill: xp_source.xp.skill, + }, + ], + }); + } else { + sourceTotalXPArr[index].xp.push({ + amount: xp_source.xp.total_amount, + date: xp_source.xp.date, + skill: xp_source.xp.skill, + }); + } + }); + + // remove the first element from the array + sourceTotalXPArr.shift(); + return sourceTotalXPArr; +}