├── .npmrc ├── static └── favicon.png ├── images └── relay-list.png ├── src ├── lib │ ├── event │ │ ├── content │ │ │ ├── NoteContentTopic.svelte │ │ │ ├── NoteContentNewline.svelte │ │ │ ├── NoteContentPerson.svelte │ │ │ ├── Kind30001.svelte │ │ │ ├── Kind30000.svelte │ │ │ ├── NoteContentLink.svelte │ │ │ ├── Kind9802.svelte │ │ │ ├── EventContent.svelte │ │ │ ├── Kind30023.svelte │ │ │ ├── Kind1.svelte │ │ │ └── Kind1063.svelte │ │ ├── EventCard.svelte │ │ └── EventCardDropdownMenu.svelte │ ├── utils │ │ ├── user │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── markdown.ts │ │ ├── relay │ │ │ └── index.ts │ │ ├── event │ │ │ └── index.ts │ │ └── notes.ts │ ├── relay │ │ ├── RelayName.svelte │ │ └── RelayList.svelte │ ├── stores │ │ └── ndk.ts │ ├── index.ts │ └── user │ │ ├── Nip05.svelte │ │ ├── Name.svelte │ │ ├── Npub.svelte │ │ ├── Avatar.svelte │ │ └── UserCard.svelte ├── routes │ └── +page.svelte ├── styles │ └── global.css ├── app.d.ts ├── app.html └── stories │ ├── Introduction.mdx │ ├── relay │ └── RelayList.stories.ts │ ├── events │ ├── kinds │ │ ├── 1063.stories.ts │ │ ├── 9802.stories.ts │ │ ├── 1.stories.ts │ │ ├── lists │ │ │ ├── 30000.stories.ts │ │ │ └── 30001.stories.ts │ │ └── 30023.stories.ts │ ├── EventContent.stories.ts │ ├── EventCardDropdownMenu.stories.ts │ └── EventCard.stories.ts │ └── user │ ├── Npub.stories.ts │ ├── UserCard.stories.ts │ ├── Name.stories.ts │ ├── Avatar.stories.ts │ └── Nip05.stories.ts ├── .storybook ├── manager.ts ├── ndk-theme.ts ├── manager-head.html ├── preview-head.html ├── main.ts └── preview.ts ├── postcss.config.cjs ├── vite.config.ts ├── tailwind.config.js ├── .eslintignore ├── .prettierignore ├── .gitignore ├── tsconfig.json ├── .prettierrc ├── svelte.config.js ├── .eslintrc.cjs ├── LICENSE ├── README.md └── package.json /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nostr-dev-kit/ndk-svelte-components/HEAD/static/favicon.png -------------------------------------------------------------------------------- /images/relay-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nostr-dev-kit/ndk-svelte-components/HEAD/images/relay-list.png -------------------------------------------------------------------------------- /src/lib/event/content/NoteContentTopic.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | #{value} 6 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/manager-api"; 2 | import ndkTheme from "./ndk-theme"; 3 | 4 | addons.setConfig({ 5 | theme: ndkTheme, 6 | }); 7 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "tailwindcss/nesting": {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/event/content/NoteContentNewline.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | {#each value as _} 6 |
7 | {/each} 8 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite"; 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | }); 7 | -------------------------------------------------------------------------------- /src/lib/utils/user/index.ts: -------------------------------------------------------------------------------- 1 | export function prettifyNip05(nip05: string): string { 2 | if (nip05.startsWith("_@")) { 3 | return nip05.substring(2); 4 | } else { 5 | return nip05; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 |

What are you looking at?

2 | 3 |

4 | If you got here by running npm run dev you're doing it wrong. You should run 5 | npm run storybook instead. 6 |

7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{svelte,js,ts}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | **/build 4 | **/dist 5 | **/.svelte-kit 6 | **/package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | pnpm-lock.yaml 13 | justfile 14 | .turbo 15 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-shadow: rgba(0, 0, 0, 0.2); 3 | --color-border: #eaeaea; 4 | 5 | --color-primary: #0070f3; 6 | } 7 | 8 | a { 9 | color: var(--color-primary); 10 | text-decoration: none; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/relay/RelayName.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | {formatRelayName(relay)} 9 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /src/lib/stores/ndk.ts: -------------------------------------------------------------------------------- 1 | import NDK from "@nostr-dev-kit/ndk"; 2 | import { writable } from "svelte/store"; 3 | 4 | const _ndk = new NDK({ 5 | explicitRelayUrls: [ 6 | "wss://relay.f7z.io", 7 | "wss://nos.lol", 8 | "wss://relay.damus.io", 9 | "wss://relay.snort.social", 10 | ], 11 | }); 12 | 13 | export default writable(_ndk); 14 | -------------------------------------------------------------------------------- /.storybook/ndk-theme.ts: -------------------------------------------------------------------------------- 1 | import { create } from "@storybook/theming/create"; 2 | 3 | export default create({ 4 | base: "light", 5 | // Typography 6 | fontBase: '"Inter", Arial, Helvetica, sans-serif', 7 | fontCode: "monospace", 8 | 9 | brandTitle: "NDK Svelte Components", 10 | brandUrl: "https://svelte-components.ndk.fyi", 11 | brandTarget: "_self", 12 | }); 13 | -------------------------------------------------------------------------------- /src/lib/event/content/NoteContentPerson.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./relay"; 2 | export * from "./user"; 3 | 4 | export function truncatedBech32(bech32: string, length?: number): string { 5 | return `${bech32.substring(0, length || 9)}...`; 6 | } 7 | 8 | export async function copyToClipboard(textToCopy: string | undefined) { 9 | try { 10 | await navigator.clipboard.writeText(textToCopy as string); 11 | } catch (err) { 12 | console.error("Failed to copy: ", err); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-svelte"], 3 | "overrides": [ 4 | { 5 | "files": "*.svelte", 6 | "options": { 7 | "parser": "svelte" 8 | } 9 | } 10 | ], 11 | "importOrder": ["^[./]"], 12 | "tabWidth": 4, 13 | "useTabs": false, 14 | "trailingComma": "es5", 15 | "printWidth": 100, 16 | "singleQuote": false, 17 | "semi": true 18 | } 19 | -------------------------------------------------------------------------------- /.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from "@storybook/sveltekit"; 2 | 3 | const config: StorybookConfig = { 4 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], 5 | addons: [ 6 | "@storybook/addon-links", 7 | "@storybook/addon-essentials", 8 | "@storybook/addon-interactions", 9 | ], 10 | framework: { 11 | name: "@storybook/sveltekit", 12 | options: {}, 13 | }, 14 | docs: { 15 | autodocs: "tag", 16 | }, 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from "@storybook/svelte"; 2 | import "../src/styles/global.css"; 3 | import ndkTheme from "./ndk-theme"; 4 | 5 | const preview: Preview = { 6 | parameters: { 7 | actions: { argTypesRegex: "^on[A-Z].*" }, 8 | controls: { 9 | matchers: { 10 | color: /(background|color)$/i, 11 | date: /Date$/, 12 | }, 13 | }, 14 | docs: { 15 | theme: ndkTheme, 16 | }, 17 | }, 18 | }; 19 | 20 | export default preview; 21 | -------------------------------------------------------------------------------- /src/lib/utils/markdown.ts: -------------------------------------------------------------------------------- 1 | import DOMPurify from "isomorphic-dompurify"; 2 | import { marked } from "marked"; 3 | import { gfmHeadingId } from "marked-gfm-heading-id"; 4 | import { mangle } from "marked-mangle"; 5 | 6 | export const markdownToHtml = (content: string): string => { 7 | marked.use(mangle()); 8 | marked.use(gfmHeadingId()); 9 | 10 | return DOMPurify.sanitize( 11 | // eslint-disable-next-line no-misleading-character-class 12 | marked.parse(content.replace(/^[\u200B\u200C\u200D\u200E\u200F\uFEFF]/, "")) 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/utils/relay/index.ts: -------------------------------------------------------------------------------- 1 | import type { NDKRelay } from "@nostr-dev-kit/ndk"; 2 | 3 | export function formatRelayName(relay: NDKRelay): string { 4 | let name = relay.url; 5 | 6 | // Some well known relays 7 | switch (relay.url) { 8 | case "wss://purplepag.es": 9 | return "Purple Pages"; 10 | case "wss://relay.damus.io": 11 | return "Damus relay"; 12 | case "wss://relay.snort.social": 13 | return "Snort relay"; 14 | } 15 | 16 | // strip protocol prefix 17 | name = name.replace(/^(ws|wss):\/\//, ""); 18 | 19 | return name; 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import EventCard from "./event/EventCard.svelte"; 2 | import EventCardDropdownMenu from "./event/EventCardDropdownMenu.svelte"; 3 | import EventContent from "./event/content/EventContent.svelte"; 4 | import RelayList from "./relay/RelayList.svelte"; 5 | import Avatar from "./user/Avatar.svelte"; 6 | import Name from "./user/Name.svelte"; 7 | import UserCard from "./user/UserCard.svelte"; 8 | 9 | export * from "./utils"; 10 | 11 | export { 12 | // Event 13 | EventContent, 14 | EventCard, 15 | EventCardDropdownMenu, 16 | 17 | // User 18 | Avatar, 19 | Name, 20 | UserCard, 21 | 22 | // Relay 23 | RelayList, 24 | }; 25 | -------------------------------------------------------------------------------- /src/lib/event/content/Kind30001.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {#each list.items as tag (tag[1])} 11 |
12 | 13 |
14 | {/each} 15 | 16 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-auto"; 2 | import { vitePreprocess } from "@sveltejs/kit/vite"; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: [vitePreprocess({ postcss: true })], 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 14 | adapter: adapter(), 15 | }, 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:svelte/recommended", 7 | "prettier", 8 | "plugin:storybook/recommended", 9 | ], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["@typescript-eslint"], 12 | parserOptions: { 13 | sourceType: "module", 14 | ecmaVersion: 2020, 15 | extraFileExtensions: [".svelte"], 16 | }, 17 | env: { 18 | browser: true, 19 | es2017: true, 20 | node: true, 21 | }, 22 | overrides: [ 23 | { 24 | files: ["*.svelte"], 25 | parser: "svelte-eslint-parser", 26 | parserOptions: { 27 | parser: "@typescript-eslint/parser", 28 | }, 29 | }, 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /src/lib/event/content/Kind30000.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#each list.items as tag (tag[1])} 12 |
13 | 14 | 15 |
16 | {/each} 17 | 18 | -------------------------------------------------------------------------------- /src/lib/utils/event/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Format bytes as human-readable text. 3 | * 4 | * @param bytes Number of bytes. 5 | * @param si True to use metric (SI) units, aka powers of 1000. False to use 6 | * binary (IEC), aka powers of 1024. 7 | * @param dp Number of decimal places to display. 8 | * 9 | * @return Formatted string. 10 | */ 11 | export function humanFileSize(bytes: number, si = false, dp = 1): string { 12 | const thresh = si ? 1000 : 1024; 13 | 14 | if (Math.abs(bytes) < thresh) { 15 | return bytes + " B"; 16 | } 17 | 18 | const units = si 19 | ? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] 20 | : ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; 21 | let u = -1; 22 | const r = 10 ** dp; 23 | 24 | do { 25 | bytes /= thresh; 26 | ++u; 27 | } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); 28 | 29 | return bytes.toFixed(dp) + " " + units[u]; 30 | } 31 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %sveltekit.head% 10 | 11 | 24 | 25 | 26 |
%sveltekit.body%
27 | 28 | 29 | -------------------------------------------------------------------------------- /src/lib/event/content/NoteContentLink.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {#if showMedia && value.isMedia} 13 | {#if isImage} 14 | {value.url} 15 | {:else if isVideo} 16 |