mirror of
https://github.com/PR0M3TH3AN/Archivestr.git
synced 2025-09-08 07:19:03 +00:00
update
This commit is contained in:
150
src/app.js
150
src/app.js
@@ -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';
|
||||
}
|
@@ -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>
|
214
src/index.html
214
src/index.html
@@ -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>
|
||||
|
898
src/script.js
898
src/script.js
File diff suppressed because it is too large
Load Diff
600
src/styles.css
600
src/styles.css
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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>
|
Reference in New Issue
Block a user