This commit is contained in:
2025-01-03 22:58:48 -05:00
parent 1e8553c653
commit c1707d94ef
6 changed files with 1516 additions and 483 deletions

View File

@@ -1,150 +0,0 @@
// Global variables
let archiveData = [];
let sortedData = [];
let currentSort = { column: 'created_at', order: 'desc' };
const rowsPerPage = 100;
let currentPage = 1;
// Event Listeners
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('fileInput').addEventListener('change', handleFileLoad);
// Add click listeners to sortable headers
document.querySelectorAll('#dataTable th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
sortTable(th.dataset.sort);
});
});
});
// File handling
function handleFileLoad(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (!Array.isArray(data)) throw new Error('Invalid archive format: Expected an array.');
archiveData = data;
sortedData = [...archiveData];
updateStats();
sortTable(currentSort.column); // Initial sort
} catch (err) {
alert(`Failed to load archive: ${err.message}`);
}
};
reader.readAsText(file);
}
// Statistics
function updateStats() {
const statsDiv = document.getElementById('stats');
const kindCounts = archiveData.reduce((acc, event) => {
acc[event.kind] = (acc[event.kind] || 0) + 1;
return acc;
}, {});
const totalEvents = archiveData.length;
let statsText = `Total Events: ${totalEvents}<br>`;
statsText += 'Most Common Types: ';
const topKinds = Object.entries(kindCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 3)
.map(([kind, count]) => `${getKindLabel(parseInt(kind))}: ${count}`);
statsText += topKinds.join(' | ');
statsDiv.innerHTML = statsText;
}
// Table rendering
function renderTable() {
const tbody = document.querySelector('#dataTable tbody');
tbody.innerHTML = '';
const start = (currentPage - 1) * rowsPerPage;
const end = start + rowsPerPage;
const pageData = sortedData.slice(start, end);
if (pageData.length === 0) {
tbody.innerHTML = `<tr class="empty-message">
<td colspan="4">No data loaded. Please load a JSON archive file.</td>
</tr>`;
return;
}
pageData.forEach((event, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="number-column">${start + index + 1}</td>
<td>${new Date(event.created_at * 1000).toLocaleString()}</td>
<td>${getKindLabel(event.kind)}</td>
<td class="content-cell">${getContentDisplay(event)}</td>
`;
tbody.appendChild(row);
});
updatePagination(end);
}
// Pagination
function updatePagination(end) {
document.getElementById('prevPage').disabled = currentPage === 1;
document.getElementById('nextPage').disabled = end >= sortedData.length;
document.getElementById('pageInfo').innerText = `Page ${currentPage} of ${Math.ceil(sortedData.length / rowsPerPage)}`;
}
function changePage(offset) {
currentPage += offset;
renderTable();
}
// Sorting
function sortTable(column) {
if (currentSort.column === column) {
currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.order = 'desc';
}
const order = currentSort.order === 'asc' ? 1 : -1;
sortedData.sort((a, b) => {
if (a[column] < b[column]) return -1 * order;
if (a[column] > b[column]) return 1 * order;
return 0;
});
currentPage = 1;
renderTable();
}
// Helper functions
function getKindLabel(kind) {
const kinds = {
0: 'Profile Metadata',
1: 'Short Text Note',
2: 'Recommend Relay',
3: 'Contacts',
4: 'Encrypted DM',
6: 'Repost',
7: 'Reaction',
40: 'Channel Creation',
41: 'Channel Metadata',
42: 'Channel Message',
30023: 'Long-form Content',
// Add more as needed
};
return `${kind} (${kinds[kind] || 'Unknown'})`;
}
function getContentDisplay(event) {
if (event.kind === 4) {
const recipient = event.tags.find(tag => tag[0] === 'p');
return `Encrypted Message: ${event.content || 'N/A'}<br>Recipient: ${recipient ? recipient[1] : 'Unknown'}`;
}
return event.content || 'No content available';
}

View File

@@ -1,107 +0,0 @@
<<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Nostr Archive Collector</title>
<style>
/* Basic styling for better UX */
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 50px auto;
background: #fff;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
}
label {
display: block;
margin-top: 15px;
}
input[type="text"], textarea {
width: 100%;
padding: 10px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
button {
margin-top: 20px;
padding: 10px 15px;
background: #28a745;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#status {
margin-top: 20px;
white-space: pre-wrap;
}
#downloadBtn {
background: #007bff;
}
#spinner {
display: none;
text-align: center;
margin-top: 20px;
}
/* Simple CSS Spinner */
.loader {
border: 8px solid #f3f3f3;
border-top: 8px solid #007bff;
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<h1>Nostr Archive Collector</h1>
<form id="archiveForm">
<label for="npub">NPub (Public Key):</label>
<input type="text" id="npub" name="npub" placeholder="Enter NPub" required>
<!-- Removed pattern and title attributes -->
<label for="relays">Relay URLs (one per line):</label>
<textarea id="relays" name="relays" rows="5" placeholder="wss://relay1.example.com
wss://relay2.example.com" required></textarea>
<button type="submit">Start Collecting</button>
</form>
<div id="spinner">
<div class="loader"></div>
</div>
<div id="status"></div>
<button id="downloadBtn" style="display: none;">Download Archive</button>
</div>
<!-- Include nostr-tools library via CDN (correct path) -->
<script src="https://cdn.jsdelivr.net/npm/nostr-tools@1.7.0/lib/nostr.bundle.js"></script>
<script src="script.js"></script>
</body>
</html>

View File

@@ -8,67 +8,169 @@
<meta http-equiv="Expires" content="0">
<title>Archivestr</title>
<meta name="description" content="Archivestr: A Nostr Archive Creation, Browser, and Broadcaster Tool">
<link rel="stylesheet" href="styles.css?v=" + Math.random()>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f9f9f9;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
text-align: center;
background: #fff;
padding: 20px 40px;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
h1 {
margin-bottom: 20px;
font-size: 24px;
}
p {
margin-bottom: 30px;
font-size: 16px;
color: #555;
}
a {
display: inline-block;
margin: 10px;
padding: 10px 15px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 16px;
transition: background-color 0.3s;
}
a:hover {
background-color: #0056b3;
}
.github-link {
margin-top: 20px;
display: inline-block;
font-size: 14px;
color: #007bff;
text-decoration: none;
}
.github-link:hover {
text-decoration: underline;
}
</style>
<!-- Corrected Cache-Busting for CSS -->
<link rel="stylesheet" href="styles.css?v=1.0">
</head>
<body>
<div class="container">
<h1>Archivestr</h1>
<p>A Nostr Archive Creation, Browser, and Broadcaster Tool</p>
<a href="collector.html">Collector</a>
<a href="view-archive.html">View Archive</a>
<a class="github-link" href="https://github.com/PR0M3TH3AN/Archivestr" target="_blank">GitHub Repository</a>
<!-- Navigation -->
<div class="nav-links">
<a href="#home" onclick="showSection('home')">Home</a>
<a href="#collector" onclick="showSection('collector')">Collector</a>
<a href="#viewer" onclick="showSection('viewer')">View Archive</a>
<a href="#broadcast" onclick="showSection('broadcast')">Broadcast</a>
</div>
<!-- Home Section -->
<div id="home" class="section active">
<p>Choose an option above to get started!</p>
</div>
<!-- Collector Section -->
<div id="collector" class="section">
<h2>Nostr Archive Collector</h2>
<form id="archiveForm">
<label for="npub">NPub (Public Key):</label>
<input type="text" id="npub" name="npub" placeholder="Enter NPub" required>
<label for="relays">Relay URLs (one per line):</label>
<textarea id="relays" name="relays" rows="5" placeholder="wss://relay1.example.com
wss://relay2.example.com" required></textarea>
<button type="submit">Start Collecting</button>
</form>
<div id="spinner">
<div class="loader"></div>
</div>
<div id="progressContainer" class="progress-container" style="display: none;">
<div id="progressBar" class="progress-bar">
<span class="progress-text">0%</span>
</div>
</div>
<div id="status"></div>
<button id="downloadBtn" style="display: none;">Download Archive</button>
</div>
<!-- Broadcast Section -->
<div id="broadcast" class="section">
<h2>Broadcast Archive</h2>
<p>Select a JSON archive file to broadcast its events to relays.</p>
<input type="file" id="broadcastFileInput" accept=".json">
<div id="broadcastStatus"></div>
<div class="relay-input">
<label for="broadcastRelays">Relay URLs (one per line):</label>
<!-- Corrected Placeholder without Indentation -->
<textarea id="broadcastRelays" rows="5" placeholder="wss://relay1.example.com
wss://relay2.example.com" required></textarea>
</div>
<button id="startBroadcastBtn" style="display: none;">Start Broadcasting</button>
<div id="broadcastProgress" class="progress-container" style="display: none;">
<div class="progress-bar">
<span class="progress-text">0%</span>
</div>
</div>
</div>
<!-- Viewer Section -->
<div id="viewer" class="section">
<h2>Archive Viewer</h2>
<div id="viewerStats" class="stats">No data loaded</div>
<input type="file" id="viewerFileInput" accept=".json">
<!-- Advanced Search Container -->
<div class="search-container">
<div class="search-bar">
<input
type="text"
id="searchInput"
placeholder="Search..."
class="search-input"
>
<button id="advancedSearchToggle" class="search-toggle">
Advanced Search
</button>
</div>
<div id="advancedSearch" class="advanced-search" style="display: none;">
<div class="search-options">
<label>
<input type="checkbox" id="useRegex">
Use Regex
</label>
<label>
<input type="checkbox" id="caseSensitive">
Case Sensitive
</label>
<div class="filter-fields">
<label>Search in:</label>
<label>
<input type="checkbox" id="searchContent" checked>
Content
</label>
<label>
<input type="checkbox" id="searchKind" checked>
Kind
</label>
<label>
<input type="checkbox" id="searchDate" checked>
Date
</label>
</div>
</div>
<div class="kind-filter">
<label>Filter by Kind:</label>
<select id="kindFilter" multiple>
<!-- Options will be populated by JavaScript -->
</select>
</div>
<div class="date-filter">
<label>Date Range:</label>
<input type="datetime-local" id="dateFrom" placeholder="From">
<input type="datetime-local" id="dateTo" placeholder="To">
</div>
</div>
</div>
<!-- Data Table -->
<table id="dataTable">
<thead>
<tr>
<th class="number-column">#</th>
<th data-sort="created_at">Time <span class="sort-indicator"></span></th>
<th data-sort="kind">Kind <span class="sort-indicator"></span></th>
<th>Content</th>
<th>Actions</th> <!-- New Actions Column -->
</tr>
</thead>
<tbody>
<tr class="empty-message">
<td colspan="5">No data loaded. Please load a JSON archive file.</td> <!-- Updated colspan -->
</tr>
</tbody>
</table>
<div class="pagination">
<button id="prevPage" onclick="changePage(-1)" disabled>Previous</button>
<span id="pageInfo">Page 1</span>
<button id="nextPage" onclick="changePage(1)" disabled>Next</button>
</div>
</div>
</div>
<footer>
<div class="footer-content">
<span class="copyright">CC0 - No Rights Reserved</span>
<a href="https://github.com/PR0M3TH3AN/Archivestr" target="_blank" class="footer-link">View on GitHub</a>
</div>
</footer>
<!-- Scripts -->
<!-- Corrected Cache-Busting for JS -->
<script src="https://cdn.jsdelivr.net/npm/nostr-tools@1.7.0/lib/nostr.bundle.js"></script>
<script src="script.js?v=1.0"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +1,357 @@
/* Base styles */
/* Base layout */
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-elevated: #1a1a1a;
--text-primary: #ffffff;
--text-secondary: #b3b3b3;
--accent-primary: #007bff;
--accent-hover: #0056b3;
--border-color: #404040;
--success-color: #28a745;
}
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f9f9f9;
background-color: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
max-width: 1200px;
width: 95%;
max-width: 1600px;
margin: 20px auto;
padding: 20px;
background: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 30px 40px;
background: var(--bg-secondary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
flex: 1;
box-sizing: border-box;
border-radius: 8px;
}
/* Header and Stats */
/* Tooltip styles */
.tooltip {
position: relative;
display: inline-block;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 250px;
background-color: var(--bg-elevated);
color: var(--text-primary);
text-align: center;
border-radius: 6px;
padding: 8px;
border: 1px solid var(--border-color);
font-size: 14px;
/* Position the tooltip */
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
/* Animation */
opacity: 0;
transition: opacity 0.3s;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* Progress bar styles */
.progress-container {
width: 100%;
background-color: var(--bg-primary);
border-radius: 4px;
margin: 10px 0;
overflow: hidden;
}
.progress-bar {
height: 20px;
background-color: var(--accent-primary);
width: 0%;
transition: width 0.3s ease;
position: relative;
}
.progress-text {
position: absolute;
right: 10px;
color: var(--text-primary);
font-size: 12px;
line-height: 20px;
}
/* Typography */
h1 {
text-align: center;
margin-bottom: 10px;
margin-bottom: 20px;
font-size: 32px;
color: var(--text-primary);
}
.stats {
text-align: center;
color: #666;
h2 {
font-size: 24px;
margin-bottom: 20px;
font-size: 1.1em;
color: var(--text-primary);
}
p {
text-align: center; /* This will center the text */
margin-bottom: 30px;
font-size: 16px;
color: var(--text-secondary);
line-height: 1.5;
}
/* Navigation */
.nav-links {
text-align: center;
margin-bottom: 40px;
padding: 20px 0;
border-bottom: 1px solid var(--border-color);
}
.nav-links a {
display: inline-block;
margin: 10px 20px;
padding: 12px 24px;
background-color: var(--bg-elevated);
color: var(--text-primary);
text-decoration: none;
border-radius: 6px;
font-size: 18px;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
}
.nav-links a:hover {
background-color: var(--accent-primary);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* Section handling */
.section {
display: none;
text-align: center;
}
.section.active {
display: block;
}
/* Form elements */
form {
max-width: 800px;
margin: 0 auto;
text-align: left;
padding: 20px;
}
label {
display: block;
margin-top: 20px;
font-size: 16px;
font-weight: bold;
color: var(--text-secondary);
}
input[type="text"],
input[type="datetime-local"],
textarea {
width: 100%;
padding: 12px;
margin-top: 8px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
box-sizing: border-box;
font-size: 16px;
color: var(--text-primary);
transition: all 0.3s ease;
}
input[type="text"]:focus,
input[type="datetime-local"]:focus,
textarea:focus {
border-color: var(--accent-primary);
outline: none;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.2);
}
textarea {
min-height: 120px;
resize: vertical;
}
input[type="file"] {
display: block;
margin: 20px auto;
margin: 30px auto;
padding: 10px;
color: var(--text-secondary);
}
button {
margin-top: 20px;
padding: 12px 24px;
background: var(--accent-primary);
color: var(--text-primary);
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
#downloadBtn {
background: var(--accent-primary);
display: block; /* Makes the button a block element */
margin: 20px auto; /* Auto margins on left and right will center it */
}
button:hover:not(:disabled) {
background: var(--accent-hover);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
button:disabled {
background: var(--bg-elevated);
cursor: not-allowed;
}
/* Advanced Search Styles */
.search-container {
margin: 20px 0;
padding: 20px;
background: var(--bg-elevated);
border-radius: 8px;
border: 1px solid var(--border-color);
}
.search-bar {
display: flex;
gap: 10px;
margin-bottom: 15px;
}
.search-input {
flex: 1;
padding: 12px 20px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 16px;
}
.search-toggle {
padding: 12px 20px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
cursor: pointer;
transition: all 0.3s ease;
}
.advanced-search {
padding: 15px;
border-top: 1px solid var(--border-color);
margin-top: 15px;
}
.search-options {
display: flex;
gap: 20px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.filter-fields {
display: flex;
gap: 15px;
align-items: center;
}
.kind-filter {
margin: 15px 0;
}
.kind-filter select {
padding: 8px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
min-width: 200px;
}
.date-filter {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
/* Table styles */
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
border-collapse: separate;
border-spacing: 0;
margin: 30px 0;
background: var(--bg-secondary);
border-radius: 8px;
overflow: hidden;
}
th, td {
padding: 10px;
border: 1px solid #ddd;
padding: 15px;
border: 1px solid var(--border-color);
text-align: left;
vertical-align: top;
}
th {
background: var(--bg-elevated);
font-weight: bold;
color: var(--text-primary);
cursor: pointer;
background: #f4f4f4;
position: relative;
transition: background-color 0.3s ease;
}
th:hover {
background: #e0e0e0;
background: var(--accent-primary);
}
.empty-message {
text-align: center;
color: #888;
margin-top: 20px;
td {
color: var(--text-secondary);
}
tr:hover {
background: var(--bg-elevated);
}
.number-column {
width: 50px;
text-align: right;
color: var(--text-secondary);
}
.content-cell {
max-width: 600px;
max-width: 800px;
white-space: pre-wrap;
word-break: break-word;
}
@@ -82,10 +370,39 @@ th:hover {
content: '↓';
}
.number-column {
width: 50px;
text-align: right;
color: #666;
/* Stats and messages */
.stats {
text-align: center;
color: var(--text-secondary);
margin-bottom: 20px;
font-size: 1.1em;
padding: 20px;
background: var(--bg-elevated);
border-radius: 6px;
}
.empty-message {
text-align: center;
color: var(--text-secondary);
}
#status {
margin-top: 20px;
white-space: pre-wrap;
color: var(--text-secondary);
}
/* Search highlighting */
.search-highlight {
background-color: rgba(0, 123, 255, 0.2);
border-radius: 3px;
padding: 2px;
}
.regex-highlight {
background-color: rgba(255, 123, 0, 0.2);
border-radius: 3px;
padding: 2px;
}
/* Pagination */
@@ -97,14 +414,233 @@ th:hover {
.pagination button {
margin: 0 5px;
padding: 5px 10px;
background: #007BFF;
color: white;
background: var(--accent-primary);
color: var(--text-primary);
border: none;
cursor: pointer;
border-radius: 3px;
}
.pagination button:disabled {
background: #ccc;
background: var(--bg-elevated);
cursor: not-allowed;
}
/* Spinner */
#spinner {
display: none;
text-align: center;
margin-top: 20px;
}
.loader {
border: 8px solid var(--bg-elevated);
border-top: 8px solid var(--accent-primary);
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Footer */
footer {
background-color: var(--bg-elevated);
color: var(--text-secondary);
padding: 20px;
text-align: center;
margin-top: auto;
}
.footer-content {
max-width: 1600px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.copyright {
color: var(--text-secondary);
font-size: 14px;
}
.footer-link {
color: var(--text-primary);
text-decoration: none;
font-size: 14px;
transition: color 0.3s;
}
.footer-link:hover {
color: var(--accent-primary);
}
/* Responsive adjustments */
@media (max-width: 1200px) {
.container {
width: 95%;
padding: 20px;
}
.content-cell {
max-width: 600px;
}
}
@media (max-width: 768px) {
.container {
width: 100%;
margin: 0;
padding: 15px;
border-radius: 0;
}
.nav-links a {
display: block;
margin: 10px auto;
width: 80%;
max-width: 300px;
}
.search-bar {
flex-direction: column;
}
.date-filter {
flex-direction: column;
align-items: stretch;
}
/* Broadcast section styles */
.relay-input {
max-width: 800px;
margin: 20px auto;
text-align: left;
}
.relay-input textarea {
width: 100%;
margin-top: 10px;
}
#broadcastStatus {
margin: 20px 0;
color: var(--text-secondary);
}
#startBroadcastBtn {
display: block;
margin: 20px auto;
}
#broadcast .progress-container {
max-width: 800px;
margin: 20px auto;
}
.broadcast-status {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 15px;
}
.status-section {
padding: 10px;
background: #f9f9f9;
border-radius: 4px;
}
.status-section h3 {
margin: 0 0 10px 0;
color: #333;
font-size: 1.1em;
display: flex;
align-items: center;
gap: 8px;
}
.status-section h3 .spinner {
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
margin-left: 5px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.status-section p {
margin: 5px 0;
color: #666;
display: flex;
align-items: center;
gap: 8px;
}
.event-id {
font-family: monospace;
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
display: inline-block;
margin-right: 5px;
}
.copy-button {
background: #e0e0e0;
border: none;
border-radius: 3px;
padding: 2px 8px;
cursor: pointer;
font-size: 0.8em;
color: #666;
}
.copy-button:hover {
background: #d0d0d0;
}
.copy-button.copied {
background: #90EE90;
color: #006400;
}
.status-section.error {
background: #fff0f0;
border-left: 3px solid #ff4444;
}
.final-summary {
margin-top: 20px;
padding: 20px;
background: #f0f8ff;
border: 1px solid #b8daff;
border-radius: 5px;
}
.summary-section {
margin: 10px 0;
}
.summary-section h4 {
margin: 10px 0;
color: #2c5282;
}
}

View File

@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Nostr Archive Viewer</title>
<link rel="stylesheet" href="styles.css?v=" + Math.random()>
</head>
<body>
<div class="container">
<h1>Nostr Archive Viewer</h1>
<div id="stats" class="stats">No data loaded</div>
<input type="file" id="fileInput" accept=".json">
<table id="dataTable">
<thead>
<tr>
<th class="number-column">#</th>
<th data-sort="created_at">Time <span class="sort-indicator"></span></th>
<th data-sort="kind">Kind <span class="sort-indicator"></span></th>
<th>Content</th>
</tr>
</thead>
<tbody>
<tr class="empty-message">
<td colspan="4">No data loaded. Please load a JSON archive file.</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button id="prevPage" onclick="changePage(-1)" disabled>Previous</button>
<span id="pageInfo">Page 1</span>
<button id="nextPage" onclick="changePage(1)" disabled>Next</button>
</div>
</div>
<script src="app.js"></script>
</body>
</html>