Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search leaderboard and scroll to your position #69

Merged
merged 11 commits into from
Sep 15, 2023
3 changes: 3 additions & 0 deletions public/assets/icons/my-position-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 29 additions & 3 deletions src/components/leaderboard/LeaderboardRow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Image from 'next/image';
import { RefObject } from 'react';
SheepTester marked this conversation as resolved.
Show resolved Hide resolved
import styles from './style.module.scss';

interface LeaderboardRowProps {
Expand All @@ -7,11 +8,24 @@ interface LeaderboardRowProps {
name: string;
points: number;
image: string;
match?: {
index: number;
length: number;
};
rowRef: RefObject<HTMLDivElement> | null;
}

const LeaderboardRow = ({ position, rank, name, points, image }: LeaderboardRowProps) => {
const LeaderboardRow = ({
position,
rank,
name,
points,
image,
match,
rowRef,
}: LeaderboardRowProps) => {
return (
<div className={styles.row} data-style={position % 2 === 0 ? 'even' : 'odd'}>
<div className={styles.row} ref={rowRef}>
<span className={styles.position}>{position}</span>
<Image
src={image}
Expand All @@ -22,7 +36,19 @@ const LeaderboardRow = ({ position, rank, name, points, image }: LeaderboardRowP
className={styles.profilePicture}
/>
<div className={styles.nameRank}>
<span className={styles.name}>{name}</span>
<span className={styles.name}>
{match ? (
<>
{name.slice(0, match.index)}
<span className={styles.match}>
{name.slice(match.index, match.index + match.length)}
</span>
{name.slice(match.index + match.length)}
</>
) : (
name
)}
</span>
<span className={styles.rank}>{rank}</span>
</div>
<span className={styles.points}>{points} points</span>
Expand Down
10 changes: 9 additions & 1 deletion src/components/leaderboard/LeaderboardRow/style.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
padding-right: 1.5rem;
}

&[data-style='odd'] {
&:nth-child(2n) {
SheepTester marked this conversation as resolved.
Show resolved Hide resolved
background-color: var(--theme-background);
}

Expand All @@ -32,18 +32,26 @@
}

.nameRank {
align-items: center;
display: grid;
gap: 2rem;
grid-template-columns: 1fr 1fr;

@media (max-width: vars.$breakpoint-md) {
align-items: flex-start;
display: flex;
flex-direction: column;
gap: 0.5em;
}

.name {
font-weight: 500;

.match {
background-color: var(--theme-primary-2);
border-radius: 2px;
color: var(--theme-background);
}
}

.rank {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type Styles = {
match: string;
name: string;
nameRank: string;
points: string;
Expand Down
98 changes: 72 additions & 26 deletions src/pages/leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,103 @@ import { LeaderboardRow, TopThreeCard } from '@/components/leaderboard';
import { LeaderboardAPI } from '@/lib/api';
import withAccessType from '@/lib/hoc/withAccessType';
import { CookieService, PermissionService } from '@/lib/services';
import { PublicProfile } from '@/lib/types/apiResponses';
import { PrivateProfile, PublicProfile } from '@/lib/types/apiResponses';
import { CookieType } from '@/lib/types/enums';
import { getProfilePicture, getUserRank } from '@/lib/utils';
import MyPositionIcon from '@/public/assets/icons/my-position-icon.svg';
import styles from '@/styles/pages/leaderboard.module.scss';
import { GetServerSideProps } from 'next';
import { useRef, useState } from 'react';

interface Match {
index: number;
length: number;
}
function filter<T extends PublicProfile>(users: T[], query: string): (T & { match?: Match })[] {
const search = query.toLowerCase();
return users.flatMap(user => {
const index = `${user.firstName} ${user.lastName}`.toLowerCase().indexOf(search);
return index !== -1 ? [{ ...user, match: { index, length: search.length } }] : [];
});
}

interface LeaderboardProps {
leaderboard: PublicProfile[];
user: PrivateProfile;
}

const LeaderboardPage = (props: LeaderboardProps) => {
const { leaderboard } = props;
const LeaderboardPage = ({ leaderboard, user: { uuid } }: LeaderboardProps) => {
const [query, setQuery] = useState('');
const myPosition = useRef<HTMLDivElement>(null);

const topThreeUsers = leaderboard.slice(0, 3);

const leaderboardRows = leaderboard.slice(3);
const results = leaderboard.map((user, index) => ({ ...user, position: index + 1 }));
const topThreeUsers = query === '' ? filter(results.slice(0, 3), query) : [];
const leaderboardRows = filter(query === '' ? results.slice(3) : results, query);

return (
<div className={styles.container}>
<div className={styles.header}>
<h1 className={styles.heading}>Leaderboard</h1>
<button
className={styles.myPosition}
type="button"
onClick={() => {
myPosition.current?.scrollIntoView();
// Remove `.flash` in case it was already applied
myPosition.current?.classList.remove(styles.flash);
window.requestAnimationFrame(() => {
myPosition.current?.classList.add(styles.flash);
});
}}
>
My Position
<MyPositionIcon />
</button>
<input
className={styles.search}
type="search"
placeholder="Search Users"
aria-label="Search Users"
value={query}
onChange={e => setQuery(e.currentTarget.value)}
/>
<select name="timeOptions" id="timeOptions">
<option>All Time</option>
</select>
</div>
<div className={styles.topThreeContainer}>
{topThreeUsers.map((user, index) => (
<TopThreeCard
key={user.uuid}
position={index + 1}
rank={getUserRank(user)}
name={`${user.firstName} ${user.lastName}`}
points={user.points}
image={getProfilePicture(user)}
/>
))}
</div>
<div className={styles.leaderboard}>
{leaderboardRows.map((user, index) => {
return (
<LeaderboardRow
{topThreeUsers.length > 0 && (
<div className={styles.topThreeContainer}>
{topThreeUsers.map(user => (
<TopThreeCard
key={user.uuid}
position={index + 4}
position={user.position}
rank={getUserRank(user)}
name={`${user.firstName} ${user.lastName}`}
points={user.points}
image={getProfilePicture(user)}
/>
);
})}
</div>
))}
</div>
)}
{leaderboardRows.length > 0 && (
<div className={styles.leaderboard}>
{leaderboardRows.map(user => {
return (
<LeaderboardRow
key={user.uuid}
position={user.position}
rank={getUserRank(user)}
name={`${user.firstName} ${user.lastName}`}
points={user.points}
image={getProfilePicture(user)}
match={user.match}
rowRef={user.uuid === uuid ? myPosition : null}
/>
);
})}
</div>
)}
{topThreeUsers.length === 0 && leaderboardRows.length === 0 && <p>No results.</p>}
</div>
);
};
Expand Down
74 changes: 68 additions & 6 deletions src/styles/pages/leaderboard.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,55 @@
.header {
align-items: center;
display: flex;
justify-content: space-between;
}
gap: 0 2rem;

@media (max-width: vars.$breakpoint-lg) {
display: grid;
grid-auto-flow: column;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
justify-content: space-between;
justify-items: end;
}

.heading {
font-size: 1.5rem;
justify-self: start;
letter-spacing: 1%;
line-height: 2rem;
margin-right: auto;
}

.heading {
font-size: 1.5rem;
letter-spacing: 1%;
line-height: 2rem;
.myPosition {
align-items: center;
background-color: var(--theme-primary-2);
border-radius: 1rem;
color: var(--theme-background);
display: flex;
font-size: 1rem;
font-weight: bold;
gap: 0.5rem;
height: 2rem;
justify-self: start;
padding: 0 1rem;
}

.search {
background: none;
border: 1px solid var(--theme-accent-line-1);
border-radius: 1rem;
color: inherit;
font-size: 14px;
height: 2rem;
max-width: 18rem;
padding-left: 1.5rem;
padding-right: 0.5rem;
width: 100%;

&::placeholder {
color: var(--theme-text-on-background-3);
}
}
}

.topThreeContainer {
Expand All @@ -41,4 +83,24 @@
margin: 0 -2rem;
}
}

.flash {
animation: flash 2s;
@keyframes flash {
0%,
20%,
40%,
60% {
background-color: var(--theme-background);
color: var(--theme-text-on-background-1);
}

10%,
30%,
50% {
background-color: var(--theme-primary-2);
color: var(--theme-background);
}
}
}
}
3 changes: 3 additions & 0 deletions src/styles/pages/leaderboard.module.scss.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export type Styles = {
container: string;
flash: string;
header: string;
heading: string;
leaderboard: string;
myPosition: string;
search: string;
topThreeContainer: string;
};

Expand Down