@@ -25,6 +25,9 @@ import {
2525 addPin ,
2626 clearAlias ,
2727 clearExempt ,
28+ countNames ,
29+ countTldsForUser ,
30+ countTldsNotOwnedBy ,
2831 DEFAULT_TLD_PRICE_USD ,
2932 getName ,
3033 getTld ,
@@ -774,6 +777,8 @@ const PIT_CSS = `
774777 font-size:.8rem;line-height:1.55;resize:vertical;min-height:9em}
775778.pit-bulk textarea:focus{outline:none;border-color:var(--acid)}
776779.pit-tabs{display:flex;gap:4px;margin:22px 0 26px;border-bottom:1px solid var(--line)}
780+ .pit-pager{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin:22px 0 4px;font-size:.72rem}
781+ .pit-pager .btn[aria-disabled]{opacity:.4;pointer-events:none}
777782.pit-tab{font-family:var(--mono);font-size:.76rem;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);
778783 padding:11px 15px;border-bottom:2px solid transparent;margin-bottom:-1px}
779784.pit-tab:hover{color:var(--text)}
@@ -814,12 +819,67 @@ const pitTabs = (active, counts = null) => `
814819
815820const forSale = ( t ) => t . price_usd !== null && t . price_usd !== undefined ;
816821
822+ /**
823+ * How much of the namespace one page may draw.
824+ *
825+ * /pit ships no script at all, and it still locked browsers up: the page grew
826+ * as endings x names-under-them, and neither end was bounded. Every name is a
827+ * form with a CSRF field, two inputs and two buttons, so an account holding 50
828+ * endings with 100 names each rendered 3 MiB of HTML and 36k elements. Nothing
829+ * has to be slow for that to jam -- it is the DOM, and the sticky blurred app
830+ * bar repainting over it on every scroll frame.
831+ *
832+ * So the page shows a window and says what it is not showing. `?tld=` opens one
833+ * ending in full, which is also where the "show all N" links go -- and where
834+ * TronBrowser's `mosh.<tld>` already pointed, on a page that until now ignored
835+ * the parameter and drew everything anyway.
836+ */
837+ const TLDS_PER_PAGE = 20 ;
838+ const NAMES_PER_TLD = 10 ;
839+ const NAMES_FOCUSED = 250 ;
840+
841+ /** `?page=` as a 1-based page number; anything unreadable is page 1. */
842+ const pageParam = ( value ) => {
843+ const n = Number . parseInt ( value , 10 ) ;
844+ return Number . isFinite ( n ) && n > 1 ? n : 1 ;
845+ } ;
846+
847+ /** Prev/next for a window into `total` rows, or nothing when it all fits. */
848+ const pager = ( { page, total, perPage, href } ) => {
849+ const pages = Math . max ( 1 , Math . ceil ( total / perPage ) ) ;
850+ if ( pages <= 1 ) return "" ;
851+ const link = ( n , label ) => `<a class="btn" href="${ href ( n ) } ">${ label } </a>` ;
852+ return `<nav class="pit-pager">
853+ ${ page > 1 ? link ( page - 1 , "← Newer" ) : `<span class="btn faint" aria-disabled="true">← Newer</span>` }
854+ <span class="mono faint">page ${ page } of ${ pages } · ${ total } endings</span>
855+ ${ page < pages ? link ( page + 1 , "Older →" ) : `<span class="btn faint" aria-disabled="true">Older →</span>` }
856+ </nav>` ;
857+ } ;
858+
817859moshpitRouter . get ( "/pit" , async ( req , res ) => {
818- const [ theirs , mine , bal ] = await Promise . all ( [
819- listTldsNotOwnedBy ( req . user ?. id ?? null , { limit : 100 } ) ,
820- req . user ? listTldsForUser ( req . user . id ) : [ ] ,
860+ // An unknown ?tab= falls back to Yours rather than rendering an empty page.
861+ const tab = req . query . tab === "theirs" ? "theirs" : "yours" ;
862+ const pageNo = pageParam ( req . query . page ) ;
863+
864+ // `?tld=` opens a single ending in full. Only meaningful for one you hold --
865+ // Theirs is one row per ending and has nothing to expand.
866+ const wanted = normalizeTld ( req . query . tld ) || null ;
867+ const focused = tab === "yours" && req . user && wanted
868+ ? await getTld ( wanted ) . then ( ( t ) => ( t && t . user_id === req . user . id ? t : null ) )
869+ : null ;
870+
871+ const [ theirs , theirsTotal , mine , mineTotal , bal ] = await Promise . all ( [
872+ tab === "theirs"
873+ ? listTldsNotOwnedBy ( req . user ?. id ?? null , { limit : TLDS_PER_PAGE , offset : ( pageNo - 1 ) * TLDS_PER_PAGE } )
874+ : [ ] ,
875+ countTldsNotOwnedBy ( req . user ?. id ?? null ) ,
876+ req . user && ! focused
877+ ? listTldsForUser ( req . user . id , { limit : TLDS_PER_PAGE , offset : ( pageNo - 1 ) * TLDS_PER_PAGE } )
878+ : [ ] ,
879+ req . user ? countTldsForUser ( req . user . id ) : 0 ,
821880 req . user ? balance ( req . user . id ) : 0 ,
822881 ] ) ;
882+ const shown = focused ? [ focused ] : mine ;
823883
824884 // `?name=mosh.whatever` — somebody typed a Moshpit name and ended up here
825885 // instead of at a site. Work out what they can actually do about it.
@@ -839,18 +899,22 @@ moshpitRouter.get("/pit", async (req, res) => {
839899 } ) ;
840900 }
841901
842- // An unknown ?tab= falls back to Yours rather than rendering an empty page.
843- const tab = req . query . tab === "theirs" ? "theirs" : "yours" ;
844- const forSaleCount = theirs . filter ( forSale ) . length ;
902+ const forSaleCount = await countTldsNotOwnedBy ( req . user ?. id ?? null , { forSale : true } ) ;
845903
846- // Per-TLD detail is only needed by the panel actually on screen, and only
847- // Yours has any: Theirs is one row per ending.
904+ // Per-TLD detail is only needed by the endings actually on screen, and only
905+ // Yours has any: Theirs is one row per ending. That is the whole fix -- this
906+ // used to run one listNames per ending the account held, however many that
907+ // was, and then render every row it got back.
848908 const exemptions = new Map ( ) ;
849909 const names = new Map ( ) ;
910+ const nameTotals = new Map ( ) ;
850911 if ( tab === "yours" ) {
851912 // Exemptions are only meaningful for a TLD that points somewhere.
852- await Promise . all ( mine . filter ( ( t ) => t . alias_of ) . map ( async ( t ) => exemptions . set ( t . tld , await listExempt ( t . tld ) ) ) ) ;
853- await Promise . all ( mine . map ( async ( t ) => names . set ( t . tld , await listNames ( t . tld ) ) ) ) ;
913+ await Promise . all ( shown . filter ( ( t ) => t . alias_of ) . map ( async ( t ) => exemptions . set ( t . tld , await listExempt ( t . tld ) ) ) ) ;
914+ await Promise . all ( shown . map ( async ( t ) => {
915+ names . set ( t . tld , await listNames ( t . tld , focused ? NAMES_FOCUSED : NAMES_PER_TLD ) ) ;
916+ nameTotals . set ( t . tld , await countNames ( t . tld ) ) ;
917+ } ) ) ;
854918 }
855919
856920 const msg = req . query . err ? `<p class="pit-msg err">${ esc ( req . query . err ) } </p>`
@@ -859,12 +923,13 @@ moshpitRouter.get("/pit", async (req, res) => {
859923 const mineHtml = ! req . user
860924 ? `<p class="dim">Sign in with your moshcode account to claim one — the same login the CLI uses.</p>
861925 <p><a class="btn acid" href="/">Sign in →</a></p>`
862- : mine . length
863- ? mine . map ( ( t ) => `
926+ : shown . length
927+ ? shown . map ( ( t ) => `
864928 <div class="pit-tld">
865929 <h3 class="acid">.${ esc ( t . tld ) } </h3>
866930 <div class="mono faint" style="font-size:.72rem">
867931 ${ t . alias_of ? `points at <span class="acid">.${ esc ( t . alias_of ) } </span>` : "stands on its own" }
932+ · ${ nameTotals . get ( t . tld ) ?? 0 } name${ ( nameTotals . get ( t . tld ) ?? 0 ) === 1 ? "" : "s" }
868933 </div>
869934 <div class="pit-names">
870935 ${ ( names . get ( t . tld ) || [ ] ) . length
@@ -878,6 +943,12 @@ moshpitRouter.get("/pit", async (req, res) => {
878943 <button class="btn" type="submit" name="release" value="1">Release</button>
879944 </form>` ) . join ( "" )
880945 : `<p class="mono faint" style="font-size:.72rem;margin:6px 0">no names under .${ esc ( t . tld ) } yet</p>` }
946+ ${ ( nameTotals . get ( t . tld ) ?? 0 ) > ( names . get ( t . tld ) || [ ] ) . length ? `
947+ <p class="mono faint" style="font-size:.72rem;margin:6px 0">
948+ ${ ( names . get ( t . tld ) || [ ] ) . length } of ${ nameTotals . get ( t . tld ) } shown${ focused
949+ ? ` — this ending holds more than the ${ NAMES_FOCUSED } a page will draw`
950+ : ` · <a class="acid" href="/pit?tld=${ encodeURIComponent ( t . tld ) } ">open .${ esc ( t . tld ) } on its own →</a>` }
951+ </p>` : "" }
881952 <form method="post" action="/pit/${ esc ( t . tld ) } /names" class="pit-row">
882953 ${ csrfInput ( req ) }
883954 <input name="label" placeholder="new name" autocomplete="off" required>
@@ -955,22 +1026,35 @@ moshpitRouter.get("/pit", async (req, res) => {
9551026 ${ landingCard ( req , landing ) }
9561027 ${ msg }
9571028 ${ req . user ? claimForm ( req ) + bulkClaimForm ( req ) : "" }
958- ${ pitTabs ( tab , { yours : mine . length , theirs : theirs . length , forSale : forSaleCount } ) }
1029+ ${ pitTabs ( tab , { yours : mineTotal , theirs : theirsTotal , forSale : forSaleCount } ) }
9591030
9601031 <section class="pit-panel">
9611032 ${ tab === "yours" ? `
1033+ ${ focused ? `
1034+ <p class="dim" style="max-width:62ch;margin:0 0 14px">
1035+ <a class="acid" href="/pit">← all ${ mineTotal } of your endings</a> · showing
1036+ <span class="mono acid">.${ esc ( focused . tld ) } </span> on its own.
1037+ </p>` : `
9621038 <p class="dim" style="max-width:62ch;margin:0 0 14px">
9631039 Endings you hold. Names under them are yours to mint for nothing — or put a price on the
9641040 ending and let anyone buy one.
965- </p>
1041+ </p>` }
9661042 ${ mineHtml }
1043+ ${ focused ? "" : pager ( {
1044+ page : pageNo , total : mineTotal , perPage : TLDS_PER_PAGE ,
1045+ href : ( n ) => `/pit?tab=yours&page=${ n } ` ,
1046+ } ) }
9671047 ` : `
9681048 <p class="dim" style="max-width:62ch;margin:0 0 14px">
9691049 Endings somebody else holds. Where the operator has set a price you can buy a name under it —
9701050 <span class="mono">foo.whatever</span> without owning <span class="mono">.whatever</span>. Paid in crypto
9711051 through CoinPay; the name lands the moment the payment confirms.
9721052 </p>
9731053 ${ theirsHtml }
1054+ ${ pager ( {
1055+ page : pageNo , total : theirsTotal , perPage : TLDS_PER_PAGE ,
1056+ href : ( n ) => `/pit?tab=theirs&page=${ n } ` ,
1057+ } ) }
9741058 ` }
9751059 </section>
9761060</main>${ footer } ` ,
0 commit comments