Implement New Views and Refactor App Structure
- Added new components: Header, Sidebar, ExportView, ImportView, EventsView, ComposeView, RecoveryView, SprocketView, and SearchResultsView to enhance the application's functionality and user experience. - Updated App.svelte to integrate the new views and improve the overall layout. - Refactored existing components for better organization and maintainability. - Adjusted CSS styles for improved visual consistency across the application. - Incremented version number to v0.19.3 to reflect the latest changes and additions.
This commit is contained in:
431
app/web/src/SearchResultsView.svelte
Normal file
431
app/web/src/SearchResultsView.svelte
Normal file
@@ -0,0 +1,431 @@
|
||||
<script>
|
||||
export let searchTab = null;
|
||||
export let searchResults = new Map();
|
||||
export let expandedEvents = new Set();
|
||||
export let userRole = "";
|
||||
export let userPubkey = "";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function loadSearchResults(tabId, query, refresh) {
|
||||
dispatch("loadSearchResults", { tabId, query, refresh });
|
||||
}
|
||||
|
||||
function handleSearchScroll(event, tabId) {
|
||||
dispatch("searchScroll", { event, tabId });
|
||||
}
|
||||
|
||||
function toggleEventExpansion(eventId) {
|
||||
dispatch("toggleEventExpansion", eventId);
|
||||
}
|
||||
|
||||
function deleteEvent(eventId) {
|
||||
dispatch("deleteEvent", eventId);
|
||||
}
|
||||
|
||||
function copyEventToClipboard(event, e) {
|
||||
dispatch("copyEventToClipboard", { event, e });
|
||||
}
|
||||
|
||||
function truncatePubkey(pubkey) {
|
||||
if (!pubkey) return "";
|
||||
return pubkey.slice(0, 8) + "..." + pubkey.slice(-8);
|
||||
}
|
||||
|
||||
function getKindName(kind) {
|
||||
const kindNames = {
|
||||
0: "Profile",
|
||||
1: "Text Note",
|
||||
2: "Recommend Relay",
|
||||
3: "Contacts",
|
||||
4: "Encrypted DM",
|
||||
5: "Delete",
|
||||
6: "Repost",
|
||||
7: "Reaction",
|
||||
8: "Badge Award",
|
||||
16: "Generic Repost",
|
||||
40: "Channel Creation",
|
||||
41: "Channel Metadata",
|
||||
42: "Channel Message",
|
||||
43: "Channel Hide Message",
|
||||
44: "Channel Mute User",
|
||||
1984: "Reporting",
|
||||
9734: "Zap Request",
|
||||
9735: "Zap",
|
||||
10000: "Mute List",
|
||||
10001: "Pin List",
|
||||
10002: "Relay List",
|
||||
22242: "Client Auth",
|
||||
24133: "Nostr Connect",
|
||||
27235: "HTTP Auth",
|
||||
30000: "Categorized People",
|
||||
30001: "Categorized Bookmarks",
|
||||
30008: "Profile Badges",
|
||||
30009: "Badge Definition",
|
||||
30017: "Create or update a stall",
|
||||
30018: "Create or update a product",
|
||||
30023: "Long-form Content",
|
||||
30024: "Draft Long-form Content",
|
||||
30078: "Application-specific Data",
|
||||
30311: "Live Event",
|
||||
30315: "User Statuses",
|
||||
30402: "Classified Listing",
|
||||
30403: "Draft Classified Listing",
|
||||
31922: "Date-Based Calendar Event",
|
||||
31923: "Time-Based Calendar Event",
|
||||
31924: "Calendar",
|
||||
31925: "Calendar Event RSVP",
|
||||
31989: "Handler recommendation",
|
||||
31990: "Handler information",
|
||||
34550: "Community Definition",
|
||||
};
|
||||
return kindNames[kind] || `Kind ${kind}`;
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
return new Date(timestamp * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
function truncateContent(content) {
|
||||
if (!content) return "";
|
||||
return content.length > 100 ? content.slice(0, 100) + "..." : content;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if searchTab}
|
||||
<div class="search-results-view">
|
||||
<div class="search-results-header">
|
||||
<h2>🔍 Search Results: "{searchTab.query}"</h2>
|
||||
<button
|
||||
class="refresh-btn"
|
||||
on:click={() =>
|
||||
loadSearchResults(searchTab.id, searchTab.query, true)}
|
||||
disabled={searchResults.get(searchTab.id)?.isLoading}
|
||||
>
|
||||
🔄 Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="search-results-content"
|
||||
on:scroll={(e) => handleSearchScroll(e, searchTab.id)}
|
||||
>
|
||||
{#if searchResults.get(searchTab.id)?.events?.length > 0}
|
||||
{#each searchResults.get(searchTab.id).events as event}
|
||||
<div
|
||||
class="search-result-item"
|
||||
class:expanded={expandedEvents.has(event.id)}
|
||||
>
|
||||
<div
|
||||
class="search-result-row"
|
||||
on:click={() => toggleEventExpansion(event.id)}
|
||||
on:keydown={(e) =>
|
||||
e.key === "Enter" &&
|
||||
toggleEventExpansion(event.id)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="search-result-avatar">
|
||||
<div class="avatar-placeholder">👤</div>
|
||||
</div>
|
||||
<div class="search-result-info">
|
||||
<div class="search-result-author">
|
||||
{truncatePubkey(event.pubkey)}
|
||||
</div>
|
||||
<div class="search-result-kind">
|
||||
<span class="kind-number">{event.kind}</span
|
||||
>
|
||||
<span class="kind-name"
|
||||
>{getKindName(event.kind)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-result-content">
|
||||
<div class="event-timestamp">
|
||||
{formatTimestamp(event.created_at)}
|
||||
</div>
|
||||
<div class="event-content-single-line">
|
||||
{truncateContent(event.content)}
|
||||
</div>
|
||||
</div>
|
||||
{#if userRole === "admin" || userRole === "owner" || (userRole === "write" && event.pubkey && event.pubkey === userPubkey)}
|
||||
<button
|
||||
class="delete-btn"
|
||||
on:click|stopPropagation={() =>
|
||||
deleteEvent(event.id)}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if expandedEvents.has(event.id)}
|
||||
<div class="search-result-details">
|
||||
<div class="json-container">
|
||||
<pre class="event-json">{JSON.stringify(
|
||||
event,
|
||||
null,
|
||||
2,
|
||||
)}</pre>
|
||||
<button
|
||||
class="copy-json-btn"
|
||||
on:click|stopPropagation={(e) =>
|
||||
copyEventToClipboard(event, e)}
|
||||
title="Copy minified JSON to clipboard"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{:else if !searchResults.get(searchTab.id)?.isLoading}
|
||||
<div class="no-results">
|
||||
<p>No results found for "{searchTab.query}"</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if searchResults.get(searchTab.id)?.isLoading}
|
||||
<div class="loading-search">
|
||||
<div class="spinner"></div>
|
||||
<p>Searching...</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.search-results-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1em;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--header-bg);
|
||||
}
|
||||
|
||||
.search-results-header h2 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: var(--primary);
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: var(--accent-hover-color);
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
background: var(--secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.search-results-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5em;
|
||||
background: var(--card-bg);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-result-item.expanded {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.search-result-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.search-result-avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2em;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.search-result-info {
|
||||
flex-shrink: 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.search-result-author {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9em;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.search-result-kind {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.kind-number {
|
||||
background: var(--primary);
|
||||
color: var(--text-color);
|
||||
padding: 0.1em 0.4em;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.7em;
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.kind-name {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.search-result-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.event-timestamp {
|
||||
font-size: 0.8em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.6;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.event-content-single-line {
|
||||
color: var(--text-color);
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
background: var(--danger);
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
padding: 0.5em;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: var(--danger);
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.search-result-details {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 1em;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.json-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.event-json {
|
||||
background: var(--code-bg);
|
||||
padding: 1em;
|
||||
border: 0;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.4;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
color: var(--code-text);
|
||||
}
|
||||
|
||||
.copy-json-btn {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
background: var(--primary);
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.8em;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.copy-json-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 2em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.loading-search {
|
||||
text-align: center;
|
||||
padding: 2em;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-top: 2px solid var(--primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1em;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user