aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.d.ts12
-rw-r--r--src/app.html15
-rw-r--r--src/app.postcss4
-rw-r--r--src/lib/Article.svelte49
-rw-r--r--src/lib/ArticleHeader.svelte40
-rw-r--r--src/lib/Card.svelte0
-rw-r--r--src/lib/Toc.svelte24
-rw-r--r--src/lib/articleParser.ts36
-rw-r--r--src/lib/cards/Article.svelte118
-rw-r--r--src/lib/cards/Editor.svelte130
-rw-r--r--src/lib/cards/Search.svelte113
-rw-r--r--src/lib/cards/Settings.svelte149
-rw-r--r--src/lib/cards/UserArticles.svelte88
-rw-r--r--src/lib/cards/Welcome.svelte85
-rw-r--r--src/lib/components/LinkToArticle.svelte8
-rw-r--r--src/lib/components/Note.svelte70
-rw-r--r--src/lib/components/Searchbar.svelte38
-rw-r--r--src/lib/components/Toc.svelte21
-rw-r--r--src/lib/consts.ts4
-rw-r--r--src/lib/ndk.ts16
-rw-r--r--src/lib/state.ts13
-rw-r--r--src/lib/stores.ts3
-rw-r--r--src/lib/types.ts9
-rw-r--r--src/lib/utils.ts66
-rw-r--r--src/routes/+layout.server.ts1
-rw-r--r--src/routes/+layout.svelte12
-rw-r--r--src/routes/+page.svelte22
-rw-r--r--src/routes/[...path]/+page.svelte12
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}
+ &nbsp;• &nbsp;<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
+ >
+ &nbsp;• &nbsp;<a class="cursor-pointer" on:click={shareCopy}
+ >{#if copied}Copied!{:else}Share{/if}</a
+ >&nbsp;&nbsp;• &nbsp;<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}>&#x25B2;</button>
+ <p>{note.getVotes()}</p>
+ <button on:click={note.voteDown}>&#x25BC;</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 />