commit
2a6cbb5a16
43 changed files with 7766 additions and 0 deletions
@ -0,0 +1,10 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,4 @@
|
||||
/* Write your global styles here, in PostCSS syntax */ |
||||
@tailwind base; |
||||
@tailwind components; |
||||
@tailwind utilities; |
||||
@ -0,0 +1,49 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,4 @@
|
||||
export const wikiKind = 30818; |
||||
export const standardRelays = [ |
||||
'wss://nostr.thesamecat.io' |
||||
]; |
||||
@ -0,0 +1,16 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,3 @@
|
||||
import { writable } from "svelte/store"; |
||||
|
||||
export let idList = writable([]); |
||||
@ -0,0 +1,9 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1 @@
|
||||
export const ssr = false; |
||||
@ -0,0 +1,12 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite'; |
||||
import { defineConfig } from 'vite'; |
||||
|
||||
export default defineConfig({ |
||||
plugins: [sveltekit()] |
||||
}); |
||||
Loading…
Reference in new issue