Skip to content

Commit

Permalink
Leader Board : Shows Top Play Contributor of the month and Top Play C…
Browse files Browse the repository at this point in the history
…ontributors of all time (reactplay#843)

* Adding leader board page.

* 1. Heading to next line.
2. sorting based on date when monthly contributor play count is same.

* Addressing review comment.

* Fixing lint errors.

* Added the API env file

* header styles.

* Removeing eslint rule form component.

* Fixing responsive issues.

* Adding a test and fixing breaking tests.

* Adding tooltip.

* Bigger resolution fix.

Co-authored-by: Nagarjun Shroff <[email protected]>
Co-authored-by: Tapas Adhikary <[email protected]>
  • Loading branch information
3 people committed Jan 10, 2023
1 parent f4157cf commit 8e84d87
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .env.development
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
REACT_APP_NHOST_BACKEND_URL=https://rgkjmwftqtbpayoyolwh.nhost.run/
REACT_APP_NHOST_VERSION=v1
REACT_APP_NHOST_ENDPOINT=graphql
DISABLE_ESLINT_PLUGIN=true
REACT_APP_PLAY_WEB_SVC=https://api.reactplay.io/.netlify/functions/server
DISABLE_ESLINT_PLUGIN=true
1 change: 1 addition & 0 deletions cypress/e2e/homePage.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('Test home page', () => {
cy.get('[data-testid="github-btn"]').should('be.visible');
cy.get('[data-testid="twitter-btn"]').should('be.visible');
cy.get('[data-testid="share-btn"]').should('be.visible');
cy.get('[data-testid="leaderboard-btn"]').should('be.visible');

cy.get('@browseBtn').click();
cy.get('[data-testid="plays-search-box-container"]').should('be.visible');
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/constant.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const TWEET_COUNT = 10;
export const TWEET_COUNT = 11;
export const CONTRIBUTORS_COUNT = 29;
6 changes: 6 additions & 0 deletions src/common/header/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ const Header = () => {
showBrowse: false,
setHeaderStyle: true
});
} else if (pathName.startsWith('/leaderboard')) {
setShowHideBits({
showSearch: false,
showBrowse: true,
setHeaderStyle: true
});
}
}, [pathName]);

Expand Down
13 changes: 12 additions & 1 deletion src/common/header/HeaderNav.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { BsTwitter, BsGithub } from 'react-icons/bs';
import { BsTwitter, BsGithub, BsTrophyFill } from 'react-icons/bs';
import { FaLightbulb } from 'react-icons/fa';
import { BiMoney } from 'react-icons/bi';
import { IoAddSharp, IoShareSocial, IoHeartSharp } from 'react-icons/io5';
Expand Down Expand Up @@ -103,6 +103,17 @@ const HeaderNav = ({ showBrowse }) => {
</a>
)}
</li>
<li>
<Link
className="app-header-btn app-header-btn--default"
data-testid="leaderboard-btn"
title="Leader Board"
to="/leaderboard"
>
<BsTrophyFill className="icon idea-icon" />
<span className="btn-label">Leader Board</span>
</Link>
</li>
<li>
<Link
className="app-header-btn app-header-btn--default"
Expand Down
4 changes: 3 additions & 1 deletion src/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import TechStack from './techstack/TechStacks';
import ExtendedFooter from './footer/ExtendedFooter';
import CreatePlay from './createplay/CreatePlay';
import PlayCreated from './playcreated/PlayCreated';
import LeaderBoard from './playleaderboard/LeaderBoard';

export {
Header,
Expand All @@ -23,5 +24,6 @@ export {
CreatePlay,
PlayCreated,
TechStack,
ExtendedFooter
ExtendedFooter,
LeaderBoard
};
98 changes: 98 additions & 0 deletions src/common/playleaderboard/LeaderBoard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useState, useEffect, useCallback } from 'react';
import { submit } from '../services/request';
import { FetchPlayCountByUser } from 'common/services/request/query/fetch-leaderboard-data';
import TopPlayCreatorOfTheMonth from './TopPlayCreatorOfTheMonth';
import TopPlayCreators from './TopPlayCreators';
import { Watch } from 'react-loader-spinner';
import { groupBy } from 'lodash';
import { format, lastDayOfMonth } from 'date-fns';

const LeaderBoard = () => {
const [top10Contributors, updateTop10Contributors] = useState([]);
const [topContributorOfTheMonth, updateTopContributorOfTheMonth] = useState([]);
const [publishedPlays, updatePublishedPlays] = useState([]);
const { getAllPlaysByUser, getAllPlaysByUserByMonth } = FetchPlayCountByUser;

const formatData = useCallback((data, monthlyContribution = false) => {
const formattedData = [];
const finalData = [];
const filteredData = data.filter((d) => publishedPlays.includes(d.slug));
filteredData.map((d) => {
formattedData.push({
created_at: d.created_at,
displayName: d.user.displayName,
avatarUrl: d.user.avatarUrl
});
});
const groupByUser = groupBy(formattedData, 'displayName');
Object.values(groupByUser).map((v) =>
finalData.push({
displayName: v[0].displayName,
avatarUrl: v.map((t) => t.avatarUrl)[0],
count: v.length,
created_at: v[0].created_at
})
);
const sortByCount = finalData.sort((a, b) => b.count - a.count);
if (monthlyContribution) {
// once the data being sorted based on created date, will pick the first one.
return sortByCount.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0];
}

return sortByCount.slice(0, 10);
});

useEffect(() => {
async function fetchData() {
const res = await fetch(`${process.env.REACT_APP_PLAY_WEB_SVC}/api/plays/published`);
const data = await res.json();
updatePublishedPlays(data);
}
fetchData();
}, []);

useEffect(() => {
async function fetchTopContributors() {
const data = await submit(getAllPlaysByUser());
updateTop10Contributors(formatData(data));
}

async function fetchTopContributorOfTheMonth() {
const firstDateOfMonth = format(new Date(), 'yyyy-MM-01');
const lastDateOfMonth = format(lastDayOfMonth(new Date()), 'yyyy-MM-dd');
const data = await submit(getAllPlaysByUserByMonth(firstDateOfMonth, lastDateOfMonth));
updateTopContributorOfTheMonth(formatData(data, true));
}

fetchTopContributors();
fetchTopContributorOfTheMonth();
}, [publishedPlays]);

return (
<main className="app-body app-body-overflow-hidden">
{publishedPlays.length && (topContributorOfTheMonth || top10Contributors) ? (
<div className=" overflow-auto lg:flex flex-row justify-center">
{topContributorOfTheMonth && (
<TopPlayCreatorOfTheMonth topPlayCreatorOfTheMonth={topContributorOfTheMonth} />
)}
<div className="flex flex-col m-4 items-center">
{publishedPlays && top10Contributors && (
<>
<div className="leaderboard-heading">Top play creators of all time</div>
<div>
<TopPlayCreators topPlayCreators={top10Contributors} />
</div>
</>
)}
</div>
</div>
) : (
<div className="leaderboard-loader">
<Watch color="#0096AB" height="100" width="100" />
</div>
)}
</main>
);
};

export default LeaderBoard;
31 changes: 31 additions & 0 deletions src/common/playleaderboard/TopPlayCreatorOfTheMonth.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { format } from 'date-fns';

const TopPlayCreatorOfTheMonth = ({ topPlayCreatorOfTheMonth }) => {
return (
<div className="m-4 text-center flex lg:flex flex-col gap-4 items-center">
<div className="flex flex-col lg:w-[600px] md:w-[600px]">
<div className="leaderboard-heading">Top play creator of the month</div>
<div className="leaderboard-heading">{format(new Date(), 'LLLL yyyy')}</div>
</div>
<div className="flex flex-col items-center m-1">
<div>
<img
alt="profile pic"
className="rounded-full border-solid h-48 lg:h-80"
src={topPlayCreatorOfTheMonth.avatarUrl}
title={topPlayCreatorOfTheMonth.displayName}
/>
</div>
<div className="flex flex-col m-8 items-center">
<div className="leaderboard-text">{topPlayCreatorOfTheMonth.displayName}</div>
<div className="flex flex-row gap-2 items-center">
<div className="font-medium text-sm">Number of play(s) created:</div>
<div className="font-medium text-sm">{topPlayCreatorOfTheMonth.count}</div>
</div>
</div>
</div>
</div>
);
};

export default TopPlayCreatorOfTheMonth;
57 changes: 57 additions & 0 deletions src/common/playleaderboard/TopPlayCreators.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import './leaderBoard.css';

const TopPlayCreators = ({ topPlayCreators }) => {
const profilePicture = (name, avatarUrl) => {
return (
<div className="flex flex-row items-center gap-4">
<img
alt={name}
className="rounded-full border-solid h-8 w-8"
src={avatarUrl}
title={name}
/>
<div className="leaderboard-table-cell">{name}</div>
</div>
);
};

return (
<TableContainer>
<Table aria-label="leader board">
<TableHead>
<TableRow>
<TableCell align="left" className="leaderboard-table-header">
Name
</TableCell>
<TableCell align="center" className="leaderboard-table-header">
Number of plays
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{topPlayCreators.map((creator) => (
<TableRow
key={creator.displayName}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell align="left" className="leaderboard-table-cell" component="th" scope="row">
{profilePicture(creator.displayName, creator.avatarUrl)}
</TableCell>
<TableCell align="center" className="leaderboard-table-cell">
{creator.count}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};

export default TopPlayCreators;
25 changes: 25 additions & 0 deletions src/common/playleaderboard/leaderBoard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.leaderboard-table-cell {
padding: 6px !important;
}

.leaderboard-table-header {
font-weight: 700 !important;
font-family: var(--ff-accent) !important;
}

.leaderboard-heading {
font-family: var(--ff-accent);
font-size: 32px !important;
}

.leaderboard-text {
font-family: var(--ff-accent) !important;
font-size: var(--fs-md) !important;
}

.leaderboard-loader {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
5 changes: 4 additions & 1 deletion src/common/routing/RouteDefs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CreatePlay,
PlayCreated,
TechStack,
LeaderBoard,
PageNotFound
} from 'common';
import PlayList from 'common/playlists/PlayList';
Expand All @@ -29,7 +30,8 @@ const RouteDefs = () => {
{ path: '/plays', title: 'ReactPlay - Plays' },
{ path: '/ideas', title: 'ReactPlay - Ideas' },
{ path: '/tech-stacks', title: 'ReactPlay - Tech Stacks' },
{ path: '/plays/create', title: 'ReactPlay - Create Play' }
{ path: '/plays/create', title: 'ReactPlay - Create Play' },
{ path: '/leaderboard', title: 'ReactPlay - Leader Board' }
];

return (
Expand Down Expand Up @@ -66,6 +68,7 @@ const RouteDefs = () => {
<Route index element={<PlayList />} />
</Route>
<Route element={<PlayIdeas />} path="/ideas" />
<Route element={<LeaderBoard />} path="/leaderboard" />
</Routes>
<Footer />
</BrowserRouter>
Expand Down
41 changes: 41 additions & 0 deletions src/common/services/request/query/fetch-leaderboard-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const allPlaysByUser = {
display: 'Fetch play count group by the creator',
name: 'Fetch_Play_By_Users',
function: 'plays',
return: [
'created_at',
'slug',
{
user: ['avatarUrl', 'displayName', 'id', 'email']
}
]
};

export const FetchPlayCountByUser = {
getAllPlaysByUser() {
return { ...allPlaysByUser };
},

getAllPlaysByUserByMonth(gte, lte) {
return {
...allPlaysByUser,
where: {
clause: {
operator: 'and',
conditions: [
{
field: 'created_at',
operator: 'lte',
value: lte
},
{
field: 'created_at',
operator: 'gte',
value: gte
}
]
}
}
};
}
};

0 comments on commit 8e84d87

Please sign in to comment.