Files
next.orly.dev/app/web/src/RecoveryView.svelte
mleku 8ef3114f5c Refactor project to modularize constants and utilities.
Moved reusable constants and helper functions to dedicated modules for improved maintainability and reusability. Improved build configuration to differentiate output directories for development and production. Enhanced server error handling and added safeguards for disabled web UI scenarios.
2025-12-05 19:25:13 +00:00

360 lines
11 KiB
Svelte

<script>
export let recoverySelectedKind = null;
export let recoveryCustomKind = "";
export let isLoadingRecovery = false;
export let recoveryEvents = [];
export let recoveryHasMore = false;
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
const replaceableKinds = [
{ value: 0, label: "Profile (0)" },
{ value: 3, label: "Contacts (3)" },
{ value: 10000, label: "Mute List (10000)" },
{ value: 10001, label: "Pin List (10001)" },
{ value: 10002, label: "Relay List (10002)" },
{ value: 30000, label: "Categorized People (30000)" },
{ value: 30001, label: "Categorized Bookmarks (30001)" },
{ value: 30008, label: "Profile Badges (30008)" },
{ value: 30009, label: "Badge Definition (30009)" },
{ value: 30017, label: "Create or update a stall (30017)" },
{ value: 30018, label: "Create or update a product (30018)" },
{ value: 30023, label: "Long-form Content (30023)" },
{ value: 30024, label: "Draft Long-form Content (30024)" },
{ value: 30078, label: "Application-specific Data (30078)" },
{ value: 30311, label: "Live Event (30311)" },
{ value: 30315, label: "User Statuses (30315)" },
{ value: 30402, label: "Classified Listing (30402)" },
{ value: 30403, label: "Draft Classified Listing (30403)" },
{ value: 31922, label: "Date-Based Calendar Event (31922)" },
{ value: 31923, label: "Time-Based Calendar Event (31923)" },
{ value: 31924, label: "Calendar (31924)" },
{ value: 31925, label: "Calendar Event RSVP (31925)" },
{ value: 31989, label: "Handler recommendation (31989)" },
{ value: 31990, label: "Handler information (31990)" },
{ value: 34550, label: "Community Definition (34550)" },
];
function selectRecoveryKind() {
dispatch("selectRecoveryKind");
}
function handleCustomKindInput() {
dispatch("handleCustomKindInput");
}
function loadRecoveryEvents() {
dispatch("loadRecoveryEvents");
}
function repostEventToAll(event) {
dispatch("repostEventToAll", event);
}
function repostEvent(event) {
dispatch("repostEvent", event);
}
function copyEventToClipboard(event, e) {
dispatch("copyEventToClipboard", { event, e });
}
function isCurrentVersion(event) {
// This logic would need to be passed from parent or implemented here
// For now, just return false for old versions
return false;
}
</script>
<div class="recovery-tab">
<div>
<h3>Event Recovery</h3>
<p>Search and recover old versions of replaceable events</p>
</div>
<div class="recovery-controls-card">
<div class="recovery-controls">
<div class="kind-selector">
<label for="recovery-kind">Select Event Kind:</label>
<select
id="recovery-kind"
bind:value={recoverySelectedKind}
on:change={selectRecoveryKind}
>
<option value={null}>Choose a replaceable kind...</option>
{#each replaceableKinds as kind}
<option value={kind.value}>{kind.label}</option>
{/each}
</select>
</div>
<div class="custom-kind-input">
<label for="custom-kind">Or enter custom kind number:</label>
<input
id="custom-kind"
type="number"
bind:value={recoveryCustomKind}
on:input={handleCustomKindInput}
placeholder="e.g., 10001"
min="0"
/>
</div>
</div>
</div>
{#if (recoverySelectedKind !== null && recoverySelectedKind !== undefined && recoverySelectedKind >= 0) || (recoveryCustomKind !== "" && parseInt(recoveryCustomKind) >= 0)}
<div class="recovery-results">
{#if isLoadingRecovery}
<div class="loading">Loading events...</div>
{:else if recoveryEvents.length === 0}
<div class="no-events">No events found for this kind</div>
{:else}
<div class="events-list">
{#each recoveryEvents as event}
{@const isCurrent = isCurrentVersion(event)}
<div class="event-item" class:old-version={!isCurrent}>
<div class="event-header">
<div class="event-header-left">
<span class="event-kind">
{#if isCurrent}
Current Version{/if}</span
>
<span class="event-timestamp">
{new Date(
event.created_at * 1000,
).toLocaleString()}
</span>
</div>
<div class="event-header-actions">
{#if !isCurrent}
<button
class="repost-all-button"
on:click={() =>
repostEventToAll(event)}
>
🌐 Repost to All
</button>
<button
class="repost-button"
on:click={() => repostEvent(event)}
>
🔄 Repost
</button>
{/if}
<button
class="copy-json-btn"
on:click|stopPropagation={(e) =>
copyEventToClipboard(event, e)}
>
📋 Copy JSON
</button>
</div>
</div>
<div class="event-content">
<pre class="event-json">{JSON.stringify(
event,
null,
2,
)}</pre>
</div>
</div>
{/each}
</div>
{#if recoveryHasMore}
<button
class="load-more"
on:click={loadRecoveryEvents}
disabled={isLoadingRecovery}
>
Load More Events
</button>
{/if}
{/if}
</div>
{/if}
</div>
<style>
.recovery-tab {
width: 100%;
max-width: 1200px;
margin: 0;
padding: 20px;
background: var(--header-bg);
color: var(--text-color);
border-radius: 8px;
box-sizing: border-box;
}
.recovery-tab h3 {
margin: 0 0 0.5rem 0;
color: var(--text-color);
font-size: 1.5rem;
font-weight: 600;
}
.recovery-tab p {
margin: 0 0 1.5rem 0;
color: var(--text-color);
opacity: 0.8;
line-height: 1.4;
}
.recovery-controls-card {
background-color: var(--card-bg);
border-radius: 0.5em;
padding: 1em;
margin-bottom: 1.5rem;
}
.recovery-controls {
display: flex;
flex-direction: column;
gap: 1em;
}
.kind-selector,
.custom-kind-input {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.kind-selector label,
.custom-kind-input label {
font-weight: 600;
color: var(--text-color);
}
.kind-selector select,
.custom-kind-input input {
padding: 0.5em;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--input-bg);
color: var(--input-text-color);
font-size: 0.9em;
}
.recovery-results {
margin-top: 1.5rem;
}
.loading,
.no-events {
text-align: center;
padding: 2em;
color: var(--text-color);
opacity: 0.7;
}
.events-list {
display: flex;
flex-direction: column;
gap: 1em;
}
.event-item {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1em;
}
.event-item.old-version {
border-color: var(--warning);
background: var(--warning-bg);
}
.event-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1em;
flex-wrap: wrap;
gap: 1em;
}
.event-header-left {
display: flex;
flex-direction: column;
gap: 0.25em;
}
.event-kind {
font-weight: 600;
color: var(--primary);
}
.event-timestamp {
font-size: 0.9em;
color: var(--text-color);
opacity: 0.7;
}
.event-header-actions {
display: flex;
gap: 0.5em;
flex-wrap: wrap;
}
.repost-all-button,
.repost-button,
.copy-json-btn {
background: var(--accent-color);
color: var(--accent-hover-color);
border: none;
padding: 0.5em;
border-radius: 0.5em;
cursor: pointer;
font-size: 0.8em;
transition: background-color 0.2s;
}
.repost-all-button:hover,
.repost-button:hover,
.copy-json-btn:hover {
background: var(--accent-hover-color);
}
.event-content {
margin-top: 1em;
}
.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);
}
.load-more {
width: 100%;
padding: 12px;
background: var(--primary);
color: var(--text-color);
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
margin-top: 20px;
transition: background 0.2s ease;
}
.load-more:hover:not(:disabled) {
background: var(--accent-hover-color);
}
.load-more:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>