diff options
Diffstat (limited to 'src')
28 files changed, 1158 insertions, 0 deletions
diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..899c7e8 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..05b4710 --- /dev/null +++ b/src/app.html @@ -0,0 +1,15 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="icon" href="%sveltekit.assets%/favicon.png" /> + <meta name="viewport" content="width=device-width" /> + %sveltekit.head% + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css" /> + <html data-theme="dark" /> + </head> + + <body data-sveltekit-preload-data="hover"> + <div style="display: contents">%sveltekit.body%</div> + </body> +</html> diff --git a/src/app.postcss b/src/app.postcss new file mode 100644 index 0000000..1a7b7cf --- /dev/null +++ b/src/app.postcss @@ -0,0 +1,4 @@ +/* Write your global styles here, in PostCSS syntax */ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/lib/Article.svelte b/src/lib/Article.svelte new file mode 100644 index 0000000..2e95e9a --- /dev/null +++ b/src/lib/Article.svelte @@ -0,0 +1,49 @@ +<script lang="ts"> + import { ndk } from '$lib/ndk'; + import Toc from '$lib/components/Toc.svelte'; + import Notes from '$lib/components/Note.svelte'; + import {idList} from '$lib/stores'; + let events: NDKEvent[] = []; + async function getEvents() { + $idList.forEach(async (id) => { + const event = await $ndk.fetchEvent(id); + events = [...events, event]; + }); + } +</script> + +{#await getEvents() then article} + <div class="article"> + <div class="toc"> + <Toc notes={events} /> + </div> + + <div class="article-content"> + <Notes notes={events} /> + </div> + </div> + +{/await} + +<style> + .article { + display: flex; + padding: 1rem; + } + .toc { + padding: 3%; + min-width: 5%; + padding-top: 1%; + border: 1px white solid; + border-radius: 10px; + border-top-width: 5px; + } + .article-content { + min-width: 80%; + max-width: 85%; + padding: 1%; + border: 1px white solid; + border-radius: 10px; + border-top-width: 5px; + } +</style> diff --git a/src/lib/ArticleHeader.svelte b/src/lib/ArticleHeader.svelte new file mode 100644 index 0000000..f9ac9ee --- /dev/null +++ b/src/lib/ArticleHeader.svelte @@ -0,0 +1,40 @@ +<script lang="ts"> + import {nip19} from 'nostr-tools'; + import {ndk} from '$lib/ndk'; + import {idList} from '$lib/stores'; + + export let event: NDKEvent; + const title: string = JSON.parse(event.content).title; + const href: string = nip19.noteEncode(event.id) + const handleSendEvents = () => { + $idList=[]; + for (const id of event.tags.filter((tag)=> tag[0]==='e').map((tag)=> tag[1])) { + $idList = [...$idList, id]; + } + }; + + + +</script> + +<a data-sveltekit-preload-data="tap" href="/{href}"> + <div class="ArticleHeader" on:click={handleSendEvents}> + <h2>{title}</h2> + </div> +</a> + +<style> + .ArticleHeader { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + border: 1px solid purple; + border-radius: 10px; + padding: 5px; + border-top-width: 5px; + } + .ArticleHeader h2 { + font-size: 1.5rem; + } +</style> diff --git a/src/lib/Card.svelte b/src/lib/Card.svelte new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/lib/Card.svelte diff --git a/src/lib/Toc.svelte b/src/lib/Toc.svelte new file mode 100644 index 0000000..536f99f --- /dev/null +++ b/src/lib/Toc.svelte @@ -0,0 +1,24 @@ +<script lang="ts"> + import type { NDKEvent } from '@nostr-dev-kit/ndk'; + import {nip19} from 'nostr-tools'; + export let notes: NDKEvent[] = []; + // check if notes is empty + if (notes.length === 0) { + console.log('notes is empty'); + } +</script> + +<div class="toc"> + <h2>Table of contents</h2> + <ul> + {#each notes as note} + <li><a href="#{nip19.noteEncode(note.id)}">{note.getMatchingTags('title')[0][1]}</a></li> + {/each} + </ul> +</div> + +<style> + .toc h2 { + text-align: center; + } +</style> diff --git a/src/lib/articleParser.ts b/src/lib/articleParser.ts new file mode 100644 index 0000000..375d3a6 --- /dev/null +++ b/src/lib/articleParser.ts @@ -0,0 +1,36 @@ +import MarkdownIt from 'markdown-it'; +import LinkToArticle from '$components/LinkToArticle.svelte'; +import plainText from 'markdown-it-plain-text'; + +const md = new MarkdownIt(); +const mdTxt = new MarkdownIt().use(plainText); + +export function parse(markdown: string) { + let parsedMarkdown = md.render(markdown); + + parsedMarkdown = parsedMarkdown.replace(/\[\[(.*?)\]\]/g, (match: any, content: any) => { + const container = document.createElement('span'); + + const linkToArticle = new LinkToArticle({ + target: container, + props: { + content: content + } + }); + + return container.outerHTML; + }); + + return parsedMarkdown; +} + +export function parsePlainText(markdown: string) { + mdTxt.render(markdown); + + /* @ts-ignore */ // markdown-it-plain-text doesnt have typescript support?? + let parsedText = mdTxt.plainText.replace(/\[\[(.*?)\]\]/g, (match: any, content: any) => { + return content; + }); + + return parsedText; +} diff --git a/src/lib/cards/Article.svelte b/src/lib/cards/Article.svelte new file mode 100644 index 0000000..920eb46 --- /dev/null +++ b/src/lib/cards/Article.svelte @@ -0,0 +1,118 @@ +<script lang="ts"> + import { ndk } from '$lib/ndk'; + import { afterUpdate, onMount } from 'svelte'; + import type { NDKEvent } from '@nostr-dev-kit/ndk'; + import { formatDate, next } from '$lib/utils'; + import { parse } from '$lib/articleParser.js'; + import type { Tab } from '$lib/types'; + import { page } from '$app/stores'; + import { tabBehaviour, userPublickey } from '$lib/state'; + + export let eventid: string; + export let createChild: (tab: Tab) => void; + export let replaceSelf: (tab: Tab) => void; + let event: NDKEvent | null = null; + let copied = false; + + function addClickListenerToWikilinks() { + const elements = document.querySelectorAll('[id^="wikilink-v0-"]'); + + elements.forEach((element) => { + element.addEventListener('click', () => { + let a = element.id.slice(12); + if ($tabBehaviour == 'replace') { + replaceSelf({ id: next(), type: 'find', data: a }); + } else { + createChild({ id: next(), type: 'find', data: a }); + } + }); + }); + } + + function shareCopy() { + navigator.clipboard.writeText(`https://${$page.url.hostname}/article/${eventid}`); + copied = true; + setTimeout(() => { + copied = false; + }, 2500); + } + + onMount(async () => { + event = await $ndk.fetchEvent(eventid); + }); + + afterUpdate(() => { + addClickListenerToWikilinks(); + }); +</script> + +<div> + <!-- svelte-ignore a11y-no-static-element-interactions --> + <!-- svelte-ignore a11y-missing-attribute --> + <!-- svelte-ignore a11y-click-events-have-key-events --> + <article class="prose font-sans mx-auto p-2 lg:max-w-4xl"> + {#if event !== null} + <h1 class="mb-0"> + {#if event?.tags.find((e) => e[0] == 'title')?.[0] && event?.tags.find((e) => e[0] == 'title')?.[1]} + {event.tags.find((e) => e[0] == 'title')?.[1]} + {:else} + {event.tags.find((e) => e[0] == 'd')?.[1]} + {/if} + </h1> + <span> + {#await event.author?.fetchProfile()} + by <a + class="cursor-pointer" + on:click={() => { + $tabBehaviour == 'replace' + ? replaceSelf({ type: 'user', id: next(), data: event?.author.hexpubkey() }) + : createChild({ type: 'user', id: next(), data: event?.author.hexpubkey() }); + }}>...</a + >, + {:then profile} + by <a + class="cursor-pointer" + on:click={() => { + $tabBehaviour == 'replace' + ? replaceSelf({ type: 'user', id: next(), data: event?.author.hexpubkey() }) + : createChild({ type: 'user', id: next(), data: event?.author.hexpubkey() }); + }}>{profile !== null && JSON.parse(Array.from(profile)[0]?.content)?.name}</a + >, + {/await} + {#if event.created_at} + updated on {formatDate(event.created_at)} + {/if} + • <a + class="cursor-pointer" + on:click={() => { + $tabBehaviour == 'child' + ? createChild({ id: next(), type: 'editor', data: { forkId: event?.id } }) + : replaceSelf({ id: next(), type: 'editor', data: { forkId: event?.id } }); + }} + >{#if $userPublickey == event.author.hexpubkey()}Edit{:else}Fork{/if}</a + > + • <a class="cursor-pointer" on:click={shareCopy} + >{#if copied}Copied!{:else}Share{/if}</a + > • <a + class="cursor-pointer" + on:click={() => { + $tabBehaviour == 'child' + ? createChild({ + id: next(), + type: 'find', + data: event?.tags.find((e) => e[0] == 'd')?.[1] + }) + : replaceSelf({ + id: next(), + type: 'find', + data: event?.tags.find((e) => e[0] == 'd')?.[1] + }); + }}>Versions</a + > + </span> + + <!-- Content --> + {@html parse(event?.content)} + {/if} + </article> +</div> diff --git a/src/lib/cards/Editor.svelte b/src/lib/cards/Editor.svelte new file mode 100644 index 0000000..0de7189 --- /dev/null +++ b/src/lib/cards/Editor.svelte @@ -0,0 +1,130 @@ +<script lang="ts"> + import { ndk } from '$lib/ndk'; + import { wikiKind } from '$lib/consts'; + import { NDKEvent } from '@nostr-dev-kit/ndk'; + import { onMount } from 'svelte'; + import type { Tab } from '$lib/types'; + import { userPublickey } from '$lib/state'; + + export let replaceSelf: (tab: Tab) => void; + export let data: any; + if (!data.title) data.title = ''; + if (!data.summary) data.summary = ''; + if (!data.content) data.content = ''; + let forkev: NDKEvent | null; + + let success = 0; + let error: string = ''; + + onMount(async () => { + if (data.forkId) { + forkev = await $ndk.fetchEvent(data.forkId); + data.title = + forkev?.tags.find((e) => e[0] == 'title')?.[0] && + forkev?.tags.find((e) => e[0] == 'title')?.[1] + ? forkev.tags.find((e) => e[0] == 'title')?.[1] + : forkev?.tags.find((e) => e[0] == 'd')?.[1]; + data.summary = + forkev?.tags.find((e) => e[0] == 'summary')?.[0] && + forkev?.tags.find((e) => e[0] == 'summary')?.[1] + ? forkev?.tags.find((e) => e[0] == 'summary')?.[1] + : undefined; + data.content = forkev?.content; + } + }); + + async function publish() { + if (data.title && data.content) { + try { + let event = new NDKEvent($ndk); + event.kind = wikiKind; + event.content = data.content; + event.tags.push(['d', data.title.toLowerCase().replaceAll(' ', '-')]); + event.tags.push(['title', data.title]); + if (data.summary) { + event.tags.push(['summary', data.summary]); + } + let relays = await event.publish(); + relays.forEach((relay) => { + relay.once('published', () => { + console.log('published to', relay); + }); + relay.once('publish:failed', (relay, err) => { + console.log('publish failed to', relay, err); + }); + }); + success = 1; + } catch (err) { + console.log('failed to publish event', err); + error = String(err); + success = -1; + } + } + } +</script> + +<div class="prose font-sans mx-auto p-2 lg:max-w-4xl"> + <div class="prose"> + <h1> + {#if data.forkId && $userPublickey == forkev?.author?.hexpubkey()}Editing{:else if data.forkId}Forking{:else}Creating{/if} + an article + </h1> + </div> + <div class="mt-2"> + <label class="flex items-center" + >Title + <input + placeholder="example: Greek alphabet" + bind:value={data.title} + class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md ml-2" + /></label + > + </div> + <div class="mt-2"> + <label + >Article + <textarea + placeholder="The **Greek alphabet** has been used to write the [[Greek language]] sincie the late 9th or early 8th century BC. The Greek alphabet is the ancestor of the [[Latin]] and [[Cyrillic]] scripts." + bind:value={data.content} + rows="9" + class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" + /></label + > + </div> + <div class="mt-2"> + <details> + <summary> Add an explicit summary? </summary> + <label + >Summary + <textarea + bind:value={data.summary} + rows="3" + placeholder="The Greek alphabet is the earliest known alphabetic script to have distict letters for vowels. The Greek alphabet existed in many local variants." + class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md" + /></label + > + </details> + </div> + + <!-- Submit --> + {#if success !== 1} + <div class="mt-2"> + <button + on:click={publish} + class="inline-flex items-center px-4 py-2 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" + >Submit</button + > + </div> + {/if} + + <div> + {#if success == -1} + <p>Something went wrong :( note that only NIP07 is supported for signing</p> + <p> + Error Message: {error} + </p> + {:else if success == 1} + <p>Success!</p> + {/if} + </div> +</div> diff --git a/src/lib/cards/Search.svelte b/src/lib/cards/Search.svelte new file mode 100644 index 0000000..bb4566c --- /dev/null +++ b/src/lib/cards/Search.svelte @@ -0,0 +1,113 @@ +<script lang="ts"> + import { ndk } from '$lib/ndk'; + import { wikiKind } from '$lib/consts'; + import type { NDKEvent } from '@nostr-dev-kit/ndk'; + import { onMount } from 'svelte'; + import type { Tab } from '$lib/types'; + import { tabBehaviour } from '$lib/state'; + import { parsePlainText } from '$lib/articleParser'; + import { next } from '$lib/utils'; + + export let query: string; + export let replaceSelf: (tab: Tab) => void; + export let createChild: (tab: Tab) => void; + let results: NDKEvent[] = []; + let tried = 0; + + async function search(query: string) { + results = []; + const filter = { kinds: [wikiKind], '#d': [query] }; + const events = await $ndk.fetchEvents(filter); + if (!events) { + tried = 1; + results = []; + return; + } + tried = 1; + results = Array.from(events); + } + + onMount(async () => { + await search(query); + }); +</script> + +<article class="font-sans mx-auto p-2 lg:max-w-4xl"> + <div class="prose"> + <h1 class="mb-0">{query}</h1> + <p class="mt-0 mb-0"> + There are {#if tried == 1}{results.length}{:else}...{/if} articles with the name "{query}" + </p> + </div> + {#each results as result} + <!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions --> + <div + on:click={() => { + $tabBehaviour == 'child' + ? createChild({ id: next(), type: 'article', data: result.id }) + : replaceSelf({ id: next(), type: 'article', data: result.id }); + }} + class="cursor-pointer px-4 py-5 bg-white border border-gray-300 hover:bg-slate-50 rounded-lg mt-2 min-h-[48px]" + > + <h1> + {result.tags.find((e) => e[0] == 'title')?.[0] && + result.tags.find((e) => e[0] == 'title')?.[1] + ? result.tags.find((e) => e[0] == 'title')?.[1] + : result.tags.find((e) => e[0] == 'd')?.[1]} + </h1> + <p class="text-xs"> + <!-- implement published at? --> + <!-- {#if result.tags.find((e) => e[0] == "published_at")} + on {formatDate(result.tags.find((e) => e[0] == "published_at")[1])} + {/if} --> + {#await result.author?.fetchProfile()} + by <span class="text-gray-600 font-[600]">...</span> + {:then result} + by {result !== null && JSON.parse(Array.from(result)[0]?.content)?.name} + {/await} + </p> + <p class="text-xs"> + {#if result.tags.find((e) => e[0] == 'summary')?.[0] && result.tags.find((e) => e[0] == 'summary')?.[1]} + {result.tags + .find((e) => e[0] == 'summary')?.[1] + .slice( + 0, + 192 + )}{#if String(result.tags.find((e) => e[0] == 'summary')?.[1])?.length > 192}...{/if} + {:else} + {result.content.length <= 192 + ? parsePlainText(result.content.slice(0, 189)) + : parsePlainText(result.content.slice(0, 189)) + '...'} + {/if} + </p> + </div> + {/each} + {#if tried == 1} + <div class="px-4 py-5 bg-white border border-gray-300 rounded-lg mt-2 min-h-[48px]"> + <p class="mb-2"> + {results.length < 1 ? "Can't find this article" : "Didn't find what you are looking for?"} + </p> + <button + on:click={() => { + $tabBehaviour == 'child' + ? createChild({ id: next(), type: 'editor', data: { title: query } }) + : replaceSelf({ id: next(), type: 'editor', data: { title: query } }); + }} + class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" + > + Create this article! + </button> + <button + on:click={() => + $tabBehaviour == 'replace' + ? replaceSelf({ id: next(), type: 'settings' }) + : createChild({ id: next(), type: 'settings' })} + class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" + > + Add more relays + </button> + </div> + {:else} + <div class="px-4 py-5 rounded-lg mt-2 min-h-[48px]">Loading...</div> + {/if} +</article> diff --git a/src/lib/cards/Settings.svelte b/src/lib/cards/Settings.svelte new file mode 100644 index 0000000..2230c7f --- /dev/null +++ b/src/lib/cards/Settings.svelte @@ -0,0 +1,149 @@ +<script lang="ts"> + import { browser } from '$app/environment'; + import { standardRelays } from '$lib/consts'; + import { ndk } from '$lib/ndk'; + import { tabBehaviour, userPublickey } from '$lib/state'; + import { NDKNip07Signer } from '@nostr-dev-kit/ndk'; + import { onMount } from 'svelte'; + + let username = '...'; + let relays: string[] = []; + let newTabBehaviour = $tabBehaviour; + let newRelay = ''; + + function removeRelay(index: number) { + relays.splice(index, 1); + relays = [...relays]; + } + + async function login() { + if (browser) { + if (!$ndk.signer) { + const signer = new NDKNip07Signer(); + $ndk.signer = signer; + ndk.set($ndk); + } + if ($ndk.signer && $userPublickey == '') { + const newUserPublicKey = (await $ndk.signer.user()).hexpubkey(); + localStorage.setItem('wikinostr_loggedInPublicKey', newUserPublicKey); + $userPublickey = newUserPublicKey; + userPublickey.set($userPublickey); + } + } + } + + function logout() { + localStorage.removeItem('wikinostr_loggedInPublicKey'); + userPublickey.set(''); + } + + function addRelay() { + if (newRelay) { + relays.push(newRelay); + newRelay = ''; + relays = [...relays]; + } + } + + function saveData() { + addRelay(); + localStorage.setItem('wikinostr_tabBehaviour', newTabBehaviour); + localStorage.setItem('wikinostr_relays', JSON.stringify(relays)); + setTimeout(() => { + window.location.href = ''; + }, 1); + } + + if (browser) { + relays = JSON.parse(localStorage.getItem('wikinostr_relays') || JSON.stringify(standardRelays)); + } + + onMount(async () => { + // get user + const user = await $ndk.getUser({ hexpubkey: $userPublickey }); + const profile = await user.fetchProfile(); + if (profile) { + username = JSON.parse(Array.from(profile)[0].content).name; + } + }); +</script> + +<article class="font-sans mx-auto p-2 lg:max-w-4xl"> + <div class="prose"> + <h1 class="mt-0">Settings</h1> + </div> + + <!-- Login Options --> + <div class="my-6"> + <p class="text-sm">Account</p> + {#if $userPublickey == ''} + <p>You are not logged in!</p> + <button + on:click={login} + type="button" + class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" + >Login with NIP07 + </button> + {:else} + <p>You are logged in as <a href={`nostr://${$userPublickey}`}>{username}</a></p> + <button + on:click={logout} + type="button" + class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" + >Logout + </button> + {/if} + </div> + + <!-- Relay Selection --> + <div class="mb-6"> + <p class="text-sm">Relays</p> + {#each relays as relay, index} + <div class="border rounded-full pl-2 my-1"> + <button + class="text-red-500 py-0.5 px-1.5 rounded-full text-xl font-bold" + on:click={() => removeRelay(index)} + > + - + </button> + {relay} + </div> + {/each} + <div class="flex"> + <input + bind:value={newRelay} + type="text" + class="inline mr-0 rounded-md rounded-r-none shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300" + placeholder="wss://relay.example.com" + /> + <button + on:click={addRelay} + type="button" + class="inline-flex ml-0 rounded-md rounded-l-none items-center px-2.5 py-1.5 border border-transparent text-sm font-medium shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" + >Add</button + > + </div> + </div> + + <!-- More options --> + <div class="mb-6"> + <p class="text-sm">Tab Behaviour</p> + <select + class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md" + bind:value={newTabBehaviour} + > + <option value="replace">Replace Self Everywhere</option> + <option value="normal">Normal</option> + <option value="child">Create Child Everywhere</option> + </select> + </div> + + <!-- Save button --> + <button + on:click={saveData} + type="button" + class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" + > + Save & Reload + </button> +</article> diff --git a/src/lib/cards/UserArticles.svelte b/src/lib/cards/UserArticles.svelte new file mode 100644 index 0000000..e601d26 --- /dev/null +++ b/src/lib/cards/UserArticles.svelte @@ -0,0 +1,88 @@ +<script lang="ts"> + import { parsePlainText } from '$lib/articleParser'; + import { wikiKind } from '$lib/consts'; + import { ndk } from '$lib/ndk'; + import { tabBehaviour } from '$lib/state'; + import type { Tab } from '$lib/types'; + import { next } from '$lib/utils'; + import type { NDKEvent } from '@nostr-dev-kit/ndk'; + import { onMount } from 'svelte'; + + let results: NDKEvent[] = []; + let username = '...'; + export let createChild: (tab: Tab) => void; + export let replaceSelf: (tab: Tab) => void; + export let data: string; + + async function search() { + results = []; + const filter = { kinds: [wikiKind], limit: 1024, authors: [data] }; + const events = await $ndk.fetchEvents(filter); + if (!events) { + results = []; + return; + } + results = Array.from(events); + } + + onMount(async () => { + // get user + const user = await $ndk.getUser({ hexpubkey: data }); + const profile = await user.fetchProfile(); + if (profile) { + username = JSON.parse(Array.from(profile)[0].content).name; + } + + await search(); + }); +</script> + +<article class="font-sans mx-auto p-2 lg:max-w-4xl"> + <div> + <div class="prose"> + <h1><a href={`nostr://${data}`}>{username}</a>'s articles</h1> + </div> + {#each results as result} + <!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions --> + <div + on:click={() => + $tabBehaviour == 'replace' + ? replaceSelf({ id: next(), type: 'article', data: result.id }) + : createChild({ id: next(), type: 'article', data: result.id })} + class="cursor-pointer px-4 py-5 bg-white border border-gray-300 hover:bg-slate-50 rounded-lg mt-2 min-h-[48px]" + > + <h1> + {result.tags.find((e) => e[0] == 'title')?.[0] && + result.tags.find((e) => e[0] == 'title')?.[1] + ? result.tags.find((e) => e[0] == 'title')?.[1] + : result.tags.find((e) => e[0] == 'd')?.[1]} + </h1> + <p class="text-xs"> + <!-- implement published at? --> + <!-- {#if result.tags.find((e) => e[0] == "published_at")} + on {formatDate(result.tags.find((e) => e[0] == "published_at")[1])} + {/if} --> + {#await result.author?.fetchProfile()} + by <span class="text-gray-600 font-[600]">...</span> + {:then result} + by {result !== null && JSON.parse(Array.from(result)[0]?.content)?.name} + {/await} + </p> + <p class="text-xs"> + {#if result.tags.find((e) => e[0] == 'summary')?.[0] && result.tags.find((e) => e[0] == 'summary')?.[1]} + {result.tags + .find((e) => e[0] == 'summary')?.[1] + .slice( + 0, + 192 + )}{#if String(result.tags.find((e) => e[0] == 'summary')?.[1])?.length > 192}...{/if} + {:else} + {result.content.length <= 192 + ? parsePlainText(result.content.slice(0, 189)) + : parsePlainText(result.content.slice(0, 189)) + '...'} + {/if} + </p> + </div> + {/each} + </div> +</article> diff --git a/src/lib/cards/Welcome.svelte b/src/lib/cards/Welcome.svelte new file mode 100644 index 0000000..491f6c7 --- /dev/null +++ b/src/lib/cards/Welcome.svelte @@ -0,0 +1,85 @@ +<script lang="ts"> + import { parsePlainText } from '$lib/articleParser'; + import { wikiKind } from '$lib/consts'; + import { ndk } from '$lib/ndk'; + import { tabBehaviour } from '$lib/state'; + import type { Tab } from '$lib/types'; + import { next } from '$lib/utils'; + import type { NDKEvent } from '@nostr-dev-kit/ndk'; + import { onMount } from 'svelte'; + + let results: NDKEvent[] = []; + export let createChild: (tab: Tab) => void; + export let replaceSelf: (tab: Tab) => void; + + async function search() { + results = []; + const filter = { kinds: [wikiKind], limit: 48 }; + const events = await $ndk.fetchEvents(filter); + if (!events) { + results = []; + return; + } + results = Array.from(events); + } + + onMount(async () => { + await search(); + }); +</script> + +<article class="font-sans mx-auto p-2 lg:max-w-4xl"> + <div> + <div class="prose"> + <h1>Welcome</h1> + </div> + </div> + + <div> + <div class="prose"> + <h2>Recent Articles</h2> + </div> + {#each results as result} + <!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions --> + <div + on:click={() => + $tabBehaviour == 'replace' + ? replaceSelf({ id: next(), type: 'article', data: result.id }) + : createChild({ id: next(), type: 'article', data: result.id })} + class="cursor-pointer px-4 py-5 bg-white border border-gray-300 hover:bg-slate-50 rounded-lg mt-2 min-h-[48px]" + > + <h1> + {result.tags.find((e) => e[0] == 'title')?.[0] && + result.tags.find((e) => e[0] == 'title')?.[1] + ? result.tags.find((e) => e[0] == 'title')?.[1] + : result.tags.find((e) => e[0] == 'd')?.[1]} + </h1> + <p class="text-xs"> + <!-- implement published at? --> + <!-- {#if result.tags.find((e) => e[0] == "published_at")} + on {formatDate(result.tags.find((e) => e[0] == "published_at")[1])} + {/if} --> + {#await result.author?.fetchProfile()} + by <span class="text-gray-600 font-[600]">...</span> + {:then result} + by {result !== null && JSON.parse(Array.from(result)[0]?.content)?.name} + {/await} + </p> + <p class="text-xs"> + {#if result.tags.find((e) => e[0] == 'summary')?.[0] && result.tags.find((e) => e[0] == 'summary')?.[1]} + {result.tags + .find((e) => e[0] == 'summary')?.[1] + .slice( + 0, + 192 + )}{#if String(result.tags.find((e) => e[0] == 'summary')?.[1])?.length > 192}...{/if} + {:else} + {result.content.length <= 192 + ? parsePlainText(result.content.slice(0, 189)) + : parsePlainText(result.content.slice(0, 189)) + '...'} + {/if} + </p> + </div> + {/each} + </div> +</article> diff --git a/src/lib/components/LinkToArticle.svelte b/src/lib/components/LinkToArticle.svelte new file mode 100644 index 0000000..9d705a7 --- /dev/null +++ b/src/lib/components/LinkToArticle.svelte @@ -0,0 +1,8 @@ +<script lang="ts"> + export let content: string; +</script> + +<button + id={`wikilink-v0-${content.toLocaleLowerCase().replaceAll(' ', '-')}`} + class="text-indigo-600 underline">{content}</button +> diff --git a/src/lib/components/Note.svelte b/src/lib/components/Note.svelte new file mode 100644 index 0000000..44ba3ac --- /dev/null +++ b/src/lib/components/Note.svelte @@ -0,0 +1,70 @@ +<script lang="ts"> + import type { NDKEvent } from '@nostr-dev-kit/ndk'; + import {Converter} from 'showdown'; + const converter = new Converter(); + export let notes: NDKEvent[] = []; + notes.forEach((note) => { + note.votes = 0; + }); + import {nip19} from 'nostr-tools'; + $: notes.forEach((note) => { + note.voteUp = () => { + note.votes++; + note.update(); + }; + note.voteDown = () => { + note.votes--; + note.update(); + }; + note.getVotes = () => { + return note.votes; + }; + }); +</script> + +<div class="notes"> + {#each notes as note} + <div class="title" id={nip19.noteEncode(note.id)}> + <h4>{note.getMatchingTags('title')[0][1]}</h4> + </div> + <div class="vote"> + <button on:click={note.voteUp}>▲</button> + <p>{note.getVotes()}</p> + <button on:click={note.voteDown}>▼</button> + </div> + <div class="content"> + {@html converter.makeHtml(note.content)} + </div> + {/each} +</div> + +<style> + .notes { + display: grid; + border: 1px solid white; + } + + .title { + display: grid; + grid-column: 1/2; + margin: auto; + float: right; + border: 1px solid white; + text-align: center; + } + + .content { + display: grid; + grid-column: 1/2; + width: 100%; + padding: 10px; + border: 1px solid white; + } + .vote { + display: grid; + grid-template-rows: 1fr 1fr 1fr; + grid-column: 3/3; + width: 5%; + margin: 1%; + } +</style> diff --git a/src/lib/components/Searchbar.svelte b/src/lib/components/Searchbar.svelte new file mode 100644 index 0000000..3aeab92 --- /dev/null +++ b/src/lib/components/Searchbar.svelte @@ -0,0 +1,38 @@ +<script lang="ts"> + import { tabs } from '$lib/state'; + import { next, scrollTabIntoView } from '$lib/utils'; + import type { Tab } from '$lib/types'; + + let query = ''; + + function search() { + let a = query; + query = ''; + if (a) { + let newTabs = $tabs; + const newTab: Tab = { + id: next(), + type: 'find', + data: a.toLowerCase().replaceAll(' ', '-') + }; + newTabs.push(newTab); + tabs.set(newTabs); + scrollTabIntoView(String(newTab.id), true); + } + } +</script> + +<form on:submit|preventDefault={search} class="mt- flex rounded-md shadow-sm"> + <div class="relative flex items-stretch flex-grow focus-within:z-10"> + <input + bind:value={query} + class="focus:ring-indigo-500 focus:border-indigo-500 block w-full rounded-none rounded-l-md sm:text-sm border-gray-300" + placeholder="article name" + /> + </div> + <button + type="submit" + class="-ml-px relative inline-flex items-center space-x-2 px-3 py-2 border border-gray-300 text-sm font-medium rounded-r-md text-gray-700 bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-white" + >Go</button + > +</form> diff --git a/src/lib/components/Toc.svelte b/src/lib/components/Toc.svelte new file mode 100644 index 0000000..1d28ce6 --- /dev/null +++ b/src/lib/components/Toc.svelte @@ -0,0 +1,21 @@ +<script lang="ts"> + import type { NDKEvent } from '@nostr-dev-kit/ndk'; + import {nip19} from 'nostr-tools'; + export let notes: NDKEvent[] = []; + console.log(notes); +</script> + +<div class="toc"> + <h2>Table of contents</h2> + <ul> + {#each notes as note} + <li><a href="#{nip19.noteEncode(note.id)}">{note.getMatchingTags('title')[0][1]}</a></li> + {/each} + </ul> +</div> + +<style> + .toc h2 { + text-align: center; + } +</style> diff --git a/src/lib/consts.ts b/src/lib/consts.ts new file mode 100644 index 0000000..60435c5 --- /dev/null +++ b/src/lib/consts.ts @@ -0,0 +1,4 @@ +export const wikiKind = 30818; +export const standardRelays = [ + 'wss://nostr.thesamecat.io' +]; diff --git a/src/lib/ndk.ts b/src/lib/ndk.ts new file mode 100644 index 0000000..422cae8 --- /dev/null +++ b/src/lib/ndk.ts @@ -0,0 +1,16 @@ +import { browser } from '$app/environment'; +import NDK, { NDKEvent, NDKNip07Signer } from '@nostr-dev-kit/ndk'; +import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'; +import { writable, type Writable } from 'svelte/store'; +import { standardRelays } from './consts'; + +const relays = JSON.parse( + (browser && localStorage.getItem('wikinostr_relays')) || JSON.stringify(standardRelays) +); + +const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'indextr-ndk-cache-db' }); + +const Ndk: NDK = new NDK({ explicitRelayUrls: relays, cacheAdapter: dexieAdapter }); +Ndk.connect().then(() => console.log('ndk connected')); + +export const ndk: Writable<NDK> = writable(Ndk); diff --git a/src/lib/state.ts b/src/lib/state.ts new file mode 100644 index 0000000..71d1ae0 --- /dev/null +++ b/src/lib/state.ts @@ -0,0 +1,13 @@ +import { browser } from '$app/environment'; +import { writable, type Writable } from 'svelte/store'; +import type { Tab } from './types'; + +export const pathLoaded: Writable<boolean> = writable(false); + +export const tabs: Writable<Tab[]> = writable([{ id: 0, type: 'welcome' }]); +export const tabBehaviour: Writable<string> = writable( + (browser && localStorage.getItem('wikinostr_tabBehaviour')) || 'normal' +); +export const userPublickey: Writable<string> = writable( + (browser && localStorage.getItem('wikinostr_loggedInPublicKey')) || '' +); diff --git a/src/lib/stores.ts b/src/lib/stores.ts new file mode 100644 index 0000000..2ad4d6f --- /dev/null +++ b/src/lib/stores.ts @@ -0,0 +1,3 @@ +import { writable } from "svelte/store"; + +export let idList = writable([]); diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..9b8e84e --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,9 @@ +export type Tab = { + id: number; + type: TabType; + parent?: number; + previous?: Tab; + data?: any; +}; + +export type TabType = 'welcome' | 'find' | 'article' | 'user' | 'settings' | 'editor'; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..3678464 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,66 @@ +export function formatDate(unixtimestamp: number) { + const months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' + ]; + + const date = new Date(unixtimestamp * 1000); + const day = date.getDate(); + const month = months[date.getMonth()]; + const year = date.getFullYear(); + + const formattedDate = `${day} ${month} ${year}`; + return formattedDate; +} + +let serial = 0; + +export function next(): number { + serial++; + return serial; +} + +export function scrollTabIntoView(el: string | HTMLElement, wait: boolean) { + function scrollTab() { + const element = + typeof el === 'string' ? document.querySelector(`[id^="wikitab-v0-${el}"]`) : el; + if (!element) return; + + element.scrollIntoView({ + behavior: 'smooth', + inline: 'start' + }); + } + + if (wait) { + setTimeout(() => { + scrollTab(); + }, 1); + } else { + scrollTab(); + } +} + +export function isElementInViewport(el: string | HTMLElement) { + const element = typeof el === 'string' ? document.querySelector(`[id^="wikitab-v0-${el}"]`) : el; + if (!element) return; + + const rect = element.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +} diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..a3d1578 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1 @@ +export const ssr = false; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..c787429 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,12 @@ +<script> + // import Login from '$lib/login.svelte'; + import {tabs, userPublickey} from '$lib/state'; + // import {ndk} from '$lib/ndk'; + import {browser} from '$app/environment'; + import {NDKNip07Signer} from '@nostr-dev-kit/ndk'; + import {onMount} from 'svelte'; +</script> + + +<!-- <Login /> --> +<slot /> diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..ed4fc27 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,22 @@ +<script lang="ts"> + import ArticleHeader from '$lib/ArticleHeader.svelte'; + import {ndk} from '$lib/ndk'; + import {nip19} from "nostr-tools"; + import {idList} from '$lib/stores'; + const kind = 30040; + const count: number = 10 + + async function loadEvents() { + const eventlist = await $ndk.fetchEvents({ kinds: [kind] }); + return eventlist; + } + const eventlist = loadEvents(); +</script> + +{#await eventlist} + <p>Loading...</p> +{:then events} + {#each Array.from(events) as event, i} + <ArticleHeader event={event}/> + {/each} +{/await} diff --git a/src/routes/[...path]/+page.svelte b/src/routes/[...path]/+page.svelte new file mode 100644 index 0000000..0abbae5 --- /dev/null +++ b/src/routes/[...path]/+page.svelte @@ -0,0 +1,12 @@ +<script lang="ts"> + import {ndk} from '$lib/ndk'; + import { page } from '$app/stores'; + import Article from '$lib/Article.svelte'; + import {idList} from '$lib/stores'; + import {nip19} from 'nostr-tools'; + const id = nip19.decode($page.params.path).data; + + + +</script> +<Article /> |