Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions components/Admin/Mobile/MobileSubdomainList.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { useState } from "react";
import Link from "next/link";
import { shortenAddress, ensFontFallback } from "../../../utils/FrontUtils";
import {
shortenAddress,
ensFontFallback,
formatNameCount,
} from "../../../utils/FrontUtils";
import ConfirmationModal from "../ConfirmationModal";
import pencilIcon from "../../../public/images/icon-pencil-fill.svg";
import Image from "next/image";
Expand All @@ -9,6 +13,7 @@ export default function MobileSubdomainList({
subdomains,
deleteName,
openEditNameModal,
totalNames,
}) {
const [showModal, setShowModal] = useState(null);

Expand Down Expand Up @@ -45,6 +50,9 @@ export default function MobileSubdomainList({
<div className="w-full">
<div className="text-base font-bold text-black bg-gray-100 py-2 px-4 border-t border-b border-neutral-200">
Subname
<span className="pl-2 text-xs font-normal text-brownblack-700">
{formatNameCount(totalNames)}
</span>
</div>
<div className="w-full overflow-hidden">
{subdomains.map((subdomain, index) => (
Expand All @@ -54,23 +62,23 @@ export default function MobileSubdomainList({
>
<div className="flex items-center justify-between">
<div>
<Link
href={`https://app.ens.domains/${subdomain.name}.${subdomain.domain}`}
className="font-medium text-black"
target="_blank"
rel="noreferrer"
style={{ fontFamily: ensFontFallback }}
>
{subdomain.name}.{subdomain.domain}
</Link>
<div className="text-xs text-gray-500 mt-1">
<Link
href={`https://etherscan.io/address/${subdomain.address}`}
href={`https://app.ens.domains/${subdomain.name}.${subdomain.domain}`}
className="font-medium text-black"
target="_blank"
rel="noreferrer"
style={{ fontFamily: ensFontFallback }}
>
{shortenAddress(subdomain.address)}
{subdomain.name}.{subdomain.domain}
</Link>
<div className="text-xs text-gray-500 mt-1">
<Link
href={`https://etherscan.io/address/${subdomain.address}`}
target="_blank"
rel="noreferrer"
>
{shortenAddress(subdomain.address)}
</Link>
</div>
</div>
<button
Expand Down
11 changes: 7 additions & 4 deletions components/Admin/SubdomainTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,29 @@ import { shortenAddress, ensFontFallback } from "../../utils/FrontUtils";
import { Dialog } from "@headlessui/react";
import { useState } from "react";
import ConfirmationModal from "./ConfirmationModal";
import { formatNameCount } from "../../utils/FrontUtils";

export default function SubdomainTable({
subdomains,
admin,
openEditNameModal,
openDeleteSubnameModal
openDeleteSubnameModal,
totalNames,
showPagination
}) {

return (
<>
<div className="w-full h-full overflow-x-auto border rounded-lg border-1 border-neutral-200">
<div className={`w-full h-full overflow-x-auto border border-1 border-neutral-200 rounded-t-lg ${showPagination ? '' : 'rounded-b-lg'}`}>
<table className="min-w-full divide-y divide-neutral-200">
<thead>
<tr className=" bg-neutral-100">
<th className="px-6 py-3 text-left ">
<span className="text-sm font-bold text-brownblack-700">
Name
Subname
</span>
<span className="pl-2 text-xs font-normal text-brownblack-700">
Total: {subdomains.length}
{formatNameCount(totalNames)}
</span>
</th>
<th className="px-6 py-3 text-sm font-bold text-left text-brownblack-700">
Expand Down
61 changes: 61 additions & 0 deletions components/Admin/SubnamesPaginationControls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { Icon } from "@iconify/react";

function getPageNumbers(current, totalPages) {
// Always show first, last, current, and up to 2 before/after current
// Use ellipsis if there is a gap
const pages = [];
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
return pages;
}
if (current <= 4) {
pages.push(1, 2, 3, 4, 5, '...', totalPages);
} else if (current >= totalPages - 3) {
pages.push(1, '...', totalPages - 4, totalPages - 3, totalPages - 2, totalPages - 1, totalPages);
} else {
pages.push(1, '...', current - 1, current, current + 1, '...', totalPages);
}
return pages;
}

export default function SubnamesPaginationControls({
pagination,
onPageChange,
isLoading
}) {
const { offset, limit, total, totalPages } = pagination;
const currentPage = Math.floor(offset / limit) + 1;
const lastPage = totalPages || Math.ceil(total / limit);
const pageNumbers = getPageNumbers(currentPage, lastPage);

const handlePageClick = (page) => {
if (isLoading || page === '...' || page < 1 || page > lastPage) return;
onPageChange((page - 1) * limit);
};

if (totalPages === 1) {
return null;
}

return (
<div className="flex items-center justify-center py-1 md:border-l md:border-r md:border-b md:border-neutral-200 md:rounded-b-lg">
{pageNumbers.map((page, idx) =>
page === '...'
? <span key={idx} className="mx-1 px-2 text-gray-400">...</span>
: (
<button
key={page}
onClick={() => handlePageClick(page)}
disabled={isLoading || page === currentPage}
className={`mx-1 px-3 py-1 rounded border ${page === currentPage
? 'border-orange-400 text-orange-500 bg-orange-50 font-semibold'
: 'border-gray-200 bg-white text-gray-700 hover:bg-gray-100'} ${isLoading ? 'disabled:opacity-50 disabled:cursor-not-allowed' : ''}`}
>
{page}
</button>
)
)}
</div>
);
}
44 changes: 41 additions & 3 deletions pages/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import SubnamesTableLoading from "../components/Admin/SubnamesTableLoading";
import MobileSubdomainList from "../components/Admin/Mobile/MobileSubdomainList";
import pencilIcon from "../public/images/icon-pencil-fill.svg";
import DeleteSubnameDialog from "../components/Admin/DeleteSubnameDialog";
import SubnamesPaginationControls from "../components/Admin/SubnamesPaginationControls";

const blankNameData = {
name: "",
Expand Down Expand Up @@ -70,6 +71,12 @@ export default function Admin() {
const [brandDict, setBrandDict] = useState({});
const [selectedBrand, setSelectedBrand] = useState(null);
const [subdomains, setSubdomains] = useState([]);
const [pagination, setPagination] = useState({
total: 0,
limit: 20,
offset: 0,
totalPages: 0
});

const [adminNameModalOpen, setAdminNameModalOpen] = useState(false);
const [nameModalErrorMsg, setNameModalErrorMsg] = useState("");
Expand Down Expand Up @@ -207,13 +214,17 @@ export default function Admin() {
"/api/admin/list-subdomains?" +
new URLSearchParams({
domain_id: selectedBrand?.domain_id,
limit: pagination.limit,
offset: pagination.offset
})
).then((res) => {
res.json().then((data) => {
res.json().then((response) => {
if (res.status === 200) {
const { data, pagination: paginationData } = response;
setSubdomains(data);
setPagination(paginationData);
} else {
console.log(data);
console.log(response);
}
setIsSubdomainsLoading(false); // Set loading to false when done
});
Expand Down Expand Up @@ -242,7 +253,7 @@ export default function Admin() {
setCurrentDomainData(data);
});
});
}, [selectedBrand]);
}, [selectedBrand, pagination.offset, pagination.limit]);

function openAddNameModal() {
setEditingDomain(false);
Expand Down Expand Up @@ -608,6 +619,16 @@ export default function Admin() {
}
}, [changeResolver]);

const handlePageChange = (newOffset) => {
setPagination(prev => ({
...prev,
offset: newOffset
}));
};

// Determine if pagination should be shown
const showPagination = (pagination.totalPages > 1 || Math.ceil(pagination.total / pagination.limit) > 1);

// if they haven't authenticated, they need to click connect
if (authStatus !== "authenticated") {
return (
Expand Down Expand Up @@ -985,7 +1006,16 @@ export default function Admin() {
openDeleteSubnameModal={openDeleteSubnameModal}
selectedBrand={selectedBrand}
setSubdomains={setSubdomains}
totalNames={pagination.total}
showPagination={showPagination}
/>
{showPagination && (
<SubnamesPaginationControls
pagination={pagination}
onPageChange={handlePageChange}
isLoading={isSubdomainsLoading}
/>
)}
</div>

{/* Mobile list view */}
Expand All @@ -994,7 +1024,15 @@ export default function Admin() {
subdomains={subdomains}
openDeleteSubnameModal={openDeleteSubnameModal}
openEditNameModal={openEditNameModal}
totalNames={pagination.total}
/>
{showPagination && (
<SubnamesPaginationControls
pagination={pagination}
onPageChange={handlePageChange}
isLoading={isSubdomainsLoading}
/>
)}
</div>
</>
)}
Expand Down
60 changes: 48 additions & 12 deletions pages/api/admin/list-subdomains.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import sql from "../../../lib/db";
import { getToken } from "next-auth/jwt";

const DEFAULT_LIMIT = 20;
const DEFAULT_OFFSET = 0;

export default async function handler(req, res) {
const token = await getToken({ req });

Expand All @@ -13,24 +16,57 @@ export default async function handler(req, res) {
return res.status(400).json({ error: "Domain is required" });
}

// Parse pagination parameters with defaults
const limit = parseInt(req.query.limit) || DEFAULT_LIMIT;
const offset = parseInt(req.query.offset) || DEFAULT_OFFSET;

// Validate pagination parameters
if (isNaN(limit) || isNaN(offset) || limit < 1 || offset < 0) {
return res.status(400).json({ error: "Invalid pagination parameters" });
}

const superAdminQuery = await sql`
SELECT * FROM super_admin WHERE address = ${token.sub};
`;
SELECT *
FROM super_admin
WHERE address = ${token.sub};
`;
const adminQuery = await sql`
SELECT * FROM admin
join domain on admin.domain_id = domain.id
WHERE admin.address = ${token.sub}
and domain.id = ${req.query.domain_id}`;
SELECT *
FROM admin
JOIN domain
ON admin.domain_id = domain.id
WHERE admin.address = ${token.sub}
AND domain.id = ${req.query.domain_id}`;
if (superAdminQuery.length === 0 && adminQuery.length === 0) {
return res.status(401).json({ error: "Unauthorized. Please refresh." });
}

// Get total count first
const countQuery = await sql`
SELECT COUNT(*) as total
FROM subdomain
WHERE domain_id = ${req.query.domain_id}
`;
const total = parseInt(countQuery[0].total);

// Get paginated subdomains
const subdomainQuery = await sql`
select subdomain.id, subdomain.name, subdomain.address, domain.name as domain
from subdomain join domain
on subdomain.domain_id = domain.id
where domain.id = ${req.query.domain_id}
order by subdomain.name`;
select subdomain.id, subdomain.name, subdomain.address, domain.name as domain
from subdomain join domain
on subdomain.domain_id = domain.id
where domain.id = ${req.query.domain_id}
order by subdomain.name
LIMIT ${limit}
OFFSET ${offset}
`;

return res.status(200).json(subdomainQuery);
return res.status(200).json({
data: subdomainQuery,
pagination: {
total,
limit,
offset,
totalPages: Math.ceil(total / limit)
}
});
}
33 changes: 33 additions & 0 deletions utils/FrontUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,36 @@ export const updateResolver = async ({
}
}
};

/**
* Converts a number to a human-readable format with k/M suffixes
* @param {number} count - The number to format
* @returns {string} - Formatted string (e.g., 1000 -> 1k, 1500 -> 1.5k)
*/
export const formatNameCount = (count) => {
if (!count && count !== 0) return '';

// Convert to number if it's a string
const num = typeof count === 'string' ? parseInt(count, 10) : count;

// Handle thousands
if (num >= 1000 && num < 1000000) {
const formatted = (num / 1000).toFixed(1);
// Remove decimal if it's .0
return formatted.endsWith('.0')
? `${formatted.slice(0, -2)}k`
: `${formatted}k`;
}

// Handle millions
if (num >= 1000000) {
const formatted = (num / 1000000).toFixed(1);
// Remove decimal if it's .0
return formatted.endsWith('.0')
? `${formatted.slice(0, -2)}M`
: `${formatted}M`;
}

// Return the number as is for values less than 1000
return num.toString();
};