This commit is contained in:
Raven Scott
2025-12-17 20:05:50 -05:00
commit 742e27d3f7
276 changed files with 89838 additions and 0 deletions
@@ -0,0 +1,547 @@
:root {
/* Modern Color Palette */
--primary: #6366f1;
--primary-dark: #4f46e5;
--primary-light: #818cf8;
--secondary: #8b5cf6;
--accent: #ec4899;
--success: #10b981;
--error: #ef4444;
--warning: #f59e0b;
/* Background Colors - Darker for glass effect */
--bg-primary: #0a0e1a;
--bg-secondary: #0f1419;
--bg-tertiary: #1a1f2e;
--bg-glass: rgba(255, 255, 255, 0.03);
--bg-glass-hover: rgba(255, 255, 255, 0.06);
--bg-glass-strong: rgba(255, 255, 255, 0.08);
/* Text Colors */
--text-primary: #f1f5f9;
--text-secondary: #cbd5e1;
--text-tertiary: #94a3b8;
--text-muted: #64748b;
/* Borders & Shadows - Enhanced for glass */
--border-color: rgba(255, 255, 255, 0.08);
--border-color-strong: rgba(255, 255, 255, 0.15);
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.4);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.6), 0 10px 10px -5px rgba(0, 0, 0, 0.5);
--shadow-glow: 0 0 30px rgba(99, 102, 241, 0.4);
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Border Radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--radius-2xl: 1.5rem;
--radius-full: 9999px;
/* Transitions */
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
width: 100vw;
max-width: 100vw;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #0a0e1a 0%, #0f1419 50%, #1a1f2e 100%);
background-attachment: fixed;
width: 100vw;
max-width: 100vw;
height: 100vh;
height: 100dvh;
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
margin: 0;
padding: 0;
}
/* Container */
.container {
display: flex;
flex-direction: column;
width: 100vw;
max-width: 100vw;
min-width: 100vw;
height: 100vh;
height: 100dvh;
overflow: hidden;
margin: 0;
padding: var(--spacing-lg);
box-sizing: border-box;
}
/* Header */
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-lg) 0;
flex-shrink: 0;
margin-bottom: var(--spacing-lg);
}
.header h1 {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-align: center;
flex: 1;
}
.header-actions {
flex: 1;
display: flex;
justify-content: flex-end;
}
/* Buttons */
.btn {
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-base);
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
text-decoration: none;
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%);
pointer-events: none;
border-radius: inherit;
}
.btn-primary {
background: rgba(99, 102, 241, 0.3);
border-color: rgba(99, 102, 241, 0.5);
color: var(--text-primary);
box-shadow: 0 4px 6px -1px rgba(99, 102, 241, 0.2), 0 2px 4px -1px rgba(99, 102, 241, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.btn-primary:hover:not(:disabled) {
background: rgba(99, 102, 241, 0.5);
border-color: rgba(99, 102, 241, 0.7);
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.3), 0 4px 6px -2px rgba(99, 102, 241, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Inputs and Selects */
.input,
.select {
width: 100%;
min-width: 0;
background: rgba(15, 20, 30, 0.6);
backdrop-filter: blur(25px) saturate(200%);
-webkit-backdrop-filter: blur(25px) saturate(200%);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: var(--spacing-sm) var(--spacing-md);
color: var(--text-primary);
font-size: 0.9rem;
transition: all var(--transition-base);
font-family: inherit;
box-sizing: border-box;
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255, 255, 255, 0.03);
}
.input:focus,
.select:focus {
outline: none;
border-color: var(--primary);
background: rgba(15, 20, 30, 0.8);
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2), var(--shadow-md), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.input::placeholder {
color: var(--text-tertiary);
}
/* Tables */
.table-container {
flex: 1;
overflow-y: auto;
overflow-x: auto;
min-height: 0;
background: var(--bg-glass);
backdrop-filter: blur(30px) saturate(200%);
-webkit-backdrop-filter: blur(30px) saturate(200%);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
width: 100%;
max-width: 100%;
box-sizing: border-box;
box-shadow: var(--shadow-lg), inset 0 1px 0 rgba(255, 255, 255, 0.05);
position: relative;
}
.table-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 20%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0) 100%);
pointer-events: none;
border-radius: inherit;
z-index: 0;
}
/* When there are few records, still allow scrolling */
.table-container.few-rows {
overflow-y: auto;
}
table {
width: 100%;
max-width: 100%;
border-collapse: collapse;
table-layout: auto;
min-width: 100%;
display: table;
}
thead {
background: rgba(15, 20, 30, 0.7);
backdrop-filter: blur(30px) saturate(200%);
-webkit-backdrop-filter: blur(30px) saturate(200%);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 10;
}
th {
padding: var(--spacing-md);
text-align: left;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
user-select: none;
transition: background var(--transition-base);
display: table-cell;
}
tbody tr {
border-top: 1px solid var(--border-color);
transition: background var(--transition-base);
}
tbody tr:hover {
background: var(--bg-glass-hover);
}
tbody tr:nth-child(even) {
background-color: var(--bg-glass);
}
tbody tr:nth-child(even):hover {
background-color: var(--bg-glass-hover);
}
td {
padding: var(--spacing-md);
color: var(--text-primary);
word-wrap: break-word;
overflow-wrap: break-word;
display: table-cell;
}
/* Badges */
.badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 0.75rem;
font-weight: 600;
border-radius: var(--radius-sm);
white-space: nowrap;
}
.badge-internal {
background: var(--secondary);
color: white;
}
.badge-local {
background: var(--success);
color: white;
}
.badge-remote {
background: var(--primary);
color: white;
}
/* Links */
a {
color: var(--primary-light);
text-decoration: none;
transition: color var(--transition-base);
}
a:hover {
color: var(--primary);
text-decoration: underline;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
flex-wrap: wrap;
flex-shrink: 0;
margin-top: var(--spacing-md);
width: 100%;
min-height: 2.5rem;
visibility: visible;
opacity: 1;
}
/* Pagination info */
#paginationInfo {
display: block;
visibility: visible;
opacity: 1;
}
.pagination button {
min-width: 2.5rem;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid rgba(99, 102, 241, 0.5);
border-radius: var(--radius-sm);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-base);
background: rgba(99, 102, 241, 0.3);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
color: var(--text-primary);
box-shadow: 0 2px 4px rgba(99, 102, 241, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
position: relative;
overflow: hidden;
}
.pagination button::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0) 100%);
pointer-events: none;
border-radius: inherit;
}
.pagination button:hover:not(:disabled) {
background: rgba(99, 102, 241, 0.5);
border-color: rgba(99, 102, 241, 0.7);
box-shadow: 0 4px 6px rgba(99, 102, 241, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.15);
transform: translateY(-2px);
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination button.active {
background: var(--primary-dark);
}
.pagination span {
padding: 0 var(--spacing-sm);
color: var(--text-tertiary);
}
/* Text Utilities */
.text-center {
text-align: center;
}
.text-sm {
font-size: 0.875rem;
}
.text-tertiary {
color: var(--text-tertiary);
}
.text-muted {
color: var(--text-muted);
}
/* Flex Utilities */
.flex {
display: flex;
width: 100%;
max-width: 100%;
}
.flex-col {
flex-direction: column;
}
.flex-1 {
flex: 1;
min-width: 0;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.justify-end {
justify-content: flex-end;
}
.gap-4 {
gap: var(--spacing-md);
}
.gap-2 {
gap: var(--spacing-sm);
}
.mb-6 {
margin-bottom: var(--spacing-lg);
}
.mb-4 {
margin-bottom: var(--spacing-md);
}
.mt-4 {
margin-top: var(--spacing-md);
}
.p-3 {
padding: 0.75rem;
}
.p-8 {
padding: var(--spacing-2xl);
}
.px-4 {
padding-left: var(--spacing-md);
padding-right: var(--spacing-md);
}
.py-2 {
padding-top: var(--spacing-sm);
padding-bottom: var(--spacing-sm);
}
.space-y-4 > * + * {
margin-top: var(--spacing-md);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-secondary);
border-radius: var(--radius-sm);
}
::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: var(--radius-sm);
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-tertiary);
}
/* Responsive */
@media (max-width: 768px) {
.header {
flex-direction: column;
gap: var(--spacing-md);
}
.header h1 {
font-size: 1.5rem;
}
.container {
padding: var(--spacing-md);
}
table {
font-size: 0.875rem;
}
th, td {
padding: var(--spacing-sm);
}
}
File diff suppressed because one or more lines are too long
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="448" height="512">
<path fill="#3b82f6" d="M96 0C43 0 0 43 0 96V416c0 53 43 96 96 96H384h32c17.7 0 32-14.3 32-32s-14.3-32-32-32V384c17.7 0 32-14.3 32-32V32c0-17.7-14.3-32-32-32H384 96zm0 384H352v64H96c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16zm16 48H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16s7.2-16 16-16z"/>
</svg>

After

Width:  |  Height:  |  Size: 489 B

+377
View File
@@ -0,0 +1,377 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#000000">
<title>Peer Directory</title>
<link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="/css/tailwind.css">
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="flex-1"></div>
<h1>P2NS Directory</h1>
<div class="header-actions">
<a
href="https://p2ns.admin"
target="_blank"
class="btn btn-primary"
>
Go To Admin
</a>
</div>
</div>
<!-- Search and Controls -->
<div class="mb-6 space-y-4 flex-shrink-0">
<input
id="searchInput"
type="text"
placeholder="Search domains or hashes..."
class="input"
autofocus>
<div class="flex gap-4 items-center justify-between">
<div class="flex items-center gap-2">
<label for="sortBy" class="text-sm" style="color: var(--text-secondary);">Sort by:</label>
<select
id="sortBy"
class="select"
style="width: auto; min-width: 250px;"
>
<option value="type" selected>Type (Remote → Local → Internal)</option>
<option value="name">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
<option value="hash">Hash (A-Z)</option>
<option value="hash-desc">Hash (Z-A)</option>
<option value="type-desc">Type (Internal → Local → Remote)</option>
</select>
</div>
</div>
</div>
<!-- Domain Table -->
<div class="table-container">
<table>
<thead>
<tr>
<th>Domain</th>
<th>Hash</th>
<th>Type</th>
</tr>
</thead>
<tbody id="domainList"></tbody>
</table>
<div id="scrollSentinel" style="height: 20px; width: 100%; flex-shrink: 0;"></div>
</div>
<!-- Loading indicator and info -->
<div class="mb-4 text-center text-sm text-tertiary flex-shrink-0" style="min-height: 1.5rem;">
<span id="paginationInfo">Loading...</span>
</div>
</div>
<script>
let currentPage = 1;
let currentLimit = 20;
let currentSort = 'type';
let allDomains = [];
let totalDomains = 0;
let isSearching = false;
let isLoading = false;
let hasMore = true;
let scrollObserver = null;
function getTypeBadge(type) {
const badges = {
internal: {
icon: '⚙️',
text: 'Internal',
class: 'badge badge-internal'
},
local: {
icon: '🏠',
text: 'Local',
class: 'badge badge-local'
},
remote: {
icon: '🌐',
text: 'Remote',
class: 'badge badge-remote'
}
};
const badge = badges[type] || badges.remote;
return `<span class="${badge.class}">
<span>${badge.icon}</span>
<span>${badge.text}</span>
</span>`;
}
function renderDomain(domainObj) {
const tr = document.createElement('tr');
// Domain cell with link
const domainCell = document.createElement('td');
const domainLink = document.createElement('a');
domainLink.href = 'https://' + domainObj.domain;
domainLink.textContent = domainObj.domain;
domainLink.target = '_blank';
domainCell.appendChild(domainLink);
// Hash cell
const hashCell = document.createElement('td');
hashCell.style.cssText = 'word-break: break-all; color: var(--text-tertiary);';
hashCell.textContent = domainObj.hash || 'none';
// Type cell with badge
const typeCell = document.createElement('td');
typeCell.innerHTML = getTypeBadge(domainObj.type);
tr.appendChild(domainCell);
tr.appendChild(hashCell);
tr.appendChild(typeCell);
return tr;
}
async function fetchAllDomains(sort = 'type') {
try {
// Fetch all domains with a large limit
const params = new URLSearchParams({ page: '1', limit: '10000', sort });
const response = await fetch('/domains?' + params.toString());
const data = await response.json();
if (data.domains) {
allDomains = data.domains;
totalDomains = data.total;
} else if (Array.isArray(data)) {
// Fallback for old API format
allDomains = data.map(d => typeof d === 'string' ? { domain: d, type: 'remote', hash: 'none', isLocal: false } : d);
totalDomains = allDomains.length;
}
return allDomains;
} catch (err) {
console.error('Failed to fetch all domains:', err);
return [];
}
}
async function fetchDomains(page = 1, limit = 20, sort = 'type', searchQuery = '', append = false, retryCount = 0) {
if (isLoading) return;
try {
isLoading = true;
let domains = [];
let total = 0;
if (searchQuery) {
// When searching, fetch all domains and filter client-side
if (allDomains.length === 0 || currentSort !== sort) {
await fetchAllDomains(sort);
currentSort = sort;
}
const query = searchQuery.toLowerCase();
const filtered = allDomains.filter(d =>
d.domain.toLowerCase().includes(query) ||
(d.hash && d.hash.toLowerCase().includes(query))
);
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
domains = filtered.slice(startIndex, endIndex);
total = filtered.length;
hasMore = endIndex < filtered.length;
renderDomains(domains, append);
updatePaginationInfo(page, total, limit);
} else {
// Normal paginated API call
const params = new URLSearchParams({ page: page.toString(), limit: limit.toString(), sort });
const response = await fetch('/domains?' + params.toString());
const data = await response.json();
if (data.domains) {
domains = data.domains;
total = data.total;
const totalPages = data.totalPages;
hasMore = page < totalPages;
renderDomains(domains, append);
updatePaginationInfo(page, total, limit);
totalDomains = data.total;
} else {
// Fallback
if (allDomains.length === 0) {
allDomains = Array.isArray(data) ? data.map(d => typeof d === 'string' ? { domain: d, type: 'remote', hash: 'none', isLocal: false } : d) : [];
totalDomains = allDomains.length;
}
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
domains = allDomains.slice(startIndex, endIndex);
total = allDomains.length;
hasMore = endIndex < allDomains.length;
renderDomains(domains, append);
updatePaginationInfo(page, total, limit);
}
}
// Setup infinite scroll if there's more to load
// Use setTimeout to ensure DOM is updated
setTimeout(() => {
if (hasMore) {
setupInfiniteScroll();
} else {
cleanupInfiniteScroll();
}
}, 100);
// If no domains found and not searching, retry after a delay
if (domains.length === 0 && !searchQuery && !append) {
const domainList = document.getElementById('domainList');
domainList.innerHTML = '<tr><td colspan="3" class="p-8 text-center" style="color: var(--text-tertiary);">No domains found. Retrying...</td>';
// Wait 2 seconds before retrying
await new Promise(resolve => setTimeout(resolve, 2000));
// Retry the request
isLoading = false;
return fetchDomains(page, limit, sort, searchQuery, append, retryCount + 1);
}
} catch (err) {
console.error('Failed to fetch domains:', err);
const domainList = document.getElementById('domainList');
if (!append) {
domainList.innerHTML = '<tr><td colspan="3" class="p-4 text-center" style="color: var(--error);">Failed to load domains. Retrying...</td>';
}
// Wait 2 seconds before retrying on error
await new Promise(resolve => setTimeout(resolve, 2000));
// Retry the request
isLoading = false;
return fetchDomains(page, limit, sort, searchQuery, append, retryCount + 1);
} finally {
isLoading = false;
}
}
function renderDomains(domains, append = false) {
const domainList = document.getElementById('domainList');
const tableContainer = document.querySelector('.table-container');
if (!append) {
domainList.innerHTML = '';
}
if (domains.length === 0 && !append) {
const tr = document.createElement('tr');
tr.innerHTML = '<td colspan="3" class="p-8 text-center" style="color: var(--text-tertiary);">No domains found.</td>';
domainList.appendChild(tr);
// Remove few-rows class when empty
if (tableContainer) {
tableContainer.classList.remove('few-rows');
}
return;
}
domains.forEach(domainObj => {
const tr = renderDomain(domainObj);
domainList.appendChild(tr);
});
// Add 'few-rows' class if there are 5 or fewer total rows for better appearance
if (tableContainer) {
const totalRows = domainList.querySelectorAll('tr').length;
if (totalRows <= 5) {
tableContainer.classList.add('few-rows');
} else {
tableContainer.classList.remove('few-rows');
}
}
}
function updatePaginationInfo(page, total, limit) {
const paginationInfo = document.getElementById('paginationInfo');
const domainList = document.getElementById('domainList');
const loadedCount = domainList.querySelectorAll('tr').length;
paginationInfo.textContent = `Showing ${loadedCount} of ${total} domains${hasMore ? ' (scroll for more)' : ''}`;
}
function setupInfiniteScroll() {
cleanupInfiniteScroll();
const sentinel = document.getElementById('scrollSentinel');
const tableContainer = document.querySelector('.table-container');
if (!sentinel || !tableContainer) {
// Retry after a short delay if elements aren't ready
setTimeout(setupInfiniteScroll, 100);
return;
}
scrollObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && hasMore && !isLoading) {
console.log('Loading more domains...');
loadMoreDomains();
}
});
}, {
root: tableContainer,
rootMargin: '100px',
threshold: 0.1
});
scrollObserver.observe(sentinel);
console.log('Infinite scroll observer set up');
}
function cleanupInfiniteScroll() {
if (scrollObserver) {
scrollObserver.disconnect();
scrollObserver = null;
}
}
async function loadMoreDomains() {
if (isLoading || !hasMore) return;
currentPage++;
const searchQuery = document.getElementById('searchInput').value;
await fetchDomains(currentPage, currentLimit, currentSort, searchQuery, true);
}
// Event listeners
document.getElementById('searchInput').addEventListener('input', async (e) => {
const searchQuery = e.target.value;
currentPage = 1;
hasMore = true;
isSearching = searchQuery.length > 0;
cleanupInfiniteScroll();
if (isSearching) {
// Clear cache when search changes to force refetch
allDomains = [];
}
await fetchDomains(currentPage, currentLimit, currentSort, searchQuery, false);
});
document.getElementById('sortBy').addEventListener('change', async (e) => {
currentSort = e.target.value;
currentPage = 1;
hasMore = true;
cleanupInfiniteScroll();
// Clear cache when sort changes
allDomains = [];
const searchQuery = document.getElementById('searchInput').value;
await fetchDomains(currentPage, currentLimit, currentSort, searchQuery, false);
});
// Initial fetch
fetchDomains(currentPage, currentLimit, currentSort, '', false);
</script>
</body>
</html>
@@ -0,0 +1,33 @@
{
"name": "peer.directory",
"short_name": "Directory",
"description": "P2NS peer directory browser - Browse and search P2NS domains",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#000000",
"orientation": "any",
"icons": [
{
"src": "/icon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["utilities", "productivity"],
"lang": "en"
}