Browse Source

initial commit

master
limina1 2 years ago
commit
2a6cbb5a16
  1. 10
      .gitignore
  2. 14
      Dockerfile
  3. 21
      LICENSE
  4. 38
      README.md
  5. 9
      docker-compose.yaml
  6. BIN
      images/article_page.png
  7. BIN
      images/homepage.png
  8. 44
      package.json
  9. 2633
      pnpm-lock.yaml
  10. 12
      src/app.d.ts
  11. 15
      src/app.html
  12. 4
      src/app.postcss
  13. 49
      src/lib/Article.svelte
  14. 40
      src/lib/ArticleHeader.svelte
  15. 0
      src/lib/Card.svelte
  16. 24
      src/lib/Toc.svelte
  17. 36
      src/lib/articleParser.ts
  18. 118
      src/lib/cards/Article.svelte
  19. 130
      src/lib/cards/Editor.svelte
  20. 113
      src/lib/cards/Search.svelte
  21. 149
      src/lib/cards/Settings.svelte
  22. 88
      src/lib/cards/UserArticles.svelte
  23. 85
      src/lib/cards/Welcome.svelte
  24. 8
      src/lib/components/LinkToArticle.svelte
  25. 70
      src/lib/components/Note.svelte
  26. 38
      src/lib/components/Searchbar.svelte
  27. 21
      src/lib/components/Toc.svelte
  28. 4
      src/lib/consts.ts
  29. 16
      src/lib/ndk.ts
  30. 13
      src/lib/state.ts
  31. 3
      src/lib/stores.ts
  32. 9
      src/lib/types.ts
  33. 66
      src/lib/utils.ts
  34. 1
      src/routes/+layout.server.ts
  35. 12
      src/routes/+layout.svelte
  36. 22
      src/routes/+page.svelte
  37. 12
      src/routes/[...path]/+page.svelte
  38. BIN
      static/favicon.png
  39. 23
      svelte.config.js
  40. 12
      tailwind.config.cjs
  41. 17
      tsconfig.json
  42. 6
      vite.config.ts
  43. 3781
      yarn.lock

10
.gitignore vendored

@ -0,0 +1,10 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

14
Dockerfile

@ -0,0 +1,14 @@
FROM node:18.7.0
WORKDIR /app
COPY package.json package.json
COPY yarn.lock yarn.lock
RUN yarn
COPY . .
RUN yarn build
CMD [ "yarn", "preview", "--host" ]

21
LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 limina1
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

38
README.md

@ -0,0 +1,38 @@
# indextr
indextr is a nostr knowledge base (NKB).
## Requests for help and feedback
- Rendering other note types. Heterogeneous articles can potentially include any other kind - 0, 1, 1808, 30023, sandboxed executable code, images with captions External API calls to other interactive services (e.g. music notes that play audio when clicking)
- Article creation and uploading within the browser. Currently, articles are created and uploaded through NodeJS, would be nice for user entry fields.
- Design requests
# Screenshot
- Home Page, Displays Kind 30040 and article metadata
![home page](./images/homepage.png)
- Article Page. Composes article from notes (kind 30041, but potentially any other renderable kind) listed from the 30040 event
![article page](./images/article_page.png)
Still under development. Read more [here](https://github.com/limina1/indextr-principles/tree/main/details.md).
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

9
docker-compose.yaml

@ -0,0 +1,9 @@
version: '3'
services:
wikinostr:
build:
context: .
dockerfile: Dockerfile
ports:
- 3023:4173

BIN
images/article_page.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

BIN
images/homepage.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

44
package.json

@ -0,0 +1,44 @@
{
"name": "nostrwiki",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1",
"@sveltejs/kit": "^2.4.3",
"@types/markdown-it": "^13.0.7",
"autoprefixer": "^10.4.17",
"eslint-plugin-svelte": "^2.35.1",
"postcss": "^8.4.33",
"postcss-load-config": "^5.0.2",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.9",
"svelte-check": "^3.6.3",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.0.12"
},
"type": "module",
"dependencies": {
"@nostr-dev-kit/ndk": "^2.3.3",
"@nostr-dev-kit/ndk-cache-dexie": "^2.2.4",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"markdown-it": "^14.0.0",
"markdown-it-plain-text": "^0.3.0",
"marked": "^11.1.1",
"nostr-tools": "^2.1.4",
"showdown": "^2.1.0"
}
}

2633
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

12
src/app.d.ts vendored

@ -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 {};

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

4
src/app.postcss

@ -0,0 +1,4 @@
/* Write your global styles here, in PostCSS syntax */
@tailwind base;
@tailwind components;
@tailwind utilities;

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

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

0
src/lib/Card.svelte

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

36
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;
}

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

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

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

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

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

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

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

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

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

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

4
src/lib/consts.ts

@ -0,0 +1,4 @@
export const wikiKind = 30818;
export const standardRelays = [
'wss://nostr.thesamecat.io'
];

16
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);

13
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')) || ''
);

3
src/lib/stores.ts

@ -0,0 +1,3 @@
import { writable } from "svelte/store";
export let idList = writable([]);

9
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';

66
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)
);
}

1
src/routes/+layout.server.ts

@ -0,0 +1 @@
export const ssr = false;

12
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 />

22
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}

12
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 />

BIN
static/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

23
svelte.config.js

@ -0,0 +1,23 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: [vitePreprocess({})],
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(),
alias: {
$lib: 'src/lib',
$components: 'src/lib/components',
$cards: 'src/lib/cards'
}
}
};
export default config;

12
tailwind.config.cjs

@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config}*/
const config = {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: [require('@tailwindcss/forms'), require('@tailwindcss/typography')]
};
module.exports = config;

17
tsconfig.json

@ -0,0 +1,17 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

6
vite.config.ts

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});

3781
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save