10 changed files with 474 additions and 142 deletions
@ -0,0 +1,206 @@
@@ -0,0 +1,206 @@
|
||||
<script lang="ts"> |
||||
import { |
||||
ClipboardCheckOutline, |
||||
ClipboardCleanOutline, |
||||
CodeOutline, |
||||
DotsVerticalOutline, |
||||
EyeOutline, |
||||
ShareNodesOutline |
||||
} from "flowbite-svelte-icons"; |
||||
import { Button, Modal, Popover } from "flowbite-svelte"; |
||||
import { standardRelays } from "$lib/consts"; |
||||
import { neventEncode } from "$lib/utils"; |
||||
import { type AddressPointer, naddrEncode } from "nostr-tools/nip19"; |
||||
import InlineProfile from "$components/util/InlineProfile.svelte"; |
||||
|
||||
let { event } = $props(); |
||||
|
||||
let jsonModalOpen: boolean = $state(false); |
||||
let detailsModalOpen: boolean = $state(false); |
||||
let eventIdCopied: boolean = $state(false); |
||||
let shareLinkCopied: boolean = $state(false); |
||||
let title: string = $derived(event.getMatchingTags('title')[0]?.[1]); |
||||
let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown'); |
||||
let version: string = $derived(event.getMatchingTags('version')[0]?.[1] ?? '1'); |
||||
let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null); |
||||
let originalAuthor: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null); |
||||
let summary: string = $derived(event.getMatchingTags('summary')[0]?.[1] ?? null); |
||||
let type: string = $derived(event.getMatchingTags('type')[0]?.[1] ?? null); |
||||
let language: string = $derived(event.getMatchingTags('l')[0]?.[1] ?? null); |
||||
let source: string = $derived(event.getMatchingTags('source')[0]?.[1] ?? null); |
||||
let publisher: string = $derived(event.getMatchingTags('published_by')[0]?.[1] ?? null); |
||||
let identifier: string = $derived(event.getMatchingTags('i')[0]?.[1] ?? null); |
||||
|
||||
let isOpen = $state(false); |
||||
|
||||
function openPopover() { |
||||
isOpen = true; |
||||
} |
||||
|
||||
function closePopover() { |
||||
isOpen = false; |
||||
const menu = document.getElementById('dots-' + event.id); |
||||
if (menu) menu.blur(); |
||||
} |
||||
|
||||
function shareNjump() { |
||||
const relays: string[] = standardRelays; |
||||
const dTag : string | undefined = event.dTag; |
||||
|
||||
if (typeof dTag === 'string') { |
||||
const opts: AddressPointer = { |
||||
identifier: dTag, |
||||
pubkey: event.pubkey, |
||||
kind: 30040, |
||||
relays |
||||
}; |
||||
const naddr = naddrEncode(opts); |
||||
console.debug(naddr); |
||||
navigator.clipboard.writeText(`https://njump.me/${naddr}`); |
||||
shareLinkCopied = true; |
||||
setTimeout(() => { |
||||
shareLinkCopied = false; |
||||
}, 4000); |
||||
} |
||||
|
||||
else { |
||||
console.log('dTag is undefined'); |
||||
} |
||||
} |
||||
|
||||
function copyEventId() { |
||||
console.debug("copyEventID"); |
||||
const relays: string[] = standardRelays; |
||||
const nevent = neventEncode(event, relays); |
||||
|
||||
navigator.clipboard.writeText(nevent); |
||||
|
||||
eventIdCopied = true; |
||||
setTimeout(() => { |
||||
eventIdCopied = false; |
||||
}, 4000); |
||||
} |
||||
|
||||
function viewJson() { |
||||
console.debug("viewJSON"); |
||||
jsonModalOpen = true; |
||||
} |
||||
|
||||
function viewDetails() { |
||||
console.log('Details'); |
||||
detailsModalOpen = true; |
||||
} |
||||
|
||||
</script> |
||||
|
||||
<div class="group" role="group" onmouseenter={openPopover}> |
||||
<!-- Main button --> |
||||
<Button type="button" |
||||
id="dots-{event.id}" |
||||
class=" hover:bg-primary-0 dark:text-highlight dark:hover:bg-primary-800 p-1 dots" color="none" |
||||
data-popover-target="popover-actions"> |
||||
<DotsVerticalOutline class="h-6 w-6"/> |
||||
<span class="sr-only">Open actions menu</span> |
||||
</Button> |
||||
|
||||
{#if isOpen} |
||||
<Popover id="popover-actions" |
||||
placement="bottom" |
||||
trigger="click" |
||||
class='popover-leather w-fit z-10' |
||||
onmouseleave={closePopover} |
||||
> |
||||
<div class='flex flex-row justify-between space-x-4'> |
||||
<div class='flex flex-col text-nowrap'> |
||||
<ul class="space-y-2"> |
||||
<li> |
||||
<a href="" role="button" class='btn-leather' onclick={viewDetails}> |
||||
<EyeOutline class="inline mr-2" /> View details |
||||
</a> |
||||
</li> |
||||
<li> |
||||
<a role="button" class='btn-leather' onclick={shareNjump}> |
||||
{#if shareLinkCopied} |
||||
<ClipboardCheckOutline class="inline mr-2" /> Copied! |
||||
{:else} |
||||
<ShareNodesOutline class="inline mr-2" /> Share via NJump |
||||
{/if} |
||||
</a> |
||||
</li> |
||||
<li> |
||||
<a role="button" class='btn-leather' onclick={copyEventId}> |
||||
{#if eventIdCopied} |
||||
<ClipboardCheckOutline class="inline mr-2" /> Copied! |
||||
{:else} |
||||
<ClipboardCleanOutline class="inline mr-2" /> Copy event ID |
||||
{/if} |
||||
</a> |
||||
</li> |
||||
<li> |
||||
<a href="" role="button" class='btn-leather' onclick={viewJson}> |
||||
<CodeOutline class="inline mr-2" /> View JSON |
||||
</a> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
</Popover> |
||||
{/if} |
||||
<!-- Event JSON --> |
||||
<Modal class='modal-leather' title='Event JSON' bind:open={jsonModalOpen} autoclose outsideclose size='sm'> |
||||
<div class="overflow-auto bg-highlight dark:bg-primary-900 text-sm rounded p-1"> |
||||
<code>{JSON.stringify(event.rawEvent())}</code> |
||||
</div> |
||||
</Modal> |
||||
<!-- Event details --> |
||||
<Modal class='modal-leather' title='Publication details' bind:open={detailsModalOpen} autoclose outsideclose size='sm'> |
||||
<div class="flex flex-row space-x-4"> |
||||
{#if image} |
||||
<div class="flex col"> |
||||
<img class="max-w-48" src={image} /> |
||||
</div> |
||||
{/if} |
||||
<div class="flex flex-col col space-y-5 justify-center align-middle"> |
||||
<h1 class="text-3xl font-bold mt-5">{title}</h1> |
||||
<h2 class="text-base font-bold">by |
||||
{#if originalAuthor !== null} |
||||
<InlineProfile pubkey={originalAuthor} title={author} /> |
||||
{:else} |
||||
{author} |
||||
{/if} |
||||
</h2> |
||||
<h4 class='text-base font-thin mt-2'>Version: {version}</h4> |
||||
</div> |
||||
</div> |
||||
|
||||
{#if summary} |
||||
<div class="flex flex-row "> |
||||
<p class='text-base text-primary-900 dark:text-highlight'>{summary}</p> |
||||
</div> |
||||
{/if} |
||||
|
||||
<div class="flex flex-row "> |
||||
<h4 class='text-base font-normal mt-2'>Index author: <InlineProfile pubkey={event.pubkey} /></h4> |
||||
</div> |
||||
|
||||
<div class="flex flex-col pb-4 space-y-1"> |
||||
{#if source !== null} |
||||
<h5 class="text-sm">Source: <a class="underline" href={source} target="_blank">{source}</a></h5> |
||||
{/if} |
||||
{#if type !== null} |
||||
<h5 class="text-sm">Publication type: {type}</h5> |
||||
{/if} |
||||
{#if language !== null} |
||||
<h5 class="text-sm">Language: {language}</h5> |
||||
{/if} |
||||
{#if publisher !== null} |
||||
<h5 class="text-sm">Published by: {publisher}</h5> |
||||
{/if} |
||||
{#if identifier !== null} |
||||
<h5 class="text-sm">{identifier}</h5> |
||||
{/if} |
||||
|
||||
</div> |
||||
|
||||
</Modal> |
||||
</div> |
||||
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
<script lang='ts'> |
||||
import { ClipboardCheckOutline, ClipboardCleanOutline } from "flowbite-svelte-icons"; |
||||
|
||||
let { displayText, copyText = displayText} = $props(); |
||||
|
||||
let copied: boolean = $state(false); |
||||
|
||||
async function copyToClipboard() { |
||||
try { |
||||
await navigator.clipboard.writeText(copyText); |
||||
copied = true; |
||||
setTimeout(() => { |
||||
copied = false; |
||||
}, 4000); |
||||
} catch (err) { |
||||
console.error("Failed to copy: ", err); |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<a role="button" class='btn-leather text-nowrap' onclick={copyToClipboard}> |
||||
{#if copied} |
||||
<ClipboardCheckOutline class="!fill-none dark:!fill-none inline mr-1" /> Copied! |
||||
{:else} |
||||
<ClipboardCleanOutline class="!fill-none dark:!fill-none inline mr-1" /> {displayText} |
||||
{/if} |
||||
</a> |
||||
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
<script lang='ts'> |
||||
import { Avatar } from 'flowbite-svelte'; |
||||
import { type NDKUserProfile } from "@nostr-dev-kit/ndk"; |
||||
import { ndkInstance } from '$lib/ndk'; |
||||
|
||||
let { pubkey, title = null } = $props(); |
||||
|
||||
const externalProfileDestination = 'https://njump.me/' |
||||
let loading = $state(true); |
||||
let anon = $state(false); |
||||
let npub = $state(''); |
||||
|
||||
let profile = $state<NDKUserProfile | null>(null); |
||||
let pfp = $derived(profile?.image); |
||||
let username = $derived(profile?.name); |
||||
|
||||
async function fetchUserData(pubkey: string) { |
||||
let user; |
||||
user = $ndkInstance |
||||
.getUser({ pubkey: pubkey ?? undefined }); |
||||
|
||||
npub = user.npub; |
||||
|
||||
user.fetchProfile() |
||||
.then(userProfile => { |
||||
profile = userProfile; |
||||
if (!profile?.name) anon = true; |
||||
loading = false; |
||||
}); |
||||
} |
||||
|
||||
// Fetch data when component mounts |
||||
$effect(() => { |
||||
if (pubkey) { |
||||
fetchUserData(pubkey); |
||||
} |
||||
}); |
||||
|
||||
function shortenNpub(long: string|undefined) { |
||||
if (!long) return ''; |
||||
return long.slice(0, 8) + '…' + long.slice(-4); |
||||
} |
||||
</script> |
||||
|
||||
{#if loading} |
||||
{title ?? '…'} |
||||
{:else if anon } |
||||
<a class='underline' href='{externalProfileDestination}{npub}' title={title ?? npub} target='_blank'>{shortenNpub(npub)}</a> |
||||
{:else if npub } |
||||
<a href='{externalProfileDestination}{npub}' title={title ?? username} target='_blank'> |
||||
<Avatar rounded |
||||
class='h-6 w-6 mx-1 cursor-pointer inline' |
||||
src={pfp} |
||||
alt={username} /> |
||||
<span class='underline'>{username ?? shortenNpub(npub)}</span> |
||||
</a> |
||||
{:else} |
||||
{title ?? pubkey} |
||||
{/if} |
||||
@ -0,0 +1,101 @@
@@ -0,0 +1,101 @@
|
||||
<script lang='ts'> |
||||
import CopyToClipboard from "$components/util/CopyToClipboard.svelte"; |
||||
import { logout, ndkInstance } from '$lib/ndk'; |
||||
import { ArrowRightToBracketOutline, UserOutline, FileSearchOutline } from "flowbite-svelte-icons"; |
||||
import { Avatar, Popover } from "flowbite-svelte"; |
||||
import type { NDKUserProfile } from "@nostr-dev-kit/ndk"; |
||||
|
||||
const externalProfileDestination = 'https://njump.me/' |
||||
|
||||
let { pubkey, isNav = false } = $props(); |
||||
|
||||
let profile = $state<NDKUserProfile | null>(null); |
||||
let pfp = $derived(profile?.image); |
||||
let username = $derived(profile?.name); |
||||
let tag = $derived(profile?.name); |
||||
let npub = $state<string | undefined >(undefined); |
||||
|
||||
$effect(() => { |
||||
const user = $ndkInstance |
||||
.getUser({ pubkey: pubkey ?? undefined }); |
||||
|
||||
npub = user.npub; |
||||
|
||||
user.fetchProfile() |
||||
.then(userProfile => { |
||||
profile = userProfile; |
||||
}); |
||||
}); |
||||
|
||||
async function handleSignOutClick() { |
||||
logout($ndkInstance.activeUser!); |
||||
profile = null; |
||||
} |
||||
|
||||
function shortenNpub(long: string|undefined) { |
||||
if (!long) return ''; |
||||
return long.slice(0, 8) + '…' + long.slice(-4); |
||||
} |
||||
</script> |
||||
|
||||
<div class="relative"> |
||||
{#if profile} |
||||
<div class="group"> |
||||
<Avatar |
||||
rounded |
||||
class='h-6 w-6 cursor-pointer' |
||||
src={pfp} |
||||
alt={username} |
||||
/> |
||||
{#key username || tag} |
||||
<Popover |
||||
target="avatar" |
||||
class='popover-leather w-[180px]' |
||||
trigger='hover' |
||||
> |
||||
<div class='flex flex-row justify-between space-x-4'> |
||||
<div class='flex flex-col'> |
||||
{#if username} |
||||
<h3 class='text-lg font-bold'>{username}</h3> |
||||
{#if isNav}<h4 class='text-base'>@{tag}</h4>{/if} |
||||
{/if} |
||||
<ul class="space-y-2 mt-2"> |
||||
<li> |
||||
<CopyToClipboard displayText={shortenNpub(npub)} copyText={npub} /> |
||||
</li> |
||||
<li> |
||||
<a class='hover:text-primary-400 dark:hover:text-primary-500 text-nowrap mt-3 m-0' href='{externalProfileDestination}{npub}' target='_blank'> |
||||
<UserOutline class='mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none' /><span class='underline'>View profile</span> |
||||
</a> |
||||
</li> |
||||
{#if isNav} |
||||
<li> |
||||
<a |
||||
href="" |
||||
id='sign-out-button' |
||||
class='btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500' |
||||
onclick={handleSignOutClick} |
||||
role="button" |
||||
> |
||||
<ArrowRightToBracketOutline class='mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none' /> Sign out |
||||
</a> |
||||
</li> |
||||
{:else} |
||||
<!-- li> |
||||
<a |
||||
href="" |
||||
class='btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500' |
||||
role="button" |
||||
> |
||||
<FileSearchOutline class='mr-1 !h-6 inline !fill-none dark:!fill-none' /> More content |
||||
</a> |
||||
</li --> |
||||
{/if} |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
</Popover> |
||||
{/key} |
||||
</div> |
||||
{/if} |
||||
</div> |
||||
Loading…
Reference in new issue