diff options
Diffstat (limited to 'frontend/src/pages/User.tsx')
| -rw-r--r-- | frontend/src/pages/User.tsx | 525 |
1 files changed, 308 insertions, 217 deletions
diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx index d43c0c6..8c699b1 100644 --- a/frontend/src/pages/User.tsx +++ b/frontend/src/pages/User.tsx | |||
| @@ -1,15 +1,25 @@ | |||
| 1 | import React from 'react'; | 1 | import React from "react"; |
| 2 | import { Link, useLocation, useNavigate } from 'react-router-dom'; | 2 | import { Link, useLocation, useNavigate } from "react-router-dom"; |
| 3 | import { Helmet } from 'react-helmet'; | 3 | import { Helmet } from "react-helmet"; |
| 4 | 4 | ||
| 5 | import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '@images/Images'; | 5 | import { |
| 6 | import { UserProfile } from '@customTypes/Profile'; | 6 | SteamIcon, |
| 7 | import { Game, GameChapters } from '@customTypes/Game'; | 7 | TwitchIcon, |
| 8 | import { Map } from '@customTypes/Map'; | 8 | YouTubeIcon, |
| 9 | import { API } from '@api/Api'; | 9 | PortalIcon, |
| 10 | import { ticks_to_time } from '@utils/Time'; | 10 | FlagIcon, |
| 11 | import "@css/Profile.css"; | 11 | StatisticsIcon, |
| 12 | import useMessage from '@hooks/UseMessage'; | 12 | SortIcon, |
| 13 | ThreedotIcon, | ||
| 14 | DownloadIcon, | ||
| 15 | HistoryIcon, | ||
| 16 | } from "@images/Images"; | ||
| 17 | import { UserProfile } from "@customTypes/Profile"; | ||
| 18 | import { Game, GameChapters } from "@customTypes/Game"; | ||
| 19 | import { Map } from "@customTypes/Map"; | ||
| 20 | import { API } from "@api/Api"; | ||
| 21 | import { ticks_to_time } from "@utils/Time"; | ||
| 22 | import useMessage from "@hooks/UseMessage"; | ||
| 13 | 23 | ||
| 14 | interface UserProps { | 24 | interface UserProps { |
| 15 | profile?: UserProfile; | 25 | profile?: UserProfile; |
| @@ -18,7 +28,6 @@ interface UserProps { | |||
| 18 | } | 28 | } |
| 19 | 29 | ||
| 20 | const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | 30 | const User: React.FC<UserProps> = ({ token, profile, gameData }) => { |
| 21 | |||
| 22 | const { message, MessageDialogComponent } = useMessage(); | 31 | const { message, MessageDialogComponent } = useMessage(); |
| 23 | 32 | ||
| 24 | const [user, setUser] = React.useState<UserProfile | undefined>(undefined); | 33 | const [user, setUser] = React.useState<UserProfile | undefined>(undefined); |
| @@ -29,13 +38,15 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 29 | 38 | ||
| 30 | const [game, setGame] = React.useState("0"); | 39 | const [game, setGame] = React.useState("0"); |
| 31 | const [chapter, setChapter] = React.useState("0"); | 40 | const [chapter, setChapter] = React.useState("0"); |
| 32 | const [chapterData, setChapterData] = React.useState<GameChapters | null>(null); | 41 | const [chapterData, setChapterData] = React.useState<GameChapters | null>( |
| 42 | null | ||
| 43 | ); | ||
| 33 | const [maps, setMaps] = React.useState<Map[]>([]); | 44 | const [maps, setMaps] = React.useState<Map[]>([]); |
| 34 | 45 | ||
| 35 | const location = useLocation(); | 46 | const location = useLocation(); |
| 36 | const navigate = useNavigate(); | 47 | const navigate = useNavigate(); |
| 37 | 48 | ||
| 38 | const _fetch_user = async () => { | 49 | const _fetch_user = React.useCallback(async () => { |
| 39 | const userID = location.pathname.split("/")[2]; | 50 | const userID = location.pathname.split("/")[2]; |
| 40 | if (token && profile && profile.profile && profile.steam_id === userID) { | 51 | if (token && profile && profile.profile && profile.steam_id === userID) { |
| 41 | navigate("/profile"); | 52 | navigate("/profile"); |
| @@ -43,9 +54,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 43 | } | 54 | } |
| 44 | const userData = await API.get_user(userID); | 55 | const userData = await API.get_user(userID); |
| 45 | setUser(userData); | 56 | setUser(userData); |
| 46 | }; | 57 | }, [location.pathname, token, profile, navigate]); |
| 47 | 58 | ||
| 48 | const _get_game_chapters = async () => { | 59 | const _get_game_chapters = React.useCallback(async () => { |
| 49 | if (game !== "0") { | 60 | if (game !== "0") { |
| 50 | const gameChapters = await API.get_games_chapters(game); | 61 | const gameChapters = await API.get_games_chapters(game); |
| 51 | setChapterData(gameChapters); | 62 | setChapterData(gameChapters); |
| @@ -53,9 +64,9 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 53 | setPageMax(Math.ceil(user!.records.length / 20)); | 64 | setPageMax(Math.ceil(user!.records.length / 20)); |
| 54 | setPageNumber(1); | 65 | setPageNumber(1); |
| 55 | } | 66 | } |
| 56 | }; | 67 | }, [game, user]); |
| 57 | 68 | ||
| 58 | const _get_game_maps = async () => { | 69 | const _get_game_maps = React.useCallback(async () => { |
| 59 | if (chapter === "0") { | 70 | if (chapter === "0") { |
| 60 | const gameMaps = await API.get_game_maps(game); | 71 | const gameMaps = await API.get_game_maps(game); |
| 61 | setMaps(gameMaps); | 72 | setMaps(gameMaps); |
| @@ -67,251 +78,331 @@ const User: React.FC<UserProps> = ({ token, profile, gameData }) => { | |||
| 67 | setPageMax(Math.ceil(gameChapters.maps.length / 20)); | 78 | setPageMax(Math.ceil(gameChapters.maps.length / 20)); |
| 68 | setPageNumber(1); | 79 | setPageNumber(1); |
| 69 | } | 80 | } |
| 70 | }; | 81 | }, [chapter, game]); |
| 71 | 82 | ||
| 72 | React.useEffect(() => { | 83 | React.useEffect(() => { |
| 73 | _fetch_user(); | 84 | _fetch_user(); |
| 74 | }, [location]); | 85 | }, [location, _fetch_user]); |
| 75 | 86 | ||
| 76 | React.useEffect(() => { | 87 | React.useEffect(() => { |
| 77 | if (user) { | 88 | if (user) { |
| 78 | _get_game_chapters(); | 89 | _get_game_chapters(); |
| 79 | } | 90 | } |
| 80 | }, [user, game, location]); | 91 | }, [user, game, location, _get_game_chapters]); |
| 81 | 92 | ||
| 82 | React.useEffect(() => { | 93 | React.useEffect(() => { |
| 83 | if (user && game !== "0") { | 94 | if (user && game !== "0") { |
| 84 | _get_game_maps(); | 95 | _get_game_maps(); |
| 85 | } | 96 | } |
| 86 | }, [user, game, chapter, location]) | 97 | }, [user, game, chapter, location, _get_game_maps]); |
| 87 | 98 | ||
| 88 | if (!user) { | 99 | if (!user) { |
| 89 | return ( | 100 | return ( |
| 90 | <></> | 101 | <div className="flex justify-center items-center h-[50vh] text-lg text-foreground"> |
| 102 | Loading... | ||
| 103 | </div> | ||
| 91 | ); | 104 | ); |
| 92 | }; | 105 | } |
| 93 | 106 | ||
| 94 | return ( | 107 | return ( |
| 95 | <main> | 108 | <main className="ml-20 overflow-auto overflow-x-hidden relative w-[calc(100%px)] h-screen font-[--font-barlow-semicondensed-regular] text-foreground text-xl"> |
| 96 | <Helmet> | 109 | <Helmet> |
| 97 | <title>LPHUB | {user.user_name}</title> | 110 | <title>LPHUB | {user.user_name}</title> |
| 98 | <meta name="description" content={user.user_name} /> | 111 | <meta name="description" content={user.user_name} /> |
| 99 | </Helmet> | 112 | </Helmet> |
| 113 | |||
| 100 | {MessageDialogComponent} | 114 | {MessageDialogComponent} |
| 101 | <section id='section1' className='profile'> | 115 | |
| 102 | <div> | 116 | <section className="m-5 bg-gradient-to-t from-[#202232] from-50% to-[#2b2e46] to-50% rounded-3xl p-[30px] mb-[30px] text-foreground"> |
| 103 | <img src={user.avatar_link} alt="profile-image"></img> | 117 | <div className="grid grid-cols-[200px_1fr_auto] items-center gap-[25px] mb-[25px]"> |
| 104 | </div> | 118 | <img |
| 105 | <div id='profile-top'> | 119 | src={user.avatar_link} |
| 120 | alt="Profile" | ||
| 121 | className="w-[120px] h-[120px] rounded-full border-[3px] border-[rgba(205,207,223,0.2)]" | ||
| 122 | /> | ||
| 106 | <div> | 123 | <div> |
| 107 | <div>{user.user_name}</div> | 124 | <h1 className="m-0 mb-[10px] text-[50px] font-bold text-white font-[--font-barlow-semicondensed-regular]"> |
| 108 | <div> | 125 | {user.user_name} |
| 109 | {user.country_code === "XX" ? "" : <img src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} alt={user.country_code} />} | 126 | </h1> |
| 110 | </div> | 127 | {user.country_code !== "XX" && ( |
| 111 | <div> | 128 | <div className="flex items-center gap-3 mb-[15px]"> |
| 112 | {user.titles.map(e => ( | 129 | <img |
| 113 | <span className="titles" style={{ backgroundColor: `#${e.color}` }}> | 130 | src={`https://flagcdn.com/w80/${user.country_code.toLowerCase()}.jpg`} |
| 114 | {e.name} | 131 | alt={user.country_code} |
| 132 | className="w-6 h-4 rounded-[10px]" | ||
| 133 | /> | ||
| 134 | <span>{user.country_code}</span> | ||
| 135 | </div> | ||
| 136 | )} | ||
| 137 | <div className="flex flex-wrap gap-2"> | ||
| 138 | {user.titles.map((title, index) => ( | ||
| 139 | <span | ||
| 140 | key={index} | ||
| 141 | className="py-[6px] px-5 pt-[6px] rounded-[10px] text-lg font-normal text-white" | ||
| 142 | style={{ backgroundColor: `#${title.color}` }} | ||
| 143 | > | ||
| 144 | {title.name} | ||
| 115 | </span> | 145 | </span> |
| 116 | ))} | 146 | ))} |
| 117 | </div> | 147 | </div> |
| 118 | </div> | 148 | </div> |
| 119 | <div> | 149 | <div className="flex gap-[15px] items-center pr-[10px]"> |
| 120 | {user.links.steam === "-" ? "" : <a href={user.links.steam}><img src={SteamIcon} alt="Steam" /></a>} | 150 | {user.links.steam !== "-" && ( |
| 121 | {user.links.twitch === "-" ? "" : <a href={user.links.twitch}><img src={TwitchIcon} alt="Twitch" /></a>} | 151 | <a href={user.links.steam} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5"> |
| 122 | {user.links.youtube === "-" ? "" : <a href={user.links.youtube}><img src={YouTubeIcon} alt="Youtube" /></a>} | 152 | <img src={SteamIcon} alt="Steam" className="h-[50px] px-[5px] scale-90 brightness-200" /> |
| 123 | {user.links.p2sr === "-" ? "" : <a href={user.links.p2sr}><img src={PortalIcon} alt="P2SR" style={{ padding: "0" }} /></a>} | 153 | </a> |
| 154 | )} | ||
| 155 | {user.links.twitch !== "-" && ( | ||
| 156 | <a href={user.links.twitch} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5"> | ||
| 157 | <img src={TwitchIcon} alt="Twitch" className="h-[50px] px-[5px] scale-90 brightness-200" /> | ||
| 158 | </a> | ||
| 159 | )} | ||
| 160 | {user.links.youtube !== "-" && ( | ||
| 161 | <a href={user.links.youtube} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5"> | ||
| 162 | <img src={YouTubeIcon} alt="YouTube" className="h-[50px] px-[5px] scale-90 brightness-200" /> | ||
| 163 | </a> | ||
| 164 | )} | ||
| 165 | {user.links.p2sr !== "-" && ( | ||
| 166 | <a href={user.links.p2sr} className="flex items-center justify-center transition-all duration-200 hover:-translate-y-0.5"> | ||
| 167 | <img src={PortalIcon} alt="P2SR" className="h-[50px] px-[5px] scale-90 brightness-200" /> | ||
| 168 | </a> | ||
| 169 | )} | ||
| 124 | </div> | 170 | </div> |
| 125 | |||
| 126 | </div> | 171 | </div> |
| 127 | <div id='profile-bottom'> | 172 | |
| 128 | <div> | 173 | <div className="grid grid-cols-3 gap-3 mt-24"> |
| 129 | <span>Overall</span> | 174 | <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]"> |
| 130 | <span>{user.rankings.overall.rank === 0 ? "N/A " : "#" + user.rankings.overall.rank + " "} | 175 | <div className="text-inherit text-lg">Overall</div> |
| 131 | <span>({user.rankings.overall.completion_count}/{user.rankings.overall.completion_total})</span> | 176 | <div className="text-white text-[40px]"> |
| 132 | </span> | 177 | {user.rankings.overall.rank === 0 ? "N/A" : `#${user.rankings.overall.rank}`} |
| 178 | </div> | ||
| 179 | <div className="text-white text-xl"> | ||
| 180 | {user.rankings.overall.completion_count}/{user.rankings.overall.completion_total} | ||
| 181 | </div> | ||
| 133 | </div> | 182 | </div> |
| 134 | <div> | 183 | <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]"> |
| 135 | <span>Singleplayer</span> | 184 | <div className="text-inherit text-lg">Singleplayer</div> |
| 136 | <span>{user.rankings.singleplayer.rank === 0 ? "N/A " : "#" + user.rankings.singleplayer.rank + " "} | 185 | <div className="text-white text-[40px]"> |
| 137 | <span>({user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total})</span> | 186 | {user.rankings.singleplayer.rank === 0 ? "N/A" : `#${user.rankings.singleplayer.rank}`} |
| 138 | </span> | 187 | </div> |
| 188 | <div className="text-white text-xl"> | ||
| 189 | {user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total} | ||
| 190 | </div> | ||
| 139 | </div> | 191 | </div> |
| 140 | <div> | 192 | <div className="m-3 bg-[#2b2e46] rounded-[20px] p-5 text-center grid place-items-center grid-rows-[40%_50%]"> |
| 141 | <span>Cooperative</span> | 193 | <div className="text-inherit text-lg">Cooperative</div> |
| 142 | <span>{user.rankings.cooperative.rank === 0 ? "N/A " : "#" + user.rankings.cooperative.rank + " "} | 194 | <div className="text-white text-[40px]"> |
| 143 | <span>({user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total})</span> | 195 | {user.rankings.cooperative.rank === 0 ? "N/A" : `#${user.rankings.cooperative.rank}`} |
| 144 | </span> | 196 | </div> |
| 197 | <div className="text-white text-xl"> | ||
| 198 | {user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total} | ||
| 199 | </div> | ||
| 145 | </div> | 200 | </div> |
| 146 | </div> | 201 | </div> |
| 147 | </section> | 202 | </section> |
| 148 | 203 | ||
| 149 | 204 | <section className="m-5 h-[60px] grid grid-cols-2"> | |
| 150 | <section id='section2' className='profile'> | 205 | <button |
| 151 | <button onClick={() => setNavState(0)}><img src={FlagIcon} alt="" /> Player Records</button> | 206 | className={`flex justify-center items-center gap-2 bg-[#2b2e46] border-0 text-inherit font-inherit text-2xl cursor-pointer transition-colors duration-100 rounded-l-3xl hover:bg-[#202232] ${ |
| 152 | <button onClick={() => setNavState(1)}><img src={StatisticsIcon} alt="" /> Statistics</button> | 207 | navState === 0 ? 'bg-[#202232]' : '' |
| 208 | }`} | ||
| 209 | onClick={() => setNavState(0)} | ||
| 210 | > | ||
| 211 | <img src={FlagIcon} alt="" className="w-5 h-5 scale-[1.2]" /> | ||
| 212 | Player Records | ||
| 213 | </button> | ||
| 214 | <button | ||
| 215 | className={`flex justify-center items-center gap-2 bg-[#2b2e46] border-0 text-inherit font-inherit text-2xl cursor-pointer transition-colors duration-100 rounded-r-3xl hover:bg-[#202232] ${ | ||
| 216 | navState === 1 ? 'bg-[#202232]' : '' | ||
| 217 | }`} | ||
| 218 | onClick={() => setNavState(1)} | ||
| 219 | > | ||
| 220 | <img src={StatisticsIcon} alt="" className="w-5 h-5 scale-[1.2]" /> | ||
| 221 | Statistics | ||
| 222 | </button> | ||
| 153 | </section> | 223 | </section> |
| 154 | 224 | ||
| 155 | 225 | {navState === 0 && ( | |
| 156 | 226 | <section className="m-5 block bg-[#202232] rounded-3xl overflow-hidden"> | |
| 157 | 227 | <div className="grid grid-cols-2 mx-5 my-5 mt-[10px] mb-5"> | |
| 158 | 228 | <select | |
| 159 | <section id='section3' className='profile1'> | 229 | className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px]" |
| 160 | <div id='profileboard-nav'> | 230 | value={game} |
| 161 | {gameData === null ? <select>error</select> : | 231 | onChange={(e) => { |
| 162 | 232 | setGame(e.target.value); | |
| 163 | <select id='select-game' | ||
| 164 | onChange={() => { | ||
| 165 | setGame((document.querySelector('#select-game') as HTMLInputElement).value); | ||
| 166 | setChapter("0"); | 233 | setChapter("0"); |
| 167 | const chapterSelect = document.querySelector('#select-chapter') as HTMLSelectElement; | 234 | }} |
| 168 | if (chapterSelect) { | 235 | > |
| 169 | chapterSelect.value = "0"; | 236 | <option value="0">All Games</option> |
| 170 | } | 237 | {gameData?.map((g) => ( |
| 171 | }}> | 238 | <option key={g.id} value={g.id}> |
| 172 | <option value={0} key={0}>All Scores</option> | 239 | {g.name} |
| 173 | {gameData.map((e, i) => ( | 240 | </option> |
| 174 | <option value={e.id} key={i + 1}>{e.name}</option> | 241 | ))} |
| 175 | ))}</select> | 242 | </select> |
| 176 | } | ||
| 177 | 243 | ||
| 178 | {game === "0" ? | 244 | <select |
| 179 | <select disabled> | 245 | className="h-[50px] rounded-3xl text-center text-inherit font-inherit text-2xl border-0 bg-[#2b2e46] mr-[10px] disabled:opacity-50" |
| 180 | <option>All Chapters</option> | 246 | value={chapter} |
| 247 | onChange={(e) => setChapter(e.target.value)} | ||
| 248 | disabled={game === "0"} | ||
| 249 | > | ||
| 250 | <option value="0">All Chapters</option> | ||
| 251 | {chapterData?.chapters | ||
| 252 | .filter(c => !c.is_disabled) | ||
| 253 | .map((c) => ( | ||
| 254 | <option key={c.id} value={c.id}> | ||
| 255 | {c.name} | ||
| 256 | </option> | ||
| 257 | ))} | ||
| 181 | </select> | 258 | </select> |
| 182 | : chapterData === null ? <select></select> : | 259 | </div> |
| 183 | 260 | ||
| 184 | <select id='select-chapter' | 261 | <div className="h-[34px] grid text-xl pl-[60px] mx-5 my-0 grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%]"> |
| 185 | onChange={() => setChapter((document.querySelector('#select-chapter') as HTMLInputElement).value)}> | 262 | <div className="flex place-items-end cursor-pointer"> |
| 186 | <option value="0" key="0">All Chapters</option> | 263 | <span>Map Name</span> |
| 187 | {chapterData.chapters.filter(e => e.is_disabled === false).map((e, i) => ( | 264 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 188 | <option value={e.id} key={i + 1}>{e.name}</option> | 265 | </div> |
| 189 | ))}</select> | 266 | <div className="flex place-items-end cursor-pointer"> |
| 190 | } | 267 | <span>Portals</span> |
| 191 | </div> | 268 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 192 | <div id='profileboard-top'> | 269 | </div> |
| 193 | <span><span>Map Name</span><img src={SortIcon} alt="" /></span> | 270 | <div className="flex place-items-end cursor-pointer"> |
| 194 | <span style={{ justifyContent: 'center' }}><span>Portals</span><img src={SortIcon} alt="" /></span> | 271 | <span>WRΔ</span> |
| 195 | <span style={{ justifyContent: 'center' }}><span>WRΔ </span><img src={SortIcon} alt="" /></span> | 272 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 196 | <span style={{ justifyContent: 'center' }}><span>Time</span><img src={SortIcon} alt="" /></span> | 273 | </div> |
| 197 | <span> </span> | 274 | <div className="flex place-items-end cursor-pointer"> |
| 198 | <span><span>Rank</span><img src={SortIcon} alt="" /></span> | 275 | <span>Time</span> |
| 199 | <span><span>Date</span><img src={SortIcon} alt="" /></span> | 276 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 200 | <div id='page-number'> | 277 | </div> |
| 201 | <div> | 278 | <div></div> |
| 202 | <button onClick={() => { | 279 | <div className="flex place-items-end cursor-pointer"> |
| 203 | if (pageNumber !== 1) { | 280 | <span>Rank</span> |
| 204 | setPageNumber(prevPageNumber => prevPageNumber - 1); | 281 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 205 | const records = document.querySelectorAll(".profileboard-record"); | 282 | </div> |
| 206 | records.forEach((r) => { | 283 | <div className="flex place-items-end cursor-pointer"> |
| 207 | (r as HTMLInputElement).style.height = "44px"; | 284 | <span>Date</span> |
| 208 | }); | 285 | <img src={SortIcon} alt="Sort" className="h-5 scale-[0.8]" /> |
| 209 | } | 286 | </div> |
| 210 | }} | 287 | <div className="flex items-center gap-[10px] justify-center"> |
| 211 | ><i className='triangle' style={{ position: 'relative', left: '-5px', }}></i> </button> | 288 | <button |
| 212 | <span>{pageNumber}/{pageMax}</span> | 289 | className="w-8 h-8 border border-[#2b2e46] bg-[#2b2e46] rounded cursor-pointer flex items-center justify-center text-foreground transition-colors duration-100 hover:bg-[#202232] disabled:opacity-50 disabled:cursor-not-allowed" |
| 213 | <button onClick={() => { | 290 | onClick={() => setPageNumber(Math.max(1, pageNumber - 1))} |
| 214 | if (pageNumber !== pageMax) { | 291 | disabled={pageNumber === 1} |
| 215 | setPageNumber(prevPageNumber => prevPageNumber + 1); | 292 | > |
| 216 | const records = document.querySelectorAll(".profileboard-record"); | 293 | ← |
| 217 | records.forEach((r) => { | 294 | </button> |
| 218 | (r as HTMLInputElement).style.height = "44px"; | 295 | <span className="text-sm text-foreground">{pageNumber}/{pageMax}</span> |
| 219 | }); | 296 | <button |
| 220 | } | 297 | className="w-8 h-8 border border-[#2b2e46] bg-[#2b2e46] rounded cursor-pointer flex items-center justify-center text-foreground transition-colors duration-100 hover:bg-[#202232] disabled:opacity-50 disabled:cursor-not-allowed" |
| 221 | }} | 298 | onClick={() => setPageNumber(Math.min(pageMax, pageNumber + 1))} |
| 222 | ><i className='triangle' style={{ position: 'relative', left: '5px', transform: 'rotate(180deg)' }}></i> </button> | 299 | disabled={pageNumber === pageMax} |
| 300 | > | ||
| 301 | → | ||
| 302 | </button> | ||
| 223 | </div> | 303 | </div> |
| 224 | </div> | 304 | </div> |
| 225 | </div> | ||
| 226 | <hr /> | ||
| 227 | <div id='profileboard-records'> | ||
| 228 | |||
| 229 | {game === "0" | ||
| 230 | ? ( | ||
| 231 | |||
| 232 | user.records.sort((a, b) => a.map_id - b.map_id) | ||
| 233 | .map((r, index) => ( | ||
| 234 | 305 | ||
| 306 | <div> | ||
| 307 | {game === "0" ? ( | ||
| 308 | user.records | ||
| 309 | .sort((a, b) => a.map_id - b.map_id) | ||
| 310 | .map((record, index) => | ||
| 235 | Math.ceil((index + 1) / 20) === pageNumber ? ( | 311 | Math.ceil((index + 1) / 20) === pageNumber ? ( |
| 236 | <button className="profileboard-record" key={index}> | 312 | <div key={index} className="w-[calc(100%-40px)] mx-5 my-0 mt-[10px] h-11 rounded-[20px] pl-[40px] text-xl text-inherit font-inherit border-0 transition-colors duration-100 bg-[#2b2e46] grid grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%] overflow-hidden whitespace-nowrap cursor-pointer hover:bg-[#202232]"> |
| 237 | {r.scores.map((e, i) => (<> | 313 | <Link to={`/maps/${record.map_id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline"> |
| 238 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 314 | {record.map_name} |
| 239 | 315 | </Link> | |
| 240 | <Link to={`/maps/${r.map_id}`}><span>{r.map_name}</span></Link> | 316 | <span className="flex place-items-center h-11">{record.scores[0]?.score_count || 'N/A'}</span> |
| 241 | 317 | <span className={`flex place-items-center h-11 ${record.scores[0]?.score_count - record.map_wr_count > 0 ? 'text-[#dc3545]' : ''}`}> | |
| 242 | <span style={{ display: "grid" }}>{e.score_count}</span> | 318 | {record.scores[0]?.score_count - record.map_wr_count > 0 |
| 243 | 319 | ? `+${record.scores[0].score_count - record.map_wr_count}` | |
| 244 | <span style={{ display: "grid" }}>{e.score_count - r.map_wr_count > 0 ? `+${e.score_count - r.map_wr_count}` : `-`}</span> | 320 | : '–'} |
| 245 | <span style={{ display: "grid" }}>{ticks_to_time(e.score_time)}</span> | 321 | </span> |
| 246 | <span> </span> | 322 | <span className="flex place-items-center h-11">{record.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span> |
| 247 | {i === 0 ? <span>#{r.placement}</span> : <span> </span>} | 323 | <span className="flex place-items-center h-11"></span> |
| 248 | <span>{e.date.split("T")[0]}</span> | 324 | <span className="flex place-items-center h-11 font-semibold">#{record.placement}</span> |
| 249 | <span style={{ flexDirection: "row-reverse" }}> | 325 | <span className="flex place-items-center h-11">{record.scores[0]?.date.split("T")[0] || 'N/A'}</span> |
| 250 | 326 | <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11"> | |
| 251 | <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 327 | <button |
| 252 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 328 | className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" |
| 253 | {i === 0 && r.scores.length > 1 ? <button onClick={() => { | 329 | onClick={() => message("Demo Information", `Demo ID: ${record.scores[0]?.demo_id}`)} |
| 254 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | 330 | title="Demo Info" |
| 255 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | 331 | > |
| 256 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${r.scores.length * 46}px` : | 332 | <img src={ThreedotIcon} alt="Info" className="w-4 h-4" /> |
| 257 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | 333 | </button> |
| 258 | } | 334 | <button |
| 259 | }><img src={HistoryIcon} alt="history" /></button> : ""} | 335 | className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" |
| 260 | 336 | onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0]?.demo_id}`} | |
| 261 | </span> | 337 | title="Download Demo" |
| 262 | </>))} | 338 | > |
| 263 | 339 | <img src={DownloadIcon} alt="Download" className="w-4 h-4" /> | |
| 264 | </button> | 340 | </button> |
| 265 | ) : "" | 341 | {record.scores.length > 1 && ( |
| 266 | ))) : maps ? | 342 | <button className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" title="View History"> |
| 267 | 343 | <img src={HistoryIcon} alt="History" className="w-4 h-4" /> | |
| 268 | maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) | 344 | </button> |
| 269 | .map((r, index) => { | 345 | )} |
| 270 | if (Math.ceil((index + 1) / 20) === pageNumber) { | 346 | </div> |
| 271 | let record = user.records.find((e) => e.map_id === r.id); | 347 | </div> |
| 272 | return record === undefined ? ( | 348 | ) : null |
| 273 | <button className="profileboard-record" key={index} style={{ backgroundColor: "#1b1b20" }}> | 349 | ) |
| 274 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 350 | ) : ( |
| 275 | <span style={{ display: "grid" }}>N/A</span> | 351 | maps |
| 276 | <span style={{ display: "grid" }}>N/A</span> | 352 | ?.filter(map => !map.is_disabled) |
| 277 | <span>N/A</span> | 353 | .sort((a, b) => a.id - b.id) |
| 278 | <span> </span> | 354 | .map((map, index) => { |
| 279 | <span>N/A</span> | 355 | if (Math.ceil((index + 1) / 20) !== pageNumber) return null; |
| 280 | <span>N/A</span> | 356 | |
| 281 | <span style={{ flexDirection: "row-reverse" }}></span> | 357 | const record = user.records.find(r => r.map_id === map.id); |
| 282 | </button> | 358 | |
| 283 | ) : ( | 359 | return ( |
| 284 | <button className="profileboard-record" key={index}> | 360 | <div key={index} className={`w-[calc(100%-40px)] mx-5 my-0 mt-[10px] h-11 rounded-[20px] pl-[40px] text-xl text-inherit font-inherit border-0 transition-colors duration-100 bg-[#2b2e46] grid grid-cols-[15%_15%_5%_15%_5%_15%_15%_15%] overflow-hidden whitespace-nowrap cursor-pointer hover:bg-[#202232] ${!record ? 'opacity-65' : ''}`}> |
| 285 | {record.scores.map((e, i) => (<> | 361 | <Link to={`/maps/${map.id}`} className="text-[#3c91e6] no-underline font-inherit flex place-items-center h-11 hover:underline"> |
| 286 | {i !== 0 ? <hr style={{ gridColumn: "1 / span 8" }} /> : ""} | 362 | {map.name} |
| 287 | <Link to={`/maps/${r.id}`}><span>{r.name}</span></Link> | 363 | </Link> |
| 288 | <span style={{ display: "grid" }}>{record!.scores[i].score_count}</span> | 364 | <span className="flex place-items-center h-11">{record?.scores[0]?.score_count || 'N/A'}</span> |
| 289 | <span style={{ display: "grid" }}>{record!.scores[i].score_count - record!.map_wr_count > 0 ? `+${record!.scores[i].score_count - record!.map_wr_count}` : `-`}</span> | 365 | <span className={`flex place-items-center h-11 ${record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0 ? 'text-[#dc3545]' : ''}`}> |
| 290 | <span style={{ display: "grid" }}>{ticks_to_time(record!.scores[i].score_time)}</span> | 366 | {record?.scores[0]?.score_count && record.scores[0].score_count - record.map_wr_count > 0 |
| 291 | <span> </span> | 367 | ? `+${record.scores[0].score_count - record.map_wr_count}` |
| 292 | {i === 0 ? <span>#{record!.placement}</span> : <span> </span>} | 368 | : '–'} |
| 293 | <span>{record!.scores[i].date.split("T")[0]}</span> | 369 | </span> |
| 294 | <span style={{ flexDirection: "row-reverse" }}> | 370 | <span className="flex place-items-center h-11">{record?.scores[0] ? ticks_to_time(record.scores[0].score_time) : 'N/A'}</span> |
| 295 | 371 | <span className="flex place-items-center h-11"></span> | |
| 296 | <button onClick={() => { message("Demo Information", `Demo ID: ${e.demo_id}`) }}><img src={ThreedotIcon} alt="demo_id" /></button> | 372 | <span className="flex place-items-center h-11 font-semibold">{record ? `#${record.placement}` : 'N/A'}</span> |
| 297 | <button onClick={() => window.location.href = `/api/v1/demos?uuid=${e.demo_id}`}><img src={DownloadIcon} alt="download" /></button> | 373 | <span className="flex place-items-center h-11">{record?.scores[0]?.date.split("T")[0] || 'N/A'}</span> |
| 298 | {i === 0 && record!.scores.length > 1 ? <button onClick={() => { | 374 | <div className="flex gap-[5px] justify-end flex-row-reverse place-items-center h-11"> |
| 299 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "44px" || | 375 | {record?.scores[0] && ( |
| 300 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height === "" ? | 376 | <> |
| 301 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = `${record!.scores.length * 46}px` : | 377 | <button |
| 302 | (document.querySelectorAll(".profileboard-record")[index % 20] as HTMLInputElement).style.height = "44px" | 378 | className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" |
| 303 | } | 379 | onClick={() => message("Demo Information", `Demo ID: ${record.scores[0].demo_id}`)} |
| 304 | }><img src={HistoryIcon} alt="history" /></button> : ""} | 380 | title="Demo Info" |
| 305 | 381 | > | |
| 306 | </span> | 382 | <img src={ThreedotIcon} alt="Info" className="w-4 h-4" /> |
| 307 | </>))} | 383 | </button> |
| 308 | </button> | 384 | <button |
| 309 | 385 | className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" | |
| 310 | ) | 386 | onClick={() => window.location.href = `/api/v1/demos?uuid=${record.scores[0].demo_id}`} |
| 311 | } else { return null } | 387 | title="Download Demo" |
| 312 | }) : (<>{console.warn(maps)}</>)} | 388 | > |
| 313 | </div> | 389 | <img src={DownloadIcon} alt="Download" className="w-4 h-4" /> |
| 314 | </section> | 390 | </button> |
| 391 | {record.scores.length > 1 && ( | ||
| 392 | <button className="bg-transparent border-0 cursor-pointer transition-colors duration-100 p-0.5 hover:bg-[rgba(32,34,50,0.5)]" title="View History"> | ||
| 393 | <img src={HistoryIcon} alt="History" className="w-4 h-4" /> | ||
| 394 | </button> | ||
| 395 | )} | ||
| 396 | </> | ||
| 397 | )} | ||
| 398 | </div> | ||
| 399 | </div> | ||
| 400 | ); | ||
| 401 | }) | ||
| 402 | )} | ||
| 403 | </div> | ||
| 404 | </section> | ||
| 405 | )} | ||
| 315 | </main> | 406 | </main> |
| 316 | ); | 407 | ); |
| 317 | }; | 408 | }; |