10 changed files with 869 additions and 3 deletions
@ -0,0 +1,431 @@
@@ -0,0 +1,431 @@
|
||||
<script lang="ts"> |
||||
import { nip19 } from 'nostr-tools'; |
||||
import type { NostrEvent } from '../types/nostr.js'; |
||||
import { nostrClient } from '../services/nostr/nostr-client.js'; |
||||
import { relayManager } from '../services/nostr/relay-manager.js'; |
||||
import EventJsonModal from './modals/EventJsonModal.svelte'; |
||||
import PublicationStatusModal from './modals/PublicationStatusModal.svelte'; |
||||
import { |
||||
isPinned, |
||||
isBookmarked, |
||||
isHighlighted, |
||||
togglePin, |
||||
toggleBookmark, |
||||
toggleHighlight |
||||
} from '../services/user-actions.js'; |
||||
import { eventMenuStore } from '../services/event-menu-store.js'; |
||||
|
||||
interface Props { |
||||
event: NostrEvent; |
||||
showContentActions?: boolean; // Show pin/bookmark/highlight for notes with content |
||||
} |
||||
|
||||
let { event, showContentActions = false }: Props = $props(); |
||||
|
||||
let menuOpen = $state(false); |
||||
let jsonModalOpen = $state(false); |
||||
let publicationModalOpen = $state(false); |
||||
let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null); |
||||
let broadcasting = $state(false); |
||||
let copied = $state<string | null>(null); |
||||
let menuButtonElement: HTMLButtonElement | null = $state(null); |
||||
let menuDropdownElement: HTMLDivElement | null = $state(null); |
||||
let menuPosition = $state({ top: 0, right: 0 }); |
||||
|
||||
// Unique ID for this menu instance |
||||
let menuId = $derived(event.id); |
||||
|
||||
// Check if this is a note with content (kind 1 or kind 11) |
||||
let isContentNote = $derived(event.kind === 1 || event.kind === 11); |
||||
|
||||
// Track pin/bookmark/highlight state |
||||
let pinnedState = $state(false); |
||||
let bookmarkedState = $state(false); |
||||
let highlightedState = $state(false); |
||||
|
||||
// Update state when event changes |
||||
$effect(() => { |
||||
pinnedState = isPinned(event.id); |
||||
bookmarkedState = isBookmarked(event.id); |
||||
highlightedState = isHighlighted(event.id); |
||||
}); |
||||
|
||||
function toggleMenu() { |
||||
if (menuOpen) { |
||||
closeMenu(); |
||||
} else { |
||||
openMenu(); |
||||
} |
||||
} |
||||
|
||||
function openMenu() { |
||||
menuOpen = true; |
||||
// Register this menu as open - this will close any other open menu |
||||
eventMenuStore.openMenu(menuId, closeMenu); |
||||
// Position menu after opening |
||||
requestAnimationFrame(() => { |
||||
positionMenu(); |
||||
}); |
||||
} |
||||
|
||||
function positionMenu() { |
||||
if (!menuButtonElement || !menuDropdownElement) return; |
||||
|
||||
const buttonRect = menuButtonElement.getBoundingClientRect(); |
||||
|
||||
// Calculate position - align right edge of dropdown with right edge of button |
||||
// Position below button by default |
||||
const top = buttonRect.bottom + 4; |
||||
const right = window.innerWidth - buttonRect.right; |
||||
|
||||
menuPosition = { top, right }; |
||||
|
||||
// Get actual dimensions after rendering |
||||
requestAnimationFrame(() => { |
||||
if (!menuDropdownElement) return; |
||||
const dropdownRect = menuDropdownElement.getBoundingClientRect(); |
||||
|
||||
// Adjust if menu would go off screen at bottom |
||||
if (dropdownRect.bottom > window.innerHeight) { |
||||
// Position above button if there's not enough space below |
||||
const spaceAbove = buttonRect.top; |
||||
const spaceBelow = window.innerHeight - buttonRect.bottom; |
||||
if (spaceAbove > spaceBelow) { |
||||
menuPosition.top = buttonRect.top - dropdownRect.height - 4; |
||||
} |
||||
} |
||||
|
||||
// Adjust if menu would go off screen to the left |
||||
if (dropdownRect.left < 0) { |
||||
menuPosition.right = window.innerWidth - buttonRect.left; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
function closeMenu() { |
||||
menuOpen = false; |
||||
eventMenuStore.closeMenu(menuId); |
||||
} |
||||
|
||||
// Close menu when clicking outside |
||||
function handleClickOutside(e: MouseEvent) { |
||||
const target = e.target as HTMLElement; |
||||
if (!target.closest('.event-menu-container') && !target.closest('.menu-dropdown')) { |
||||
eventMenuStore.closeCurrentMenu(); |
||||
} |
||||
} |
||||
|
||||
$effect(() => { |
||||
if (menuOpen) { |
||||
document.addEventListener('click', handleClickOutside); |
||||
window.addEventListener('scroll', closeMenu, true); |
||||
window.addEventListener('resize', positionMenu); |
||||
// Reposition on scroll to keep menu aligned |
||||
const handleScroll = () => { |
||||
if (menuOpen) { |
||||
positionMenu(); |
||||
} |
||||
}; |
||||
window.addEventListener('scroll', handleScroll, true); |
||||
return () => { |
||||
document.removeEventListener('click', handleClickOutside); |
||||
window.removeEventListener('scroll', closeMenu, true); |
||||
window.removeEventListener('resize', positionMenu); |
||||
window.removeEventListener('scroll', handleScroll, true); |
||||
}; |
||||
} |
||||
}); |
||||
|
||||
async function copyUserId() { |
||||
try { |
||||
const npub = nip19.npubEncode(event.pubkey); |
||||
await navigator.clipboard.writeText(npub); |
||||
copied = 'npub'; |
||||
setTimeout(() => { |
||||
copied = null; |
||||
}, 2000); |
||||
closeMenu(); |
||||
} catch (error) { |
||||
console.error('Failed to copy user ID:', error); |
||||
} |
||||
} |
||||
|
||||
async function copyEventId() { |
||||
try { |
||||
const nevent = nip19.neventEncode({ |
||||
id: event.id, |
||||
relays: [] |
||||
}); |
||||
await navigator.clipboard.writeText(nevent); |
||||
copied = 'nevent'; |
||||
setTimeout(() => { |
||||
copied = null; |
||||
}, 2000); |
||||
closeMenu(); |
||||
} catch (error) { |
||||
console.error('Failed to copy event ID:', error); |
||||
} |
||||
} |
||||
|
||||
function viewJson() { |
||||
jsonModalOpen = true; |
||||
closeMenu(); |
||||
} |
||||
|
||||
async function broadcastEvent() { |
||||
broadcasting = true; |
||||
closeMenu(); |
||||
|
||||
try { |
||||
// Get all available relays for broadcasting |
||||
const relays = relayManager.getPublishRelays( |
||||
[...relayManager.getThreadReadRelays(), ...relayManager.getFeedReadRelays()], |
||||
true |
||||
); |
||||
|
||||
const results = await nostrClient.publish(event, { relays }); |
||||
publicationResults = results; |
||||
publicationModalOpen = true; |
||||
} catch (error) { |
||||
console.error('Error broadcasting event:', error); |
||||
publicationResults = { |
||||
success: [], |
||||
failed: [{ relay: 'Unknown', error: error instanceof Error ? error.message : 'Unknown error' }] |
||||
}; |
||||
publicationModalOpen = true; |
||||
} finally { |
||||
broadcasting = false; |
||||
} |
||||
} |
||||
|
||||
async function shareWithAitherboard() { |
||||
try { |
||||
const url = `${window.location.origin}/thread/${event.id}`; |
||||
await navigator.clipboard.writeText(url); |
||||
copied = 'share'; |
||||
setTimeout(() => { |
||||
copied = null; |
||||
}, 2000); |
||||
closeMenu(); |
||||
} catch (error) { |
||||
console.error('Failed to copy share URL:', error); |
||||
} |
||||
} |
||||
|
||||
function pinNote() { |
||||
pinnedState = togglePin(event.id); |
||||
closeMenu(); |
||||
} |
||||
|
||||
function bookmarkNote() { |
||||
bookmarkedState = toggleBookmark(event.id); |
||||
closeMenu(); |
||||
} |
||||
|
||||
function highlightNote() { |
||||
highlightedState = toggleHighlight(event.id); |
||||
closeMenu(); |
||||
} |
||||
</script> |
||||
|
||||
<div class="event-menu-container"> |
||||
<button |
||||
bind:this={menuButtonElement} |
||||
class="menu-button" |
||||
onclick={toggleMenu} |
||||
aria-label="Event menu" |
||||
aria-expanded={menuOpen} |
||||
> |
||||
<span class="menu-icon">⋯</span> |
||||
</button> |
||||
|
||||
{#if menuOpen} |
||||
<div |
||||
bind:this={menuDropdownElement} |
||||
class="menu-dropdown" |
||||
style="top: {menuPosition.top}px; right: {menuPosition.right}px;" |
||||
> |
||||
<button class="menu-item" onclick={copyUserId}> |
||||
Copy user ID |
||||
{#if copied === 'npub'} |
||||
<span class="copied-indicator">✓</span> |
||||
{/if} |
||||
</button> |
||||
<button class="menu-item" onclick={copyEventId}> |
||||
Copy event ID |
||||
{#if copied === 'nevent'} |
||||
<span class="copied-indicator">✓</span> |
||||
{/if} |
||||
</button> |
||||
<button class="menu-item" onclick={viewJson}> |
||||
View JSON |
||||
</button> |
||||
<button class="menu-item" onclick={broadcastEvent} disabled={broadcasting}> |
||||
{broadcasting ? 'Broadcasting...' : 'Broadcast event'} |
||||
</button> |
||||
<button class="menu-item" onclick={shareWithAitherboard}> |
||||
Share with aitherboard |
||||
{#if copied === 'share'} |
||||
<span class="copied-indicator">✓</span> |
||||
{/if} |
||||
</button> |
||||
|
||||
{#if showContentActions && isContentNote} |
||||
<div class="menu-divider"></div> |
||||
<button class="menu-item" onclick={pinNote} class:active={pinnedState}> |
||||
Pin note |
||||
{#if pinnedState} |
||||
<span class="action-indicator">✓</span> |
||||
{/if} |
||||
</button> |
||||
<button class="menu-item" onclick={bookmarkNote} class:active={bookmarkedState}> |
||||
Bookmark note |
||||
{#if bookmarkedState} |
||||
<span class="action-indicator">✓</span> |
||||
{/if} |
||||
</button> |
||||
<button class="menu-item" onclick={highlightNote} class:active={highlightedState}> |
||||
Highlight note |
||||
{#if highlightedState} |
||||
<span class="action-indicator">✓</span> |
||||
{/if} |
||||
</button> |
||||
{/if} |
||||
</div> |
||||
{/if} |
||||
</div> |
||||
|
||||
<EventJsonModal bind:open={jsonModalOpen} bind:event /> |
||||
<PublicationStatusModal bind:open={publicationModalOpen} bind:results={publicationResults} /> |
||||
|
||||
<style> |
||||
.event-menu-container { |
||||
position: relative; |
||||
display: inline-block; |
||||
/* Ensure menu can overflow container */ |
||||
overflow: visible; |
||||
} |
||||
|
||||
.menu-button { |
||||
background: none; |
||||
border: none; |
||||
cursor: pointer; |
||||
padding: 0.25rem 0.5rem; |
||||
color: var(--fog-text-light, #9ca3af); |
||||
font-size: 1.25rem; |
||||
line-height: 1; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
border-radius: 4px; |
||||
transition: background-color 0.2s, color 0.2s; |
||||
} |
||||
|
||||
.menu-button:hover { |
||||
background: var(--fog-highlight, #f3f4f6); |
||||
color: var(--fog-text, #1f2937); |
||||
} |
||||
|
||||
:global(.dark) .menu-button { |
||||
color: var(--fog-dark-text-light, #6b7280); |
||||
} |
||||
|
||||
:global(.dark) .menu-button:hover { |
||||
background: var(--fog-dark-highlight, #374151); |
||||
color: var(--fog-dark-text, #f1f5f9); |
||||
} |
||||
|
||||
.menu-icon { |
||||
user-select: none; |
||||
transform: rotate(90deg); |
||||
display: inline-block; |
||||
} |
||||
|
||||
.menu-dropdown { |
||||
position: fixed; |
||||
background: var(--fog-post, #ffffff); |
||||
border: 1px solid var(--fog-border, #e5e7eb); |
||||
border-radius: 6px; |
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); |
||||
min-width: 200px; |
||||
z-index: 1000; |
||||
overflow: visible; |
||||
} |
||||
|
||||
:global(.dark) .menu-dropdown { |
||||
background: var(--fog-dark-post, #1f2937); |
||||
border-color: var(--fog-dark-border, #374151); |
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); |
||||
} |
||||
|
||||
.menu-item { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
width: 100%; |
||||
padding: 0.5rem 0.75rem; |
||||
background: none; |
||||
border: none; |
||||
text-align: left; |
||||
cursor: pointer; |
||||
font-size: 0.875rem; |
||||
color: var(--fog-text, #1f2937); |
||||
transition: background-color 0.2s; |
||||
} |
||||
|
||||
.menu-item:hover:not(:disabled) { |
||||
background: var(--fog-highlight, #f3f4f6); |
||||
} |
||||
|
||||
.menu-item:disabled { |
||||
opacity: 0.6; |
||||
cursor: not-allowed; |
||||
} |
||||
|
||||
.menu-item.active { |
||||
background: var(--fog-highlight, #f3f4f6); |
||||
font-weight: 500; |
||||
} |
||||
|
||||
:global(.dark) .menu-item { |
||||
color: var(--fog-dark-text, #f1f5f9); |
||||
} |
||||
|
||||
:global(.dark) .menu-item:hover:not(:disabled) { |
||||
background: var(--fog-dark-highlight, #374151); |
||||
} |
||||
|
||||
:global(.dark) .menu-item.active { |
||||
background: var(--fog-dark-highlight, #374151); |
||||
} |
||||
|
||||
.action-indicator { |
||||
color: var(--fog-accent, #64748b); |
||||
font-weight: 600; |
||||
margin-left: 0.5rem; |
||||
} |
||||
|
||||
:global(.dark) .action-indicator { |
||||
color: var(--fog-dark-accent, #94a3b8); |
||||
} |
||||
|
||||
.menu-divider { |
||||
height: 1px; |
||||
background: var(--fog-border, #e5e7eb); |
||||
margin: 0.25rem 0; |
||||
} |
||||
|
||||
:global(.dark) .menu-divider { |
||||
background: var(--fog-dark-border, #374151); |
||||
} |
||||
|
||||
.copied-indicator { |
||||
color: var(--fog-accent, #64748b); |
||||
font-weight: 600; |
||||
margin-left: 0.5rem; |
||||
} |
||||
|
||||
:global(.dark) .copied-indicator { |
||||
color: var(--fog-dark-accent, #94a3b8); |
||||
} |
||||
</style> |
||||
@ -0,0 +1,214 @@
@@ -0,0 +1,214 @@
|
||||
<script lang="ts"> |
||||
import type { NostrEvent } from '../../types/nostr.js'; |
||||
|
||||
interface Props { |
||||
open?: boolean; |
||||
event?: NostrEvent | null; |
||||
} |
||||
|
||||
let { open = $bindable(false), event = $bindable(null) }: Props = $props(); |
||||
let jsonText = $derived(event ? JSON.stringify(event, null, 2) : ''); |
||||
let copied = $state(false); |
||||
|
||||
function close() { |
||||
open = false; |
||||
} |
||||
|
||||
async function copyJson() { |
||||
if (!jsonText) return; |
||||
try { |
||||
await navigator.clipboard.writeText(jsonText); |
||||
copied = true; |
||||
setTimeout(() => { |
||||
copied = false; |
||||
}, 2000); |
||||
} catch (error) { |
||||
console.error('Failed to copy JSON:', error); |
||||
} |
||||
} |
||||
|
||||
function selectAll() { |
||||
const textarea = document.querySelector('.json-textarea') as HTMLTextAreaElement; |
||||
if (textarea) { |
||||
textarea.select(); |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
{#if open && event} |
||||
<div |
||||
class="modal-overlay" |
||||
onclick={close} |
||||
onkeydown={(e) => e.key === 'Escape' && close()} |
||||
role="dialog" |
||||
aria-modal="true" |
||||
tabindex="-1" |
||||
> |
||||
<div class="modal-content" onclick={(e) => e.stopPropagation()}> |
||||
<div class="modal-header"> |
||||
<h2>Event JSON</h2> |
||||
<button onclick={close} class="close-button">×</button> |
||||
</div> |
||||
|
||||
<div class="modal-body"> |
||||
<textarea |
||||
class="json-textarea" |
||||
readonly |
||||
value={jsonText} |
||||
onclick={selectAll} |
||||
></textarea> |
||||
</div> |
||||
|
||||
<div class="modal-footer"> |
||||
<button onclick={copyJson} class="copy-button"> |
||||
{copied ? 'Copied!' : 'Copy'} |
||||
</button> |
||||
<button onclick={close}>Close</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{/if} |
||||
|
||||
<style> |
||||
.modal-overlay { |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
background: rgba(15, 23, 42, 0.4); |
||||
backdrop-filter: blur(4px); |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
z-index: 1000; |
||||
} |
||||
|
||||
.modal-content { |
||||
background: var(--fog-post, #ffffff); |
||||
border: 1px solid var(--fog-border, #e5e7eb); |
||||
border-radius: 8px; |
||||
max-width: 800px; |
||||
width: 90%; |
||||
max-height: 80vh; |
||||
display: flex; |
||||
flex-direction: column; |
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
:global(.dark) .modal-content { |
||||
background: var(--fog-dark-post, #1f2937); |
||||
border-color: var(--fog-dark-border, #374151); |
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); |
||||
} |
||||
|
||||
.modal-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
padding: 1rem; |
||||
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
||||
} |
||||
|
||||
:global(.dark) .modal-header { |
||||
border-bottom-color: var(--fog-dark-border, #374151); |
||||
} |
||||
|
||||
.modal-header h2 { |
||||
margin: 0; |
||||
font-size: 1.25rem; |
||||
color: var(--fog-text, #1f2937); |
||||
} |
||||
|
||||
:global(.dark) .modal-header h2 { |
||||
color: var(--fog-dark-text, #f1f5f9); |
||||
} |
||||
|
||||
.close-button { |
||||
background: none; |
||||
border: none; |
||||
font-size: 1.5rem; |
||||
cursor: pointer; |
||||
padding: 0; |
||||
width: 2rem; |
||||
height: 2rem; |
||||
color: var(--fog-text, #1f2937); |
||||
} |
||||
|
||||
:global(.dark) .close-button { |
||||
color: var(--fog-dark-text, #f1f5f9); |
||||
} |
||||
|
||||
.modal-body { |
||||
padding: 1rem; |
||||
flex: 1; |
||||
overflow: auto; |
||||
} |
||||
|
||||
.json-textarea { |
||||
width: 100%; |
||||
min-height: 400px; |
||||
font-family: 'Courier New', monospace; |
||||
font-size: 0.875rem; |
||||
padding: 0.75rem; |
||||
border: 1px solid var(--fog-border, #e5e7eb); |
||||
border-radius: 4px; |
||||
background: var(--fog-bg, #ffffff); |
||||
color: var(--fog-text, #1f2937); |
||||
resize: vertical; |
||||
} |
||||
|
||||
:global(.dark) .json-textarea { |
||||
background: var(--fog-dark-bg, #0f172a); |
||||
border-color: var(--fog-dark-border, #374151); |
||||
color: var(--fog-dark-text, #f1f5f9); |
||||
} |
||||
|
||||
.modal-footer { |
||||
padding: 1rem; |
||||
border-top: 1px solid var(--fog-border, #e5e7eb); |
||||
display: flex; |
||||
justify-content: flex-end; |
||||
gap: 0.5rem; |
||||
} |
||||
|
||||
:global(.dark) .modal-footer { |
||||
border-top-color: var(--fog-dark-border, #374151); |
||||
} |
||||
|
||||
.modal-footer button { |
||||
padding: 0.5rem 1rem; |
||||
border: none; |
||||
border-radius: 4px; |
||||
cursor: pointer; |
||||
transition: background-color 0.2s; |
||||
font-size: 0.875rem; |
||||
} |
||||
|
||||
.copy-button { |
||||
background: var(--fog-accent, #64748b); |
||||
color: white; |
||||
} |
||||
|
||||
.copy-button:hover { |
||||
background: var(--fog-accent-dark, #475569); |
||||
} |
||||
|
||||
.modal-footer button:not(.copy-button) { |
||||
background: var(--fog-border, #e5e7eb); |
||||
color: var(--fog-text, #1f2937); |
||||
} |
||||
|
||||
.modal-footer button:not(.copy-button):hover { |
||||
background: var(--fog-highlight, #f3f4f6); |
||||
} |
||||
|
||||
:global(.dark) .modal-footer button:not(.copy-button) { |
||||
background: var(--fog-dark-border, #374151); |
||||
color: var(--fog-dark-text, #f1f5f9); |
||||
} |
||||
|
||||
:global(.dark) .modal-footer button:not(.copy-button):hover { |
||||
background: var(--fog-dark-highlight, #374151); |
||||
} |
||||
</style> |
||||
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
/** |
||||
* Global store for managing which EventMenu is currently open |
||||
* Ensures only one menu is open at a time |
||||
*/ |
||||
|
||||
type CloseCallback = () => void; |
||||
|
||||
class EventMenuStore { |
||||
private openMenuId: string | null = null; |
||||
private closeCallback: CloseCallback | null = null; |
||||
|
||||
/** |
||||
* Register a menu as open |
||||
* @param menuId Unique identifier for the menu (typically event.id) |
||||
* @param closeCallback Function to call to close this menu |
||||
*/ |
||||
openMenu(menuId: string, closeCallback: CloseCallback): void { |
||||
// Close any previously open menu
|
||||
if (this.openMenuId && this.openMenuId !== menuId && this.closeCallback) { |
||||
this.closeCallback(); |
||||
} |
||||
|
||||
this.openMenuId = menuId; |
||||
this.closeCallback = closeCallback; |
||||
} |
||||
|
||||
/** |
||||
* Unregister a menu as open |
||||
* @param menuId Unique identifier for the menu |
||||
*/ |
||||
closeMenu(menuId: string): void { |
||||
if (this.openMenuId === menuId) { |
||||
this.openMenuId = null; |
||||
this.closeCallback = null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Check if a specific menu is open |
||||
*/ |
||||
isOpen(menuId: string): boolean { |
||||
return this.openMenuId === menuId; |
||||
} |
||||
|
||||
/** |
||||
* Close the currently open menu (if any) |
||||
*/ |
||||
closeCurrentMenu(): void { |
||||
if (this.closeCallback) { |
||||
this.closeCallback(); |
||||
} |
||||
this.openMenuId = null; |
||||
this.closeCallback = null; |
||||
} |
||||
} |
||||
|
||||
export const eventMenuStore = new EventMenuStore(); |
||||
@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
|
||||
/** |
||||
* User actions service - manages pinned, bookmarked, and highlighted events |
||||
* Stores in localStorage for persistence |
||||
*/ |
||||
|
||||
const STORAGE_KEY_PINNED = 'aitherboard_pinned_events'; |
||||
const STORAGE_KEY_BOOKMARKED = 'aitherboard_bookmarked_events'; |
||||
const STORAGE_KEY_HIGHLIGHTED = 'aitherboard_highlighted_events'; |
||||
|
||||
/** |
||||
* Get all pinned event IDs |
||||
*/ |
||||
export function getPinnedEvents(): Set<string> { |
||||
if (typeof window === 'undefined') return new Set(); |
||||
try { |
||||
const stored = localStorage.getItem(STORAGE_KEY_PINNED); |
||||
if (!stored) return new Set(); |
||||
const ids = JSON.parse(stored) as string[]; |
||||
return new Set(ids); |
||||
} catch { |
||||
return new Set(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get all bookmarked event IDs |
||||
*/ |
||||
export function getBookmarkedEvents(): Set<string> { |
||||
if (typeof window === 'undefined') return new Set(); |
||||
try { |
||||
const stored = localStorage.getItem(STORAGE_KEY_BOOKMARKED); |
||||
if (!stored) return new Set(); |
||||
const ids = JSON.parse(stored) as string[]; |
||||
return new Set(ids); |
||||
} catch { |
||||
return new Set(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get all highlighted event IDs |
||||
*/ |
||||
export function getHighlightedEvents(): Set<string> { |
||||
if (typeof window === 'undefined') return new Set(); |
||||
try { |
||||
const stored = localStorage.getItem(STORAGE_KEY_HIGHLIGHTED); |
||||
if (!stored) return new Set(); |
||||
const ids = JSON.parse(stored) as string[]; |
||||
return new Set(ids); |
||||
} catch { |
||||
return new Set(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Check if an event is pinned |
||||
*/ |
||||
export function isPinned(eventId: string): boolean { |
||||
return getPinnedEvents().has(eventId); |
||||
} |
||||
|
||||
/** |
||||
* Check if an event is bookmarked |
||||
*/ |
||||
export function isBookmarked(eventId: string): boolean { |
||||
return getBookmarkedEvents().has(eventId); |
||||
} |
||||
|
||||
/** |
||||
* Check if an event is highlighted |
||||
*/ |
||||
export function isHighlighted(eventId: string): boolean { |
||||
return getHighlightedEvents().has(eventId); |
||||
} |
||||
|
||||
/** |
||||
* Toggle pin status of an event |
||||
*/ |
||||
export function togglePin(eventId: string): boolean { |
||||
const pinned = getPinnedEvents(); |
||||
const isCurrentlyPinned = pinned.has(eventId); |
||||
|
||||
if (isCurrentlyPinned) { |
||||
pinned.delete(eventId); |
||||
} else { |
||||
pinned.add(eventId); |
||||
} |
||||
|
||||
try { |
||||
localStorage.setItem(STORAGE_KEY_PINNED, JSON.stringify(Array.from(pinned))); |
||||
return !isCurrentlyPinned; |
||||
} catch (error) { |
||||
console.error('Failed to save pinned events:', error); |
||||
return isCurrentlyPinned; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Toggle bookmark status of an event |
||||
*/ |
||||
export function toggleBookmark(eventId: string): boolean { |
||||
const bookmarked = getBookmarkedEvents(); |
||||
const isCurrentlyBookmarked = bookmarked.has(eventId); |
||||
|
||||
if (isCurrentlyBookmarked) { |
||||
bookmarked.delete(eventId); |
||||
} else { |
||||
bookmarked.add(eventId); |
||||
} |
||||
|
||||
try { |
||||
localStorage.setItem(STORAGE_KEY_BOOKMARKED, JSON.stringify(Array.from(bookmarked))); |
||||
return !isCurrentlyBookmarked; |
||||
} catch (error) { |
||||
console.error('Failed to save bookmarked events:', error); |
||||
return isCurrentlyBookmarked; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Toggle highlight status of an event |
||||
*/ |
||||
export function toggleHighlight(eventId: string): boolean { |
||||
const highlighted = getHighlightedEvents(); |
||||
const isCurrentlyHighlighted = highlighted.has(eventId); |
||||
|
||||
if (isCurrentlyHighlighted) { |
||||
highlighted.delete(eventId); |
||||
} else { |
||||
highlighted.add(eventId); |
||||
} |
||||
|
||||
try { |
||||
localStorage.setItem(STORAGE_KEY_HIGHLIGHTED, JSON.stringify(Array.from(highlighted))); |
||||
return !isCurrentlyHighlighted; |
||||
} catch (error) { |
||||
console.error('Failed to save highlighted events:', error); |
||||
return isCurrentlyHighlighted; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue