From b487a756b25e493084011fe63f79e21aafc3a91c Mon Sep 17 00:00:00 2001 From: May Date: Wed, 4 Mar 2026 13:41:36 -0800 Subject: [PATCH] feat: :sparkles: Infinite scrolling on rankings page (#209) --- AquaNet/bun.lockb | Bin 226531 -> 231211 bytes AquaNet/src/libs/i18n/en_ref.ts | 1 + AquaNet/src/libs/i18n/zh.ts | 1 + AquaNet/src/libs/sdk.ts | 4 +- AquaNet/src/pages/Ranking.svelte | 59 +++++----------- AquaNet/src/pages/Ranking/Cap.svelte | 96 +++++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 44 deletions(-) create mode 100644 AquaNet/src/pages/Ranking/Cap.svelte diff --git a/AquaNet/bun.lockb b/AquaNet/bun.lockb index 8cd8a7e868e6f571d5026e22da97e7b1a919aefc..a2087b1f2fca07b3e6e08f9d1f4676b625380554 100755 GIT binary patch delta 8008 zcmZu$TWpla70&)%5??k25?6JqD5VORF?O)mT)Z?(AT>7wB?i(a4G!4=j>m}sFE$v6 zxf63qxDK!c3~UktV=lpefNU!vMUkqi4|!}j3p-} zSpPtSep$}=q}Z6p;1gl-#ug?f#VRCvPCi(=yB-M38L5r2J2Nc}txZ_^+HsM`SXjFd zo`u?0#j8y5qLYzxz~Yly7@r)=^#3(7T`?4T2qzMPi$>L=VJ9w1Neoz~Nh2&3RaBgS z7`0lAWCeRbs-Bc2W%2=^XV^a{PG%E$1*@y8ayCWlP>?bf9soh=Vz^M#safnnT}yE% zWC~7uPRUEBj6;PDPD!xuFWb$FDImqL#iZUUgVr0O`gMwLegMR*AB&tG)hD$L8WL6) zyg}34YtX}w8jx73p1xqzcCdntyijH7#0y5fVRH*3Q>7W=Q(M{eMk^biYN&ygZ~6tQU!kHK}7$&4e*|DWnO97F_Taf9qEZzeGwxHqwfyzmu zI0lG%hUz>@vs`CG*K#e?Be2+`;*|zAWVB650~XORy-x+OKNQe z>u$Ar$j^l==3trPL@V0oWmPrEJQq<9aSsU(k@=pL=VM7xxr&+aDVeunGS9c^jQ6O`sY;7$RC(LIIHkUo85bLJ*+`Hjh|s8X zNsN7ODt8)$U4nr;2J42N`!qN}g4Hd>v2rZKOKnZzOOfZ#kjg{kH|`kq5ciPq5LxEY zd5C*Rc!<2>(Rqk_NO*{}8@li+B(!$iEAEIF$5b`-5Ls^M_Q%^$_=v@DTZ(N9Q5#A>kqNnn&j$?jhkJ^14UoA?_jJA@X~V&O_Wo!b9W@ZHCdA zV1_r438)=9KCaOdEQBsbXcQ!hP+iIgB??iFsE=2 ziMO=iD!-+WEQaS0jWn5ugonu6NQX12hq#A?Mp5j-)*#&&Wf;1~*sK|vw>5IB;Tqg3 zFy0LrZY>6hUS_RXG+duxBi@+MW4e)9XAMQyAr)9(CTopwp3n&V1kO98(+H zsv7AhWQPpLJ$?A2E)VV@q0v*AsJe0H6KLyhb!hCiTjF}WLfJ-it2MjP9Jo#TZb^1~ z*KAv*;dhNSAp*&JID`>M-?N1ISy0v9L%T+lD8kOof)#qkWp3PzlxIf2jYL_p$3MdG z9_0HtQo>wyR)5rE4qGzd>3W=mhi1Os4{-s`g9m*$uWzuwss5eaFt<1^Hn_pE(E)U$ zrQ{n;L~b-EH&q#WQ4H@Kz z=nkd19jG~-DR&_Es7gp=r9`tGm=;?LSS> z7V*PMzQf8@4jZ*Axjw|zFhy~VaBcCSAD!eQ>z>l7sDrZ~RjYGt4ND&fV}67css`i! z2(E#M9+9%7kElDTBl`YH3Q3N7(=rqpvxE*n2C zpXuW^Ym4J1)tspD1L*|l-?@_JfZfs|PAIk~O2Y4?BZl)^oHRM=B*t-yVYSQ-f@zud z_@6wkxw;|H zIq5V6@Tjx>P-{1or_FSTxlEd$hel;_$i)vr3=@6U{nC+z8j#-!G4rKuNF*2WWswV{ zUUJ)|yNyF|NQPTf?hc9DCPm*)l*@u0fj>wO4NP9$ci*X!vfe>i)%EHf6{&a7H}W?i z6W@hw=Uj@>ySUg`TA)SCPeso|>=%dx!t5 z#P7;v=1+5irrg6h!MBmhnpg4d!H(RYUk?5iiTpD2d24XLG7ta!d>$5=U6vV|7hK6S z%?}=Bnid4>g7ZP{`&y?d`z? zoS5lf9t=n23c2bPL1Ii)3Xy4lEtpUvf4!yuB>VgDCvooJYi60;@z;a>k;<(-DEiQM zx#!*p5}lL({vEzZ{0+Y=lYO1*w{&irI)7__=cWyB_pI#S)V-lAv;FI!R<%Hn<+5J~ hsYoT&1p9_^mEQzyx69WqduQR66@AMxg>NEf{|^hO{2k291Nl+yqtiq_+zU=?fw|6cB``M{)j)U3V*iZkA0l(bNk*-`D>?Y zD(CAyefsq2^Xhy4tvi4Caa8<%SvlM~PFcR?aA2Zx!FMr|8Bf9!>_nfSk&X#oxrzLJ zd@CC%Oi*pIBuNUny;~=aiB8+SfJT#BlctlvL{+w#oT-T(lAY(UH%O9SlA;ad_QVCm zzC$PBNvaJ7K?GftpQOqP`O_pHDUY8*{4LGGT)aiKH3H`-#yQ-g%6FRL-IG`a*de%y8@iQVsECo93oy`acU;%cR*)=98cmG59% zL?3mKfmWyNU?{NKL2kFsVyiq;kT*I7r4$XUP1Q)tR4+RewPYG_eyX3z)u$nEWSVNu z7X;cqU9~Bodm~zPx<*3Nyao9&m(nHMhquUm+jpJ>o z>~I^uL~0oK3{{rd!9z1pvFr?u6lb8a{+Vdm2Bu655Lj12R04zsme1;BY`arXGT5$> zNIS}?0&Rc5Plmy$(FZh=dLYD%r#*=Kbw7yrd z7M`a7qfQt_Ke2ld;Y9}kQH?dWBITi4>7)$5H4il^z3}jCFx+e}+m)Rx@sKM2hD~*t zRNSP7P0IZ{&KEMNtVz{O%J1cTF_S8plxq&1ojS*7?3rPhDpoZq&%<0o)TD|grTO@5 zw@IaaXw)`#@CuA8I6yZ0B44qlu%Q;N9YkzShfz; z>X83Y&KZ4F(1w)EaVpRC80VrWScfuosLD|y;UKdYa82p5=mIXJu%KC^X?wl8$rY#;@Fys(g_+QH6t;^-8|1D&eCmFh$;pD@<4 zNMmsrUL+>#{36x%YmkoK|F{512cWSq{{v~o~1#OUfOJA8=AGMOGP_+0;)X1OpsAPHWt3cfNC29 zJD}~gE>*U(@$N3%E8V5q?tyu*@s~RJvCApn!{+s6eEjx$u>CSRerTD`n6)og4Ju$# zDU&LjRLc{bFKkkIP9;|G$qq*Ktl;8@%^CMfQH>2Nxq8`^d~MC7{7>?+m`RmP%GJ$h zgC>>f=INUm6c8=8Xe!StRc^Elx=OsL;wm(rwpz8_4J&L!_UmNTYNvsyw6r-#QKb&) zzvt`dT%-;a>X7{@kuy+-Qgx_oB6^XNdz|v!WKNGy&?w&{$V&V{0HmW1#p}>;9dfS` z*M;g(whq#Vk*d!5)RrPqmW_LaQE;t;(79bc>dEhXv}(yJkAHFlaBSMZefdqGRy`BdS(^=(H)1^ ztB^^5S}$hOP{@)yon-4!%|zgu+G0uS`o*5H4b;PFl`U<+yoKg>ZN!GbxQva^Fl_X~ z=GQlhn|q!WH;+C`)!T8joqYE!ZGxy&tsnYd-9c9MJK1$&l1umVzO&MgM#2iLKgUy6 z1Sq>1c?CeDOk{r^V^k&=J9Q1t)NLG#S_`|U6tJ+ymmK~~*Op1rieH?(lZBf7uZcE!svVrbU(5%+(bNu7S_7OVTL;Coy@{eo;grKzErJnFU1G;kDLY zROQj;>HM2in*-+1^07_e>hN4BZQ}Rj+AQXzARx4KX0sSfRY0_;pcKy*aZPj!&f)Gw z13B3lw5`ywZ^b4xy%l```6@6&-CN%#N{#@so-u`OP*%5vm~8?tVHl*0-||bI#7hn` zIpQ?i{cr>wzC6`IE=Sl?BC%Z~uI<=_;=b|iqF)ZfJk`SeGHQYT6nYs0IQy~~wlzT3 z@*J9q}sc7h>y z;%s`SXz>kl8Yk(|!`SzRMzBMB-bSa7zKt@CaDH37C@l_N7{~W# zI?l~i0Sen1_XP)OJB*L`$YC)>73z>ZA7moI zv{dSlew@#BrJ!*>4&eyT?ZR;g?BiZThrkKWLk*QW!JG2(2@i3f6ca-DBpB}``r;|R zXl?;Y(J2%hKLuWS%1`pg+Q@yU@is%JtuV`;20uLQ6+TP(&+ubutq!vP3re~4hLHOX4Zej_yn)VR=UCeyZcQKTU#;^7snB_gg%iiOup#r0x;c4uF}YHIutii>HTIw zfcpb+O{flKO_UyRke@$r8tY0AIn=|Fv=W*H+HP3d#4=o1^a^94U5Uj4>&VyvE`>U4 zaDZBBaIl>W450n115i;u^vcti>qS2lt48rde6ech(NNvzRk_tPO!_>0SUc~P$C)Gg zN5~iX2&JH#7Cy4>!ak_lMzXvDJ(8M~q$NfJgpbD5AjHT(8=Y9hJ0p0-x0A^ioNOKD zxbOur-t!kwzWb!ZegYRZlF=V80RL~(V892G_y@@b_M-p(<0g`M9140?Bc3c~LG0nO zFjCB7-t}JuzqyEkR|2|nk$<%cexkvLSsFr_PtbL$@N2(hd}q6ar^gCfykwnT`xJck zQ%nJApz}b7o`?44GjP1maMuFR!$5lu>Ll}5-(+GXrQMvxb`>SXSfStNQzeC(I5;FY?rg1Qg3 zk>0Bq>Y1xz_foyeLz_C8=b9xydX4X2yvDk6XGW4d#*MJ1EO6b$Drfq-*eg}81NUnV z=Kylk?}%m#7x@BA0SUhF1wWB}NP{nU^g;qdV6q`Ea~7#zvkyIq8`dE@ZivT=*P-DX7$feR=#%L4p_?8Oxb7eqZ=$(sH|d*{enxipIJ7}Y z3V;b{H%<#T7SY(3{4}L6t*u>OA?-fkP(QXl#9?_zf7NWf1FHrbGym7v=;LRj=+`Kq zbi_fv`?`swuC-}dNty%u(?mh{A4h0nU-gYsZZ~a_{s!**8*C-@Z+Ynsf7@(pf)yyb z@GUyJ@$t8a)-yi-7U{MSVndC4QpH@VC_lFUS5+U~Qj)IC#ov+M&70OuOIEuSJvV-t zQj|wljG6Av+`pD9|B#b^U7?hd#g)oArQDF*_@we5c}}vgTUnngbt`*i`9!j7wX(-{ z9~@&`?$&B0Cy$JEqVVR`Nf0qHmw!rmM49s6|Kl&$vPNl?8|jLq9#o!UR4yJ=QpVtv zwMqa7qiYqnF?epBauNq0d0O$pIl)h9A{+5 zYSew@P;N=T;^`Zo9>M|XhG?-3&u`w++uuKJbKi5@`Zgs8Z^1E#Zz)0f9^K$uw1s9o d_;%sl>Ur4RmJIeQV{@TDE8{LDC;vr`{|_`JMZ^FA diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts index e7548aba..84de145e 100644 --- a/AquaNet/src/libs/i18n/en_ref.ts +++ b/AquaNet/src/libs/i18n/en_ref.ts @@ -69,6 +69,7 @@ export const EN_REF_LEADERBOARD = { 'Leaderboard.Accuracy': 'Accuracy', 'Leaderboard.FC': 'FC', 'Leaderboard.AP': 'AP', + 'Leaderboard.Loading': "Loading..." } export const EN_REF_GENERAL = { diff --git a/AquaNet/src/libs/i18n/zh.ts b/AquaNet/src/libs/i18n/zh.ts index e1abb124..0b819651 100644 --- a/AquaNet/src/libs/i18n/zh.ts +++ b/AquaNet/src/libs/i18n/zh.ts @@ -81,6 +81,7 @@ const zhLeaderboard: typeof EN_REF_LEADERBOARD = { 'Leaderboard.Accuracy': '准确率', 'Leaderboard.FC': 'FC', 'Leaderboard.AP': 'AP', + 'Leaderboard.Loading': '请等一下。' } const zhGeneral: typeof EN_REF_GENERAL = { diff --git a/AquaNet/src/libs/sdk.ts b/AquaNet/src/libs/sdk.ts index c6e36a72..ccd8317e 100644 --- a/AquaNet/src/libs/sdk.ts +++ b/AquaNet/src/libs/sdk.ts @@ -227,8 +227,8 @@ export const GAME = { post(`/api/v2/game/mai2/my-photo`, { }), userSummary: (username: string, game: GameName): Promise => post(`/api/v2/game/${game}/user-summary`, { username }), - ranking: (game: GameName): Promise => - post(`/api/v2/game/${game}/ranking`, { }), + ranking: (game: GameName, page?: number): Promise => + post(`/api/v2/game/${game}/ranking`, typeof page === "number" ? { page } : {}), changeName: (game: GameName, newName: string): Promise<{ newName: string }> => post(`/api/v2/game/${game}/change-name`, { newName }), export: (game: GameName): Promise> => diff --git a/AquaNet/src/pages/Ranking.svelte b/AquaNet/src/pages/Ranking.svelte index 80f246b2..11432abc 100644 --- a/AquaNet/src/pages/Ranking.svelte +++ b/AquaNet/src/pages/Ranking.svelte @@ -9,53 +9,32 @@ import { t } from "../libs/i18n"; import UserCard from "../components/UserCard.svelte"; import Tooltip from "../components/Tooltip.svelte"; - import Pagination from "../components/Pagination.svelte"; + import Cap from "./Ranking/Cap.svelte"; export let game: GameName = 'mai2'; title(`Ranking`); - let d: { users: GenericRanking[] }; + let loadedPages: GenericRanking[][]; let error: string | null; - let page = 1 - const perPage = 50 - let totalPages = 1 - - function handleUpdatePage(event: CustomEvent) { - page = event.detail; - const url = new URL(window.location.toString()) - url.searchParams.set('page', page.toString()) - history.pushState({}, '', url.toString()) - window.scrollTo(0, 0) - } + let earliestPage = 0 + //const perPage = 50 onMount(() => { const url = new URL(window.location.toString()) const pageParam = url.searchParams.get('page') - if (pageParam) { - page = parseInt(pageParam, 10) || 1 - } - - window.addEventListener('popstate', () => { - const url = new URL(window.location.toString()) - const pageParam = url.searchParams.get('page') - page = parseInt(pageParam, 10) || 1 - window.scrollTo(0, 0) - }) + if (pageParam) + earliestPage = parseInt(pageParam, 10) || 0 + Promise.all([GAME.ranking(game, earliestPage)]) + .then(([users]) => { + loadedPages = [ users ] + }) + .catch((e) => error = e.message); }) - Promise.all([GAME.ranking(game)]) - .then(([users]) => { - d = { users } - totalPages = Math.ceil(users.length / perPage) - }) - .catch((e) => error = e.message); - let hoveringUser = ""; let hoverLoading = false; - - $: paginatedUsers = d ? d.users.slice((page - 1) * perPage, page * perPage) : []
@@ -68,13 +47,9 @@ - {#if d} - {#if page > 1} - - {/if} - + {#if loadedPages}
-
hoveringUser = paginatedUsers[0]?.username} role="heading" aria-level="2"> +
hoveringUser = ""} role="heading" aria-level="2"> {t("Leaderboard.Rank")} {t("Leaderboard.Rating")} @@ -82,7 +57,8 @@ {t("Leaderboard.FC")} {t("Leaderboard.AP")}
- {#each paginatedUsers as user, i (user.rank)} + + {#each loadedPages.flat() as user, i (user.rank)}
hoveringUser = user.username} on:focus={() => {}}> @@ -104,16 +80,15 @@ {user.allPerfect}
{/each} +
- - hoverLoading = l} /> {/if} - +
\ No newline at end of file