PageSourceSearch

https://site.nl/_static/57b54aa969c5e910/scripts/features/dashboard/domains/render.js

js site.nl collected 2026-10-02 15:06:45 UTC 18,818 bytes, 553 lines download raw bytes

1import { premiumBadgeHtml, isPremiumDomain } from '../../common/premium-domain.js';
2import { buildReactivationButtonHtml } from '../shared/reactivation.js';
3import { escapeHtml, setSafeHtml } from '../../../client-utils.js';
4import { initLoadingDots } from '../../../ui/loading.js';
5import { normalizeText } from '../../../value-utils.js';
6import { localizeDashboardPath } from '../path-localization.js';
7import { setDashboardClientTabActiveState } from '../shared/tabs.js';
8import {
9  DEFAULT_TAB,
10  DOMAINS_PANEL_SELECTOR,
11  DOMAINS_TABS_NAV_SELECTOR,
12  MOBILE_MEDIA_QUERY,
13  TABLE_CONFIG,
14  TRANSFERRED_AWAY_TAB_KEY,
15  resolveTabPath,
16} from './config.js';
17import {
18  getCellValue,
19  getColumns,
20  getCurrentRows,
21  getHeaderSortState,
22  getPageRows,
23  getTotalPages,
24  getUiState,
25  hasConfigurableColumnOptions,
26  sortRows,
27} from './table.js';
28
29const buildTabsHtml = (data, activeTab) =>
30  data.visibleTabs
31    .map((tab) => {
32      const isActive = tab.key === activeTab;
33      const tabClassName = [
34        'relative inline-flex shrink-0 whitespace-nowrap text-base',
35        "before:pointer-events-none before:block before:absolute before:top-full before:right-0 before:left-auto before:h-0.5 before:bg-foreground before:content-[''] before:transition-all before:duration-[250ms] before:ease-in-out hover:before:left-0 hover:before:right-auto hover:before:w-full",
36        isActive ? 'font-bold before:left-0 before:right-auto before:w-full' : 'font-semibold before:w-0',
37      ].join(' ');
38      return `
39        <a
40          href="${escapeHtml(resolveTabPath(tab.key))}"
41          data-dashboard-link
42          data-dashboard-domains-link
43          data-tab-key="${escapeHtml(tab.key)}"
44          ${isActive ? 'aria-current="page"' : ''}
45          class="${tabClassName}"
46        >
47          ${escapeHtml(data.copy.tabs[tab.key])}
48        </a>
49      `;
50    })
51    .join('');
52
53const renderedTabsStructure = new WeakMap();
54
55const getTabsStructureSignature = (data) =>
56  JSON.stringify(
57    data.visibleTabs.map((tab) => ({
58      href: resolveTabPath(tab.key),
59      key: tab.key,
60      label: data.copy.tabs[tab.key],
61    })),
62  );
63
64export const renderTabs = (root, data, activeTab) => {
65  const nav = root.querySelector(DOMAINS_TABS_NAV_SELECTOR);
66  if (!(nav instanceof HTMLElement)) {
67    return;
68  }
69
70  const structureSignature = getTabsStructureSignature(data);
71  const links = Array.from(nav.querySelectorAll('[data-dashboard-domains-link]'));
72  if (renderedTabsStructure.get(nav) === structureSignature && links.length === data.visibleTabs.length) {
73    links.forEach((link) => {
74      setDashboardClientTabActiveState(link, link.dataset.tabKey === activeTab);
75    });
76    return;
77  }
78
79  const focusedTabKey = nav.contains(root.ownerDocument.activeElement)
80    ? root.ownerDocument.activeElement?.dataset?.tabKey || ''
81    : '';
82  setSafeHtml(nav, buildTabsHtml(data, activeTab));
83  renderedTabsStructure.set(nav, structureSignature);
84  if (focusedTabKey) {
85    Array.from(nav.querySelectorAll('[data-dashboard-domains-link]'))
86      .find((link) => link.dataset.tabKey === focusedTabKey)
87      ?.focus({ preventScroll: true });
88  }
89};
90
91const buildNameCellContent = (tabKey, row, display, className = '') => {
92  if (tabKey === 'expired') {
93    return buildReactivationButtonHtml({ key: `expired:${row.id}`, label: display, className });
94  }
95  const actionMeta = getRowActionMeta(tabKey, row);
96  const classes = [className].filter(Boolean).join(' ');
97  const rowId = normalizeText(row?.id);
98  const rowAttributes = rowId
99    ? `data-dashboard-domains-row-id="${escapeHtml(rowId)}" data-dashboard-domains-row-tab="${escapeHtml(tabKey)}"`
100    : '';
101  if (actionMeta.type === 'link' && actionMeta.href) {
102    return `
103      <a
104        href="${escapeHtml(actionMeta.href)}"
105        data-dashboard-link
106        data-dashboard-domains-row-action
107        ${rowAttributes}
108        class="${escapeHtml(classes)}"
109      >
110        ${escapeHtml(display)}${isPremiumDomain(row) ? premiumBadgeHtml() : ''}
111      </a>
112    `;
113  }
114
115  if (actionMeta.type === 'button') {
116    return `
117      <button
118        type="button"
119        data-dashboard-domains-row-action
120        ${rowAttributes}
121        class="${escapeHtml(classes)}"
122      >
123        ${escapeHtml(display)}
124      </button>
125    `;
126  }
127
128  return `<span class="break-all">${escapeHtml(display)}</span>`;
129};
130
131const getRowActionMeta = (tabKey, row) => {
132  const rowId = normalizeText(row?.id);
133
134  if (tabKey === 'active' && rowId) {
135    return {
136      type: 'link',
137      href: localizeDashboardPath(`/dashboard/domains/${encodeURIComponent(rowId)}`),
138    };
139  }
140
141  if (tabKey === 'external' && rowId) {
142    return {
143      type: 'link',
144      href: localizeDashboardPath(`/dashboard/domains/external/${encodeURIComponent(rowId)}`),
145    };
146  }
147
148  if ((tabKey === 'transfer' || tabKey === 'awaiting-info') && rowId) {
149    return {
150      type: 'button',
151    };
152  }
153
154  return {
155    type: 'text',
156  };
157};
158
159const buildSettingsMenuHtml = (data, uiState, columns) => {
160  const config = TABLE_CONFIG[data.activeTab];
161  const tableCopy = data.copy.tables[data.activeTab];
162
163  if (!hasConfigurableColumnOptions(data.activeTab)) {
164    return '';
165  }
166
167  const columnOptions = config.allColumns
168    .map((column) => {
169      const isSelected = columns.includes(column);
170      const isLocked = column === 'name';
171      return `
172        <label class="relative flex cursor-pointer items-center rounded p-2 text-sm font-bold text-gray hover:bg-foreground-light">
173          <input
174            type="checkbox"
175            ${isSelected ? 'checked' : ''}
176            ${isLocked ? 'disabled' : ''}
177            data-dashboard-domains-column-toggle
178            data-column-name="${escapeHtml(column)}"
179            class="peer absolute top-0 left-0 -z-1 h-5 w-5 opacity-0"
180          />
181          <span
182            class="border-background mr-2 flex h-5 w-5 shrink-0 items-center justify-center rounded border-2 peer-checked:[&>span]:h-3 peer-checked:[&>span]:w-3"
183            aria-hidden="true"
184          >
185            <span
186              class="bg-green flex h-0 w-0 items-center justify-center overflow-hidden rounded-sm leading-none transition-all text-foreground"
187            >
188              &#10003;
189            </span>
190          </span>
191          <span class="flex-1">${escapeHtml(tableCopy.columns[column])}</span>
192        </label>
193      `;
194    })
195    .join('');
196
197  return `
198    <div
199      class="absolute right-0 top-full z-20 mt-2 w-50 rounded-lg border border-stroke-gray bg-foreground text-background p-1 space-y-1 ${uiState.isMenuOpen ? '' : 'hidden'}"
200      data-dashboard-domains-settings-menu
201    >
202      ${columnOptions}
203    </div>
204  `;
205};
206
207const buildPaginationHtml = (totalRows, uiState, copy) => {
208  const totalPages = getTotalPages(totalRows);
209  if (totalPages <= 1) {
210    return '';
211  }
212
213  return `
214    <div class="flex items-center justify-between gap-3 border-t border-stroke-gray px-4 py-4 md:px-6">
215      <button
216        type="button"
217        data-dashboard-domains-page
218        data-page="${uiState.page - 1}"
219        ${uiState.page <= 1 ? 'disabled' : ''}
220        class="inline-flex items-center rounded-md border border-stroke-gray px-3 py-2 text-sm font-bold text-gray ${uiState.page <= 1 ? 'opacity-50' : 'hover:bg-background'}"
221      >
222        ${escapeHtml(copy.common.prev)}
223      </button>
224      <p class="text-sm text-gray">${escapeHtml(copy.common.page)} ${uiState.page} / ${totalPages}</p>
225      <button
226        type="button"
227        data-dashboard-domains-page
228        data-page="${uiState.page + 1}"
229        ${uiState.page >= totalPages ? 'disabled' : ''}
230        class="inline-flex items-center rounded-md border border-stroke-gray px-3 py-2 text-sm font-bold text-gray ${uiState.page >= totalPages ? 'opacity-50' : 'hover:bg-background'}"
231      >
232        ${escapeHtml(copy.common.next)}
233      </button>
234    </div>
235  `;
236};
237
238const buildDesktopRows = (data, columns, rows) =>
239  rows
240    .map(
241      (row) => `
242        <tr class="border-t border-stroke-gray align-top">
243          ${columns
244            .map((column) => {
245              const display = getCellValue(data.activeTab, column, row, data);
246              const isAction = column === 'name' || column === 'domain_name';
247              const isOn = display === data.copy.common.on;
248              const isOff = display === data.copy.common.off;
249
250              let content = `<span>${escapeHtml(display)}</span>`;
251              if (isAction) {
252                content = buildNameCellContent(
253                  data.activeTab,
254                  row,
255                  display,
256                  'text-left font-semibold text-pink underline-offset-2 focus-visible:underline hover:underline',
257                );
258              } else if (isOn || isOff) {
259                content = `<span class="inline-flex uppercase font-bold ${isOn ? 'text-aqua' : 'text-error'}">${escapeHtml(display)}</span>`;
260              }
261
262              return `<td class="px-6 py-4">${content}</td>`;
263            })
264            .join('')}
265        </tr>
266      `,
267    )
268    .join('');
269
270const buildMobileSortButton = (column, label) => `
271  <button
272    type="button"
273    data-dashboard-domains-sort-button
274    data-sort-field="${escapeHtml(column)}"
275    class="text-left translate-y-1"
276  >
277    <span>${escapeHtml(label)}</span>
278  </button>
279`;
280
281const buildMobileCellContent = (data, column, row, display) => {
282  const isAction = column === 'name' || column === 'domain_name';
283  const isOn = display === data.copy.common.on;
284  const isOff = display === data.copy.common.off;
285
286  if (isAction) {
287    return buildNameCellContent(data.activeTab, row, display, 'text-left');
288  }
289
290  if (isOn || isOff) {
291    return `<span class="font-bold uppercase ${isOn ? 'text-aqua' : 'text-error'}">${escapeHtml(display)}</span>`;
292  }
293
294  return `<span>${escapeHtml(display)}</span>`;
295};
296
297const buildMobileRows = (data, columns, rows) => {
298  if (!columns.length) {
299    return '';
300  }
301
302  const tableCopy = data.copy.tables[data.activeTab];
303  const firstColumn = columns[0];
304  const firstColumnLabel = tableCopy.columns[firstColumn];
305
306  return rows
307    .map((row) => {
308      const firstDisplay = getCellValue(data.activeTab, firstColumn, row, data);
309
310      return `
311          <thead>
312            <tr>
313              <th>${buildMobileSortButton(firstColumn, firstColumnLabel)}</th>
314              <th>${buildMobileCellContent(data, firstColumn, row, firstDisplay)}</th>
315            </tr>
316          </thead>
317          <tbody>
318            ${columns
319              .slice(1)
320              .map((column) => {
321                const label = tableCopy.columns[column];
322                const display = getCellValue(data.activeTab, column, row, data);
323
324                return `
325                  <tr>
326                    <th>${buildMobileSortButton(column, label)}</th>
327                    <td>${buildMobileCellContent(data, column, row, display)}</td>
328                  </tr>
329                `;
330              })
331              .join('')}
332          </tbody>
333        `;
334    })
335    .join('');
336};
337
338const buildEmptyStateHtml = (data) => {
339  const emptyCopy = data.activeTab === DEFAULT_TAB ? data.copy.empty.active : data.copy.empty.default;
340  return `
341    <div class="mx-auto my-10 xs:w-4/5 xs:flex flex-wrap items-center justify-between space-y-4 xs:space-y-0">
342      <div class="flex-1 xs:min-w-78 xs:m-4">
343        <h3 class="font-uni-neue text-3xl mb-7 font-normal">${escapeHtml(emptyCopy.title)}</h3>
344        <p class="mb-7">${escapeHtml(emptyCopy.text)}</p>
345        ${
346          data.activeTab === DEFAULT_TAB
347            ? `<button type="button" class="shadow-button inline-flex h-min items-center justify-center border border-solid font-semibold outline-hidden transition-colors duration-300 focus:z-20 focus:outline-hidden focus-visible:outline-hidden px-5 py-2 border-yellow bg-yellow shadow-yellow/20 hover:bg-yellow-light focus-visible:bg-yellow-light focus-visible:ring-yellow/20 focus-visible:ring-4 rounded-md xs:min-w-60">
348              ${escapeHtml(emptyCopy.button)}
349            </button>`
350            : ''
351        }
352      </div>
353      <div class="flex-1 xs:min-w-50 xs:m-4">
354        <img src="/assets/images/dashboard/buy-domains.jpg" alt="" />
355      </div>
356    </div>
357  `;
358};
359
360const getStateCardVisuals = (tabKey) => {
361  const config = TABLE_CONFIG[tabKey];
362  return {
363    color: config?.color,
364    icon: config?.icon,
365  };
366};
367
368const buildStateCardHeaderHtml = (data, trailingContent = '') => {
369  const tableCopy = data.copy.tables[data.activeTab];
370  const visuals = getStateCardVisuals(data.activeTab);
371
372  return `
373    <div class="flex items-center p-6">
374      <div class="mr-4 flex h-12 w-12 items-center justify-center rounded-full ${escapeHtml(visuals.color)} p-2 md:h-18 md:w-18 md:p-4">
375        <span class="${escapeHtml(visuals.icon)} text-lg md:text-2xl"></span>
376      </div>
377      <div class="flex-1">
378        <h4 class="font-uni-neue mb-2 font-bold md:text-xl">${escapeHtml(tableCopy.title)}</h4>
379        <p class="dashboard-card-subtitle">${escapeHtml(tableCopy.description)}</p>
380      </div>
381      ${trailingContent}
382    </div>
383  `;
384};
385
386const buildStateCardHtml = (data, bodyHtml) => {
387  return `
388    <section class="rounded-xl border border-stroke-gray bg-white shadow-bottom shadow-black/[0.03] relative">
389      ${buildStateCardHeaderHtml(data)}
390      <div class="px-6 pb-6">${bodyHtml}</div>
391    </section>
392  `;
393};
394
395const buildDomainsLoadingStateHtml = () => `
396  <div class="flex min-h-40 items-center justify-center" data-dashboard-domains-loading-dots></div>
397`;
398
399const buildTransferredAwayEmptyStateHtml = (data) => {
400  const emptyCopy = data.copy.empty.default;
401
402  return buildStateCardHtml(data, `<p class="text-sm text-gray">${escapeHtml(emptyCopy.title)}</p>`);
403};
404
405const buildErrorStateHtml = (data) =>
406  buildStateCardHtml(
407    data,
408    `
409      <div class="rounded-lg border border-dashed border-stroke-gray bg-background p-6 text-center">
410        <p class="text-sm text-gray">${escapeHtml(data.copy.common.requestError)}</p>
411        <button
412          type="button"
413          data-dashboard-domains-retry
414          class="mt-4 inline-flex items-center rounded-md bg-foreground px-4 py-3 text-sm font-bold text-white"
415        >
416          ${escapeHtml(data.copy.common.retry)}
417        </button>
418      </div>
419    `,
420  );
421
422const mountLoadingDots = (panel) => {
423  const dotsSlot = panel.querySelector('[data-dashboard-domains-loading-dots]');
424  if (!(dotsSlot instanceof HTMLElement)) {
425    return;
426  }
427
428  dotsSlot.replaceChildren(
429    initLoadingDots({
430      className: 'text-foreground',
431      size: 16,
432    }),
433  );
434};
435
436export const renderOverviewPanel = (root, data) => {
437  const panel = root.querySelector(DOMAINS_PANEL_SELECTOR);
438  if (!(panel instanceof HTMLElement) || !data) {
439    return;
440  }
441
442  if (data.activeTab !== TRANSFERRED_AWAY_TAB_KEY && data.isStreamPendingInitialLoad) {
443    setSafeHtml(panel, buildDomainsLoadingStateHtml());
444    mountLoadingDots(panel);
445    return;
446  }
447
448  if (
449    (data.activeTab === DEFAULT_TAB || data.activeTab === 'expired') &&
450    getCurrentRows(data).length > 0 &&
451    (data.domainContacts?.status === 'idle' || data.domainContacts?.status === 'loading')
452  ) {
453    setSafeHtml(panel, buildDomainsLoadingStateHtml());
454    mountLoadingDots(panel);
455    return;
456  }
457
458  if (
459    data.activeTab === TRANSFERRED_AWAY_TAB_KEY &&
460    (data.transferredAway?.status === 'idle' || data.transferredAway?.status === 'loading')
461  ) {
462    setSafeHtml(panel, buildDomainsLoadingStateHtml());
463    mountLoadingDots(panel);
464    return;
465  }
466
467  if (data.activeTab === TRANSFERRED_AWAY_TAB_KEY && data.transferredAway?.status === 'error') {
468    setSafeHtml(panel, buildErrorStateHtml(data));
469    return;
470  }
471
472  const uiState = getUiState(root, data);
473  const columns = getColumns(data, uiState);
474  const sortedRows = sortRows(getCurrentRows(data), uiState.sortField, uiState.sortDirection, data, data.activeTab);
475  const totalPages = getTotalPages(sortedRows.length);
476  if (uiState.page > totalPages) {
477    uiState.page = totalPages;
478  }
479  const pageRows = getPageRows(sortedRows, uiState.page);
480  const tableCopy = data.copy.tables[data.activeTab];
481  const isMobile = window.matchMedia(MOBILE_MEDIA_QUERY).matches;
482
483  if (!sortedRows.length) {
484    if (data.activeTab === TRANSFERRED_AWAY_TAB_KEY) {
485      setSafeHtml(panel, buildTransferredAwayEmptyStateHtml(data));
486      return;
487    }
488
489    setSafeHtml(panel, buildEmptyStateHtml(data));
490    return;
491  }
492
493  const settingsMenu = buildSettingsMenuHtml(data, uiState, columns);
494  const settingsControls = hasConfigurableColumnOptions(data.activeTab)
495    ? `
496        <div class="absolute right-4 top-4">
497          <button data-dashboard-domains-settings-toggle aria-label="${escapeHtml(data.copy.common.settings)}">
498            <span class="icon-three-dots"></span>
499          </button>
500          ${settingsMenu}
501        </div>
502      `
503    : '';
504  const headerButtons = columns
505    .map((column) => {
506      const sortState = getHeaderSortState(uiState, column);
507      const sortAttribute = sortState === 'none' ? '' : ` aria-sort="${sortState}"`;
508      const caretClassName = [
509        'icon-caret text-[10px] transition-transform duration-200',
510        sortState === 'descending' ? '' : 'rotate-180',
511        sortState === 'none' ? 'opacity-0' : 'opacity-100',
512      ]
513        .filter(Boolean)
514        .join(' ');
515
516      return `
517        <th${sortAttribute} class="px-6 py-1 font-overpass text-left text-sm font-bold">
518          <button
519            type="button"
520            data-dashboard-domains-sort-button
521            data-sort-field="${escapeHtml(column)}"
522            class="inline-flex items-center gap-1 uppercase transition-colors focus:text-pink focus-visible:text-pink hover:text-pink group text-left"
523          >
524            <span class="group-hover:underline">${escapeHtml(tableCopy.columns[column])}</span>
525            <span class="${caretClassName}" aria-hidden="true"></span>
526          </button>
527        </th>
528      `;
529    })
530    .join('');
531
532  setSafeHtml(
533    panel,
534    `
535      <section class="rounded-xl border border-stroke-gray bg-white shadow-bottom shadow-black/[0.03] relative">
536        ${buildStateCardHeaderHtml(data, settingsControls)}
537        ${
538          isMobile
539            ? `<table class="dashboard-domains-mobile-table">${buildMobileRows(data, columns, pageRows)}</table>`
540            : `<div class="overflow-x-auto">
541                <table class="min-w-full border-collapse">
542                  <thead class="bg-background">
543                    <tr>${headerButtons}</tr>
544                  </thead>
545                  <tbody>${buildDesktopRows(data, columns, pageRows)}</tbody>
546                </table>
547              </div>`
548        }
549        ${buildPaginationHtml(sortedRows.length, uiState, data.copy)}
550      </section>
551    `,
552  );
553};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.