Browse Source

closes #77 closes #189 closes #133 available actions moved to ... menu, profiles, details

master
Nuša Pukšič 1 year ago
parent
commit
a6edcdfda3
  1. 2
      src/app.css
  2. 66
      src/lib/components/Login.svelte
  3. 101
      src/lib/components/PublicationHeader.svelte
  4. 194
      src/lib/components/util/CardActions.svelte
  5. 34
      src/lib/components/util/CopyToClipboard.svelte
  6. 6
      src/lib/components/util/InlineProfile.svelte
  7. 101
      src/lib/components/util/Profile.svelte
  8. 19
      src/styles/publications.css

2
src/app.css

@ -106,7 +106,7 @@ @@ -106,7 +106,7 @@
/* Modal */
div.modal-leather > div {
@apply bg-primary-0 dark:bg-primary-1000 border-b-[1px] border-gray-800 dark:border-gray-500;
@apply bg-primary-0 dark:bg-primary-1000 border-b-[1px] border-primary-100 dark:border-primary-600;
}
div.modal-leather > div > h1,

66
src/lib/components/Login.svelte

@ -2,8 +2,7 @@ @@ -2,8 +2,7 @@
import { type NDKUserProfile } from '@nostr-dev-kit/ndk';
import { activePubkey, loginWithExtension, logout, ndkInstance, ndkSignedIn, persistLogin } from '$lib/ndk';
import { Avatar, Button, Popover, Tooltip } from 'flowbite-svelte';
import { ArrowRightToBracketOutline } from 'flowbite-svelte-icons';
import CopyToClipboard from "$components/util/CopyToClipboard.svelte";
import Profile from "$components/util/Profile.svelte";
let profile = $state<NDKUserProfile | null>(null);
let pfp = $derived(profile?.image);
@ -11,8 +10,6 @@ @@ -11,8 +10,6 @@
let tag = $derived(profile?.name);
let npub = $state<string | undefined >(undefined);
const externalProfileDestination = 'https://nostree.me/'
let signInFailed = $state<boolean>(false);
$effect(() => {
@ -43,61 +40,13 @@ @@ -43,61 +40,13 @@
}
}
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>
{#if $ndkSignedIn}
<Avatar
rounded
class='h-6 w-6 m-4 cursor-pointer'
src={pfp}
alt={username}
/>
{#key username || tag}
<Popover
class='popover-leather w-fit'
placement='bottom'
target='avatar'
>
<div class='flex flex-row justify-between space-x-4'>
<div class='flex flex-col'>
<h3 class='text-lg font-bold'>{username}</h3>
<h4 class='text-base'>@{tag}</h4>
<CopyToClipboard displayText={shortenNpub(npub)} copyText={npub} />
<a class='text-indigo-600 underline' href='{externalProfileDestination}{npub}' target='_blank'>View profile</a>
</div>
<div class='flex flex-col justify-center'>
<Button
id='sign-out-button'
class='btn-leather !p-2 hover:text-primary-400 dark:hover:text-primary-500 hover:border-primary-400 dark:hover:border-primary-500'
pill
outline
color='alternative'
onclick={handleSignOutClick}
>
<ArrowRightToBracketOutline class='w-4 h-4 !fill-none dark:!fill-none' />
<Tooltip
class='tooltip-leather w-fit whitespace-nowrap'
triggeredBy='#sign-out-button'
placement='bottom'
>
Sign out
</Tooltip>
</Button>
</div>
</div>
</Popover>
{/key}
{:else}
<Avatar rounded class='h-6 w-6 m-4 cursor-pointer' id='avatar' />
<div class="m-4">
{#if $ndkSignedIn}
<Profile pubkey={$activePubkey} isNav={true} />
{:else}
<Avatar rounded class='h-6 w-6 cursor-pointer' id='avatar' />
<Popover
class='popover-leather w-fit'
placement='bottom'
@ -117,4 +66,5 @@ @@ -117,4 +66,5 @@
</Button> -->
</div>
</Popover>
{/if}
{/if}
</div>

101
src/lib/components/PublicationHeader.svelte

@ -2,10 +2,11 @@ @@ -2,10 +2,11 @@
import { ndkInstance } from '$lib/ndk';
import { neventEncode } from '$lib/utils';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { naddrEncode, type AddressPointer } from 'nostr-tools/nip19';
import { standardRelays } from '../consts';
import { Card, Button, Modal, Tooltip } from 'flowbite-svelte';
import { ClipboardCheckOutline, ClipboardCleanOutline, CodeOutline, ShareNodesOutline } from 'flowbite-svelte-icons';
import { Card, Img } from "flowbite-svelte";
import CardActions from "$components/util/CardActions.svelte";
import Profile from "$components/util/Profile.svelte";
import InlineProfile from "$components/util/InlineProfile.svelte";
const { event } = $props<{ event: NDKEvent }>();
@ -25,83 +26,41 @@ @@ -25,83 +26,41 @@
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 eventIdCopied: boolean = $state(false);
let jsonModalOpen: boolean = $state(false);
let shareLinkCopied: boolean = $state(false);
function copyEventId() {
console.debug("copyEventID");
const relays: string[] = standardRelays;
const nevent = neventEncode(event, relays);
navigator.clipboard.writeText(nevent);
eventIdCopied = true;
}
function viewJson() {
console.debug("viewJSON");
jsonModalOpen = true;
}
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;
}
else {
console.log('dTag is undefined');
}
}
let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null);
let authorPubkey: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null);
</script>
{#if title != null && href != null}
<Card class='ArticleBox card-leather w-lg'>
<div class='flex flex-col space-y-8'>
<a href="/{href}" class='flex flex-col space-y-2'>
<h2 class='text-lg font-bold'>{title}</h2>
<h3 class='text-base font-normal'>by {author}</h3>
{#if version != '1'}
<h3 class='text-base font-normal'>version: {version}</h3>
<Card class='ArticleBox card-leather w-lg flex flex-row space-x-2'>
{#if image != null}
<div class="col justify-center align-middle">
<Img src={image} class="rounded max-h-36 max-w-36"/>
</div>
{/if}
</a>
<div class='w-full flex space-x-2 justify-end'>
<Button class='btn-leather' size='sm' onclick={shareNjump}><ShareNodesOutline /></Button>
<Tooltip class='tooltip-leather' type='auto' placement='top' on:show={() => shareLinkCopied = false}>
{#if shareLinkCopied}
<ClipboardCheckOutline />
<div class='col flex flex-row flex-grow space-x-4'>
<div class="flex flex-col flex-grow">
<a href="/{href}" class='flex flex-col space-y-2'>
<h2 class='text-lg font-bold line-clamp-2' title="{title}">{title}</h2>
<h3 class='text-base font-normal'>
by
{#if authorPubkey != null}
<InlineProfile pubkey={authorPubkey} title={author} />
{:else}
Share via NJump
{author}
{/if}
</Tooltip>
<Button class='btn-leather' size='sm' outline onclick={copyEventId}><ClipboardCleanOutline /></Button>
<Tooltip class='tooltip-leather' type='auto' placement='top' on:show={() => eventIdCopied = false}>
{#if eventIdCopied}
<ClipboardCheckOutline />
{:else}
Copy event ID
</h3>
{#if version != '1'}
<h3 class='text-base font-thin'>version: {version}</h3>
{/if}
</Tooltip>
<Button class='btn-leather' size='sm' outline onclick={viewJson}><CodeOutline /></Button>
<Tooltip class='tooltip-leather' type='auto' placement='top'>View JSON</Tooltip>
</a>
</div>
<div class="flex flex-col justify-between items-center">
<CardActions event={event} />
<div class="group mt-16">
<Profile pubkey={event.pubkey} />
</div>
</div>
</div>
<Modal class='modal-leather' title='Event JSON' bind:open={jsonModalOpen} autoclose outsideclose size='sm'>
<code>{JSON.stringify(event.rawEvent())}</code>
</Modal>
</Card>
{/if}

194
src/lib/components/util/CardActions.svelte

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
<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 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 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>
<li>
<a href="" role="button" class='btn-leather' onclick={viewDetails}>
<EyeOutline class="inline mr-2" /> View details
</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">
{#if type !== null}
<h5 class="text-sm">Publication type: {type}</h5>
{/if}
{#if language !== null}
<h5 class="text-sm">Language: {language}</h5>
{/if}
</div>
</Modal>
</div>

34
src/lib/components/util/CopyToClipboard.svelte

@ -1,29 +1,27 @@ @@ -1,29 +1,27 @@
<script lang='ts'>
import { Tooltip } from 'flowbite-svelte';
import { FileCopyOutline } from 'flowbite-svelte-icons';
export let displayText = ""; // Shown in UI
export let copyText = displayText; // Copied to clipboard (default to same if not provided)
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>
<div role='tooltip'>
<span>
{displayText} <button class='btn-leather' id='copy-button' onclick={copyToClipboard}><FileCopyOutline class='w-4 h-4 !fill-none dark:!fill-none' /></button>
</span>
<Tooltip
class='tooltip-leather'
triggeredBy='#copy-button'
trigger='click'
placement='bottom'
type="auto"
>
Copied!
</Tooltip>
</div>
<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>

6
src/lib/components/util/InlineProfile.svelte

@ -3,9 +3,9 @@ @@ -3,9 +3,9 @@
import { type NDKUserProfile } from '@nostr-dev-kit/ndk';
import { ndkInstance } from '$lib/ndk';
let { pubkey } = $props();
let { pubkey, title = null } = $props();
const externalProfileDestination = 'https://nostree.me/'
const externalProfileDestination = 'https://njump.me/'
let loading = $state(true);
let npub = $state('');
@ -43,7 +43,7 @@ @@ -43,7 +43,7 @@
class='h-6 w-6 mx-1 cursor-pointer inline'
src={pfp}
alt={username} />
<a class='text-indigo-600 underline' href='{externalProfileDestination}{npub}' target='_blank'>{username}</a>
<a class='underline' href='{externalProfileDestination}{npub}' title={title ?? username} target='_blank'>{username}</a>
{:else}
<span>Not found</span>
{/if}

101
src/lib/components/util/Profile.svelte

@ -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>

19
src/styles/publications.css

@ -90,7 +90,7 @@ @@ -90,7 +90,7 @@
.videoblock .title,
.olist .title,
.ulist .title {
@apply my-2;
@apply my-2 font-thin text-lg;
}
.note-leather li p {
@ -115,6 +115,10 @@ @@ -115,6 +115,10 @@
@apply text-sm;
}
.leading-normal.first-letter\:text-7xl .quoteblock {
min-height: 108px;
}
/* admonition */
.note-leather .admonitionblock .title {
@apply font-semibold;
@ -128,6 +132,10 @@ @@ -128,6 +132,10 @@
@apply flex flex-col;
}
.note-leather .admonitionblock p:has(code) {
@apply my-3;
}
.note-leather .admonitionblock {
@apply rounded overflow-hidden border;
}
@ -212,4 +220,13 @@ @@ -212,4 +220,13 @@
.videoblock .content video {
@apply w-full h-full;
}
/* audio */
.audioblock .content {
@apply my-3;
}
.audioblock .content audio {
@apply w-full;
}
}
Loading…
Cancel
Save