commit
2a6cbb5a16
43 changed files with 7766 additions and 0 deletions
@ -0,0 +1,10 @@ |
|||||||
|
.DS_Store |
||||||
|
node_modules |
||||||
|
/build |
||||||
|
/.svelte-kit |
||||||
|
/package |
||||||
|
.env |
||||||
|
.env.* |
||||||
|
!.env.example |
||||||
|
vite.config.js.timestamp-* |
||||||
|
vite.config.ts.timestamp-* |
||||||
@ -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" ] |
||||||
@ -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. |
||||||
@ -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 |
||||||
|
 |
||||||
|
- Article Page. Composes article from notes (kind 30041, but potentially any other renderable kind) listed from the 30040 event |
||||||
|
 |
||||||
|
|
||||||
|
|
||||||
|
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. |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
version: '3' |
||||||
|
|
||||||
|
services: |
||||||
|
wikinostr: |
||||||
|
build: |
||||||
|
context: . |
||||||
|
dockerfile: Dockerfile |
||||||
|
ports: |
||||||
|
- 3023:4173 |
||||||
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 99 KiB |
@ -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" |
||||||
|
} |
||||||
|
} |
||||||
@ -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 {}; |
||||||
@ -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> |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
/* Write your global styles here, in PostCSS syntax */ |
||||||
|
@tailwind base; |
||||||
|
@tailwind components; |
||||||
|
@tailwind utilities; |
||||||
@ -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> |
||||||
@ -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,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> |
||||||
@ -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; |
||||||
|
} |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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 |
||||||
|
> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -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> |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
export const wikiKind = 30818; |
||||||
|
export const standardRelays = [ |
||||||
|
'wss://nostr.thesamecat.io' |
||||||
|
]; |
||||||
@ -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); |
||||||
@ -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')) || '' |
||||||
|
); |
||||||
@ -0,0 +1,3 @@ |
|||||||
|
import { writable } from "svelte/store"; |
||||||
|
|
||||||
|
export let idList = writable([]); |
||||||
@ -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'; |
||||||
@ -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) |
||||||
|
); |
||||||
|
} |
||||||
@ -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 /> |
||||||
@ -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} |
||||||
@ -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 /> |
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -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; |
||||||
@ -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; |
||||||
@ -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 |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite'; |
||||||
|
import { defineConfig } from 'vite'; |
||||||
|
|
||||||
|
export default defineConfig({ |
||||||
|
plugins: [sveltekit()] |
||||||
|
}); |
||||||
Loading…
Reference in new issue