Skip to content

Commit acd8fab

Browse files
committed
feat: add order options
1 parent 8c07450 commit acd8fab

13 files changed

Lines changed: 257 additions & 20 deletions

File tree

app/components/Buttons/Buttons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import clsx from "clsx";
22
import type { Dispatch, ReactNode, SetStateAction } from "react";
33
import css from "./buttons.module.scss";
44

5-
interface IButtons<T> {
5+
export interface IButtons<T> {
66
name: string | ReactNode;
77
id: T;
88
}

app/components/Buttons/Radio.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { type Dispatch, type SetStateAction, useEffect, useState } from "react";
2+
import type { IButtons } from "./Buttons";
3+
import Buttons from "./Buttons";
4+
5+
export default function Radio<T>({
6+
values,
7+
state,
8+
setState,
9+
column,
10+
}: {
11+
values: (IButtons<T> | null | undefined)[];
12+
state: T;
13+
setState: Dispatch<SetStateAction<T>>;
14+
column?: boolean;
15+
}) {
16+
return (
17+
<Buttons<T>
18+
buttons={values}
19+
state={[state]}
20+
setState={(val) => {
21+
const nextValue = typeof val === "function" ? val([state]) : val;
22+
23+
if (nextValue.length === 0) {
24+
return;
25+
}
26+
27+
const newValue = nextValue.filter((x) => x !== state)[0] || state;
28+
setState(newValue);
29+
}}
30+
column={column}
31+
/>
32+
);
33+
}

app/components/ChallengeManager/Challenge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export default function Challenge({
7474
{isUser && (
7575
<>
7676
{/* Position is disabled until further notice */}
77-
{/* {position > 0 && `#${user.position} - `} */}
77+
{position > 0 && `#${formatNumber(position)} - `}
7878
{user.percentile !== undefined &&
7979
`Top ${(user.percentile * 100).toFixed(1)}% - `}
8080
{user.percentile === undefined &&

app/components/ChallengeManager/ChallengeManager.tsx

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { serialize } from "cookie";
22
import { useEffect, useMemo, useState } from "react";
3+
import { FaSortAlphaDown } from "react-icons/fa";
4+
import {
5+
FaArrowUpRightDots,
6+
FaHashtag,
7+
FaRankingStar,
8+
FaRegClock,
9+
} from "react-icons/fa6";
310
import { Link } from "react-router";
411
import SimpleBar from "simplebar-react";
512
import "simplebar-react/dist/simplebar.min.css";
613
import Buttons from "@cgg/components/Buttons/Buttons";
7-
import { brandName, cookieNames } from "@cgg/config/config";
14+
import { cookieNames } from "@cgg/config/config";
815
import { useStaticData } from "@cgg/hooks/useStaticData";
916
import type { ChallengesLoaderLocation } from "@cgg/loader/challengesFilter";
1017
import { cdnAssets, getChallengeIcon, getGamemodeIcon } from "@cgg/utils/cdn";
@@ -16,6 +23,7 @@ import type { Category, GameMode, Source } from "@cgg/utils/challenges";
1623
import { categories, gameModes, sources } from "@cgg/utils/challenges";
1724
import type { IApiChallengeResponse } from "@cgg/utils/endpoints/types";
1825
import { getChallenge } from "@cgg/utils/getChallenge";
26+
import Radio from "../Buttons/Radio";
1927
import Searchbar from "../Searchbar/Searchbar";
2028
import Challenge from "./Challenge";
2129
import css from "./challengeManager.module.scss";
@@ -47,7 +55,9 @@ export default function ChallengeManager({
4755
defaultFilter.gameMode,
4856
);
4957

50-
const sortMode: SortMode = "Name-ASC";
58+
const [sortMode, setSortMode] = useState<SortMode>(
59+
isUserInterface ? "Rank" : "Name-ASC",
60+
);
5161

5262
const filter: IChallengeFilter = {
5363
search,
@@ -67,7 +77,11 @@ export default function ChallengeManager({
6777
});
6878
}, [filter]);
6979

70-
const results = sortChallenges(filterChallenges(challenges, filter), sortMode);
80+
const results = sortChallenges(
81+
filterChallenges(challenges, filter),
82+
sortMode,
83+
userData?.challenges,
84+
);
7185

7286
return (
7387
<>
@@ -76,6 +90,60 @@ export default function ChallengeManager({
7690
<SimpleBar style={{ height: "100%" }}>
7791
<div className={css.innerScrollbar}>
7892
<div className={css.options}>Options...</div>
93+
94+
{isUserInterface && (
95+
<div className={css.group}>
96+
<p className={css.heading}>Order By</p>
97+
<Radio<SortMode>
98+
values={[
99+
{
100+
name: (
101+
<div className={css.buttonText}>
102+
<FaRankingStar /> Rank
103+
</div>
104+
),
105+
id: "Rank",
106+
},
107+
{
108+
name: (
109+
<div className={css.buttonText}>
110+
<FaRegClock /> Last Updated
111+
</div>
112+
),
113+
id: "Last Updated",
114+
},
115+
{
116+
name: (
117+
<div className={css.buttonText}>
118+
<FaHashtag /> Position
119+
</div>
120+
),
121+
id: "Position",
122+
},
123+
{
124+
name: (
125+
<div className={css.buttonText}>
126+
<FaArrowUpRightDots /> Closest Levelup
127+
</div>
128+
),
129+
id: "Levelup",
130+
},
131+
{
132+
name: (
133+
<div className={css.buttonText}>
134+
<FaSortAlphaDown /> Name
135+
</div>
136+
),
137+
id: "Name-ASC",
138+
},
139+
]}
140+
setState={setSortMode}
141+
state={sortMode}
142+
column
143+
/>
144+
</div>
145+
)}
146+
79147
<div className={css.group}>
80148
<p className={css.heading}>Category</p>
81149
<Buttons<Category>
@@ -176,6 +244,7 @@ export default function ChallengeManager({
176244
challengeId: challenge.id,
177245
tier: "UNRANKED",
178246
value: 0,
247+
percentile: 100,
179248
};
180249
}
181250

app/components/ChallengeManager/challengeManager.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
.challenges {
7171
.searchbar {
7272
width: 100%;
73-
background-color: color.$accent;
73+
backdrop-filter: blur(5px);
7474
position: sticky;
7575
padding-top: 10px;
7676
top: config.$header-height;

app/components/ChallengeManager/sortChallenges.ts

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
import type { IChallengeDTO } from "@cgg/utils/challenges";
2+
import type { IApiChallenge } from "@cgg/utils/endpoints/types";
3+
import { getNextTier, getTierIndex } from "@cgg/utils/getTier";
24

3-
export type SortMode = "Name-ASC" | "Name-DESC";
5+
export type SortMode =
6+
| "Name-ASC"
7+
| "Name-DESC"
8+
| "Rank"
9+
| "Last Updated"
10+
| "Position"
11+
| "Levelup";
412

13+
// TODO: Maps would probably be more efficient here
514
export function sortChallenges(
615
challenges: IChallengeDTO[],
716
mode: SortMode,
17+
userChallenges?: IApiChallenge[],
818
): IChallengeDTO[] {
919
switch (mode) {
1020
case "Name-ASC":
@@ -16,7 +26,127 @@ export function sortChallenges(
1626
});
1727
break;
1828
case "Name-DESC":
19-
challenges = challenges.sort((a, b) => b.name.localeCompare(a.name));
29+
challenges = challenges.sort((a, b) => {
30+
if (a.retired && !b.retired) return 1;
31+
if (!a.retired && b.retired) return -1;
32+
33+
return b.name.localeCompare(a.name);
34+
});
35+
break;
36+
37+
case "Rank":
38+
// When this filter - for whatever reason - gets selected on a non-user page
39+
if (!userChallenges) {
40+
return sortChallenges(challenges, "Name-ASC");
41+
}
42+
challenges = challenges.sort((a, b) => {
43+
if (a.retired && !b.retired) return 1;
44+
if (!a.retired && b.retired) return -1;
45+
46+
const userChallengeA = userChallenges.find((uc) => uc.challengeId === a.id);
47+
const userChallengeB = userChallenges.find((uc) => uc.challengeId === b.id);
48+
const tierA = getTierIndex(userChallengeA?.tier);
49+
const tierB = getTierIndex(userChallengeB?.tier);
50+
51+
// sort by tier first, then by percentile
52+
if (tierA !== tierB) return tierB - tierA;
53+
54+
return (userChallengeA?.percentile || 0) - (userChallengeB?.percentile || 0);
55+
});
56+
break;
57+
58+
case "Last Updated":
59+
// When this filter - for whatever reason - gets selected on a non-user page
60+
if (!userChallenges) {
61+
return sortChallenges(challenges, "Name-ASC");
62+
}
63+
challenges = challenges.sort((a, b) => {
64+
if (a.retired && !b.retired) return 1;
65+
if (!a.retired && b.retired) return -1;
66+
67+
const userChallengeA = userChallenges.find((uc) => uc.challengeId === a.id);
68+
const userChallengeB = userChallenges.find((uc) => uc.challengeId === b.id);
69+
70+
if (userChallengeA && userChallengeB) {
71+
return (
72+
new Date(userChallengeB.achievedTime || 0).getTime() -
73+
new Date(userChallengeA.achievedTime || 0).getTime()
74+
);
75+
}
76+
77+
if (userChallengeA && !userChallengeB) return -1;
78+
if (!userChallengeA && userChallengeB) return 1;
79+
return 0;
80+
});
81+
break;
82+
83+
case "Position":
84+
// When this filter - for whatever reason - gets selected on a non-user page
85+
if (!userChallenges) {
86+
return sortChallenges(challenges, "Name-ASC");
87+
}
88+
challenges = challenges.sort((a, b) => {
89+
if (a.retired && !b.retired) return 1;
90+
if (!a.retired && b.retired) return -1;
91+
92+
const userChallengeA = userChallenges.find((uc) => uc.challengeId === a.id);
93+
const userChallengeB = userChallenges.find((uc) => uc.challengeId === b.id);
94+
95+
if (userChallengeA && userChallengeB) {
96+
const positionA = userChallengeA.position || 0;
97+
const positionB = userChallengeB.position || 0;
98+
99+
// A validation check with <= 0 is necessary, as it is not uncommon for the API to return negative positions
100+
if (positionA > 0 && positionB <= 0) return -1;
101+
if (positionA <= 0 && positionB > 0) return 1;
102+
if (positionA <= 0 && positionB <= 0) return 0;
103+
return (positionA as number) - (positionB as number);
104+
}
105+
106+
if (userChallengeA && !userChallengeB) return -1;
107+
if (!userChallengeA && userChallengeB) return 1;
108+
return 0;
109+
});
110+
111+
break;
112+
113+
case "Levelup":
114+
// When this filter - for whatever reason - gets selected on a non-user page
115+
if (!userChallenges) {
116+
return sortChallenges(challenges, "Name-ASC");
117+
}
118+
challenges = challenges.sort((a, b) => {
119+
if (a.retired && !b.retired) return 1;
120+
if (!a.retired && b.retired) return -1;
121+
122+
const userChallengeA = userChallenges.find((uc) => uc.challengeId === a.id);
123+
const userChallengeB = userChallenges.find((uc) => uc.challengeId === b.id);
124+
if (userChallengeA && !userChallengeB) return -1;
125+
if (!userChallengeA && userChallengeB) return 1;
126+
if (!userChallengeA && !userChallengeB) return 0;
127+
128+
const currentValueA = userChallengeA!.value;
129+
const nextValueA =
130+
a.thresholds[getNextTier(userChallengeA!.tier, a, false)].points;
131+
const currentValueB = userChallengeB!.value;
132+
const nextValueB =
133+
b.thresholds[getNextTier(userChallengeB!.tier, b, false)].points;
134+
135+
if (nextValueA === 0 && nextValueB === 0) return 0;
136+
if (nextValueA === 0) return -1;
137+
if (nextValueB === 0) return 1;
138+
139+
const progressA = Math.min(currentValueA / nextValueA, 1);
140+
const progressB = Math.min(currentValueB / nextValueB, 1);
141+
142+
// Already "maxed challenges" (progress >= 1) can be put at the back of the list
143+
if (progressA >= 1 && progressA === progressB) return 0;
144+
if (progressA >= 1 && progressB < 1) return 1;
145+
if (progressA < 1 && progressB >= 1) return -1;
146+
147+
return progressB - progressA;
148+
});
149+
20150
break;
21151
default:
22152
break;

app/components/Searchbar/searchbar.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
color: color.$text;
3333

3434
font-size: config.$font-default;
35+
36+
&::placeholder {
37+
color: color.$secondary;
38+
}
3539
}
3640

3741
&::after {

app/loader/challengesFilter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function challengesLoader(
3131
try {
3232
const parsed = JSON.parse(atob(values[cookiename] ?? "{}"));
3333
filter = {
34-
search: parsed.search ?? "",
34+
search: parsed.search.trim() ?? "",
3535
category: parsed.category ?? [],
3636
source: parsed.source ?? [],
3737
gameMode: parsed.gameMode ?? [],

app/styles/_config.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ $font-small: 0.85rem !default;
1111
*/
1212
$borderRadius: 12px !default;
1313
$navigation: 70px !default;
14-
$default-container: 900px !default;
14+
$default-container: 1000px !default;
1515
$small-container: 700px !default;
1616
$large-container: 1200px !default;
1717
$header-height: 70px !default;

app/styles/profile.layout.module.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
.bg {
44
width: 100%;
5+
max-height: 100%;
56
position: absolute;
67
z-index: 0;
78

9+
object-fit: cover;
10+
811
opacity: 0.5;
912
}
1013

0 commit comments

Comments
 (0)