├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── biome.json
├── components.json
├── next.config.ts
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
├── favicon.ico
└── og-image.png
├── src
├── actions
│ ├── auth.ts
│ └── redirectToSnippet.ts
├── app
│ ├── (auth)
│ │ ├── layout.tsx
│ │ ├── login
│ │ │ └── page.tsx
│ │ └── register
│ │ │ └── page.tsx
│ ├── (site)
│ │ ├── [nevent]
│ │ │ └── page.tsx
│ │ ├── archive
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── api
│ │ └── auth
│ │ │ └── [...nextauth]
│ │ │ └── route.ts
│ └── layout.tsx
├── auth
│ └── index.ts
├── components
│ └── ui
│ │ ├── avatar.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── mode-toggle.tsx
│ │ ├── popover.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── skeleton.tsx
│ │ ├── sonner.tsx
│ │ └── tooltip.tsx
├── features
│ ├── editor
│ │ ├── components
│ │ │ ├── ActiveEditor.tsx
│ │ │ ├── CopyButton.tsx
│ │ │ ├── Description.tsx
│ │ │ ├── DescriptionInput.tsx
│ │ │ ├── Filename.tsx
│ │ │ ├── FilenameInput.tsx
│ │ │ ├── InputTagList.tsx
│ │ │ ├── LanguageSelect.tsx
│ │ │ ├── ReadEditor.tsx
│ │ │ ├── TagList.tsx
│ │ │ └── TagsInput.tsx
│ │ ├── hooks
│ │ │ └── useSnippetEvent.ts
│ │ └── index.ts
│ ├── login
│ │ ├── components
│ │ │ ├── Login.tsx
│ │ │ └── UserDropdown.tsx
│ │ └── index.ts
│ ├── navigation
│ │ ├── components
│ │ │ ├── ArchiveNavButton.tsx
│ │ │ └── CreateNavButton.tsx
│ │ └── index.ts
│ ├── post
│ │ ├── components
│ │ │ └── PostButton.tsx
│ │ ├── hooks
│ │ │ └── usePostMutation.ts
│ │ └── index.ts
│ ├── snippet-feed
│ │ ├── components
│ │ │ ├── SnippetCard.tsx
│ │ │ ├── SnippetCardSkeleton.tsx
│ │ │ └── SnippetFeed.tsx
│ │ └── index.ts
│ └── zap
│ │ ├── components
│ │ ├── ZapButton.tsx
│ │ └── ZapDialog.tsx
│ │ ├── index.ts
│ │ └── lib
│ │ └── zap.ts
├── hooks
│ ├── useNostrProfile.ts
│ ├── useNostrRelayMetaData.ts
│ └── useNostrSnippets.ts
├── lib
│ ├── constants.ts
│ ├── languages.ts
│ ├── nostr
│ │ ├── createNevent.ts
│ │ ├── createNostrProfile.ts
│ │ ├── createNostrRelayMetadata.ts
│ │ ├── createNostrSnippet.ts
│ │ ├── finishEvent.ts
│ │ ├── getTagValue.ts
│ │ ├── parseUint8Array.ts
│ │ ├── publish.ts
│ │ └── shortNpub.ts
│ └── utils.ts
├── providers
│ ├── auth-provider.tsx
│ ├── query-client-provider.tsx
│ └── theme-provider.tsx
├── store
│ └── index.ts
├── styles
│ └── globals.css
└── types
│ └── index.ts
├── tsconfig.json
└── types.d.ts
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: christianchiarulli
4 | patreon: chrisatmachine
5 | ko_fi: chrisatmachine
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.*
7 | .yarn/*
8 | !.yarn/patches
9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 |
13 | # testing
14 | /coverage
15 |
16 | # next.js
17 | /.next/
18 | /out/
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 |
36 | # vercel
37 | .vercel
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 NODE-TEC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ✂️ Notebin
2 |
3 | Notebin is a code snippet sharing site similar to pastebin or GitHub gists.
4 |
5 | ## NIP-C0
6 |
7 | This is a simple reference implementation for [NIP-C0](https://github.com/nostr-protocol/nips/blob/master/C0.md).
8 |
9 | ## Developers
10 |
11 | - install dependencies
12 |
13 | ```shell
14 | npm i
15 | ```
16 |
17 | - run the app
18 |
19 | ```shell
20 | npm run dev
21 | ```
22 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "vcs": {
4 | "enabled": true,
5 | "clientKind": "git",
6 | "useIgnoreFile": true
7 | },
8 | "files": { "ignoreUnknown": false, "ignore": [] },
9 | "formatter": { "enabled": true, "indentStyle": "space" },
10 | "organizeImports": { "enabled": true },
11 | "linter": {
12 | "enabled": true,
13 | "rules": {
14 | "suspicious": {
15 | "noArrayIndexKey": "off"
16 | },
17 | "correctness": {
18 | "noUnusedImports": "warn",
19 | "useHookAtTopLevel": "error"
20 | },
21 | "nursery": {
22 | "useSortedClasses": {
23 | "level": "warn",
24 | "fix": "safe",
25 | "options": {
26 | "functions": ["clsx", "cva", "cn"]
27 | }
28 | }
29 | },
30 | "recommended": true
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "src/app/styles/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "~/components",
15 | "utils": "~/lib/utils",
16 | "ui": "~/components/ui",
17 | "lib": "~/lib",
18 | "hooks": "~/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | images: {
5 | dangerouslyAllowSVG: true,
6 | remotePatterns: [
7 | {
8 | protocol: "https",
9 | hostname: "**",
10 | },
11 | ],
12 | },
13 | };
14 |
15 | export default nextConfig;
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notebin",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@dicebear/collection": "^9.2.2",
13 | "@dicebear/core": "^9.2.2",
14 | "@hookform/resolvers": "^4.1.3",
15 | "@radix-ui/react-avatar": "^1.1.3",
16 | "@radix-ui/react-checkbox": "^1.1.4",
17 | "@radix-ui/react-dialog": "^1.1.6",
18 | "@radix-ui/react-dropdown-menu": "^2.1.6",
19 | "@radix-ui/react-label": "^2.1.2",
20 | "@radix-ui/react-popover": "^1.1.6",
21 | "@radix-ui/react-scroll-area": "^1.2.3",
22 | "@radix-ui/react-select": "^2.1.6",
23 | "@radix-ui/react-separator": "^1.1.2",
24 | "@radix-ui/react-slot": "^1.1.2",
25 | "@radix-ui/react-tooltip": "^1.1.8",
26 | "@tanstack/react-query": "^5.69.0",
27 | "@tanstack/react-query-devtools": "^5.69.0",
28 | "@uiw/codemirror-extensions-langs": "^4.23.10",
29 | "@uiw/codemirror-theme-github": "^4.23.10",
30 | "@uiw/react-codemirror": "^4.23.10",
31 | "class-variance-authority": "^0.7.1",
32 | "clsx": "^2.1.1",
33 | "lucide-react": "^0.484.0",
34 | "nanoid": "^5.1.5",
35 | "next": "15.2.4",
36 | "next-auth": "^4.24.11",
37 | "next-themes": "^0.4.6",
38 | "nostr-tools": "^2.11.0",
39 | "react": "^19.0.0",
40 | "react-codemirror-runmode": "^2.0.2",
41 | "react-dom": "^19.0.0",
42 | "react-hook-form": "^7.54.2",
43 | "sonner": "^2.0.2",
44 | "tailwind-merge": "^3.0.2",
45 | "tw-animate-css": "^1.2.4",
46 | "zod": "^3.24.2",
47 | "zustand": "^5.0.3"
48 | },
49 | "devDependencies": {
50 | "@biomejs/biome": "1.9.4",
51 | "@tailwindcss/postcss": "^4.0.17",
52 | "@types/node": "^20.17.28",
53 | "@types/react": "^19.0.12",
54 | "@types/react-dom": "^19.0.4",
55 | "tailwindcss": "^4.0.17",
56 | "typescript": "^5.8.2"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ["@tailwindcss/postcss"],
3 | };
4 |
5 | export default config;
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nodetec/notebin/68e5701255da6ae73d7d14be6f6d08da41ebb9c8/public/favicon.ico
--------------------------------------------------------------------------------
/public/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nodetec/notebin/68e5701255da6ae73d7d14be6f6d08da41ebb9c8/public/og-image.png
--------------------------------------------------------------------------------
/src/actions/auth.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { authOptions } from "~/auth";
4 | import type { UserWithKeys } from "~/types";
5 | import { getServerSession } from "next-auth";
6 | import { redirect } from "next/navigation";
7 |
8 | export async function getUser() {
9 | const session = await getServerSession(authOptions);
10 | const user = session?.user as UserWithKeys | undefined;
11 | return user;
12 | }
13 |
14 | export async function redirectIfNotLoggedIn() {
15 | const session = await getServerSession(authOptions);
16 | const user = session?.user as UserWithKeys | undefined;
17 | if (!user) {
18 | redirect("/login");
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/actions/redirectToSnippet.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { redirect } from "next/navigation";
4 |
5 | export async function redirectToSnippet(eventId: string) {
6 | redirect(`/${eventId}`);
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | export default function RootLayout({
2 | children,
3 | }: {
4 | children: React.ReactNode;
5 | }) {
6 | return (
7 |
8 |
9 |
10 | {children}
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/(auth)/login/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { bytesToHex } from "@noble/hashes/utils";
7 | import { Button } from "~/components/ui/button";
8 | import {
9 | Form,
10 | FormControl,
11 | FormField,
12 | FormItem,
13 | FormMessage,
14 | } from "~/components/ui/form";
15 | import { Input } from "~/components/ui/input";
16 | import { signIn } from "next-auth/react";
17 | import Link from "next/link";
18 | import { getPublicKey, nip19 } from "nostr-tools";
19 | import { useForm } from "react-hook-form";
20 | import * as z from "zod";
21 |
22 | const isValidNsec = (nsec: string) => {
23 | try {
24 | return nip19.decode(nsec).type === "nsec";
25 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
26 | } catch (e) {
27 | return false;
28 | }
29 | };
30 |
31 | const formSchema = z.object({
32 | nsec: z.string().refine(isValidNsec, {
33 | message: "Invalid nsec.",
34 | }),
35 | });
36 |
37 | export default function UserAuthForm() {
38 | const [isLoading, setIsLoading] = useState(false);
39 |
40 | const form = useForm>({
41 | resolver: zodResolver(formSchema),
42 | defaultValues: {
43 | nsec: "",
44 | },
45 | });
46 |
47 | const signInWithExtension = async (
48 | e: React.MouseEvent
49 | ) => {
50 | e.preventDefault();
51 | setIsLoading(true);
52 | if (typeof nostr !== "undefined") {
53 | const publicKey: string = await nostr.getPublicKey();
54 | await signIn("credentials", {
55 | publicKey: publicKey,
56 | secretKey: 0,
57 | redirect: true,
58 | callbackUrl: "/",
59 | });
60 | } else {
61 | alert("No extension found");
62 | }
63 | };
64 |
65 | async function onSubmit(values: z.infer) {
66 | setIsLoading(true);
67 | const { nsec } = values;
68 | const secretKeyUint8 = nip19.decode(nsec).data as Uint8Array;
69 | const publicKey = getPublicKey(secretKeyUint8);
70 | const secretKey = bytesToHex(secretKeyUint8);
71 |
72 | await signIn("credentials", {
73 | publicKey,
74 | secretKey,
75 | redirect: true,
76 | callbackUrl: "/",
77 | });
78 | }
79 |
80 | return (
81 |
82 |
83 |
Log in
84 |
85 | New to Nostr?{" "}
86 |
90 | Create an account
91 |
92 |
93 |
94 |
95 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | Or continue with
129 |
130 |
131 |
132 |
150 |
151 | );
152 | }
153 |
--------------------------------------------------------------------------------
/src/app/(auth)/register/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 |
5 | import { zodResolver } from "@hookform/resolvers/zod";
6 | import { Button } from "~/components/ui/button";
7 | import {
8 | Form,
9 | FormControl,
10 | FormField,
11 | FormItem,
12 | FormMessage,
13 | } from "~/components/ui/form";
14 | import { Input } from "~/components/ui/input";
15 | import { signIn } from "next-auth/react";
16 | import Link from "next/link";
17 | import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools";
18 | import { useForm } from "react-hook-form";
19 | import * as z from "zod";
20 |
21 | const isValidNpub = (npub: string) => {
22 | try {
23 | return nip19.decode(npub).type === "npub";
24 | } catch (e) {
25 | return false;
26 | }
27 | };
28 |
29 | const isValidNsec = (nsec: string) => {
30 | try {
31 | return nip19.decode(nsec).type === "nsec";
32 | } catch (e) {
33 | return false;
34 | }
35 | };
36 |
37 | const formSchema = z.object({
38 | npub: z.string().refine(isValidNpub, {
39 | message: "Invalid npub.",
40 | }),
41 | nsec: z.string().refine(isValidNsec, {
42 | message: "Invalid nsec.",
43 | }),
44 | });
45 |
46 | export default function RegisterForm() {
47 | const [isLoading, setIsLoading] = useState(false);
48 |
49 | const form = useForm>({
50 | resolver: zodResolver(formSchema),
51 | defaultValues: {
52 | nsec: "",
53 | npub: "",
54 | },
55 | });
56 |
57 | const { reset } = form;
58 |
59 | useEffect(() => {
60 | const secretKey = generateSecretKey();
61 | const publicKey = getPublicKey(secretKey);
62 | const nsec = nip19.nsecEncode(secretKey);
63 | const npub = nip19.npubEncode(publicKey);
64 |
65 | reset({
66 | nsec,
67 | npub,
68 | });
69 | }, [reset]);
70 |
71 | async function onSubmit(values: z.infer) {
72 | setIsLoading(true);
73 | const { npub, nsec } = values;
74 | const publicKey = nip19.decode(npub).data as string;
75 | const secretKey = nip19.decode(nsec).data as Uint8Array;
76 |
77 | await signIn("credentials", {
78 | publicKey,
79 | secretKey,
80 | redirect: true,
81 | callbackUrl: "/",
82 | });
83 | }
84 |
85 | return (
86 |
87 |
88 |
89 | Create an Account
90 |
91 |
92 | Already have an account?{" "}
93 |
97 | Sign in
98 |
99 |
100 |
101 |
102 |
161 |
162 |
163 | );
164 | }
165 |
--------------------------------------------------------------------------------
/src/app/(site)/[nevent]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth";
2 | import { nip19 } from "nostr-tools";
3 | import { authOptions } from "~/auth";
4 | import { Description, Filename, CopyButton } from "~/features/editor";
5 | import { ReadEditor } from "~/features/editor";
6 | import { TagList } from "~/features/editor/components/TagList";
7 | import { ZapButton } from "~/features/zap";
8 | import type { UserWithKeys } from "~/types";
9 |
10 | export default async function SnippetPage({
11 | params,
12 | }: {
13 | params: Promise<{ nevent: string }>;
14 | }) {
15 | const { nevent } = await params;
16 |
17 | // Normalize the nevent string to lowercase before decoding
18 | const normalizedNevent = nevent.toLowerCase();
19 | const decodeResult = nip19.decode(normalizedNevent);
20 |
21 | const session = await getServerSession(authOptions);
22 |
23 | const user = session?.user as UserWithKeys;
24 |
25 | if (decodeResult.type === "nevent") {
26 | const { kind, id, author, relays } = decodeResult.data;
27 |
28 | // TODO: refactor this nonsense
29 | return (
30 | <>
31 |
32 |
33 |
34 |
40 |
41 |
47 | {user?.publicKey && author && (
48 |
53 | )}
54 |
55 |
56 |
62 |
63 |
64 | >
65 | );
66 | }
67 |
68 | return Invalid Nevent
;
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/(site)/archive/page.tsx:
--------------------------------------------------------------------------------
1 | import { SnippetFeed } from "~/features/snippet-feed";
2 |
3 | export default function ArchivePage() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/(site)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Login } from "~/features/login/components/Login";
2 | import { shortenNpub } from "~/lib/nostr/shortNpub";
3 | import type { UserWithKeys } from "~/types";
4 | import { getServerSession } from "next-auth";
5 | import { authOptions } from "~/auth";
6 | import Link from "next/link";
7 | import { UserDropdown } from "~/features/login";
8 | import { CreateNavButton } from "~/features/navigation/components/CreateNavButton";
9 | import { ArchiveNavButton } from "~/features/navigation/components/ArchiveNavButton";
10 |
11 | export default async function SiteLayout({
12 | children,
13 | }: {
14 | children: React.ReactNode;
15 | }) {
16 | const session = await getServerSession(authOptions);
17 |
18 | const user = session?.user as UserWithKeys;
19 |
20 | const shortNpub = shortenNpub(user?.publicKey);
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 | Notebin.io
30 |
31 |
32 |
33 | {/* */}
34 |
35 |
36 |
37 |
38 | {user?.publicKey ? (
39 |
40 | ) : (
41 |
{shortNpub ?? "Login"}
42 | )}
43 |
44 |
45 | {children}
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/app/(site)/page.tsx:
--------------------------------------------------------------------------------
1 | import { FilenameInput, LanguageSelect } from "~/features/editor";
2 | import { DescriptionInput } from "~/features/editor";
3 | import { getServerSession } from "next-auth";
4 | import { authOptions } from "~/auth";
5 | import type { UserWithKeys } from "~/types";
6 | import { PostButton } from "~/features/post";
7 | import { ActiveEditor } from "~/features/editor";
8 | import { TagsInput } from "~/features/editor";
9 | import { InputTagList } from "~/features/editor";
10 |
11 | export default async function HomePage() {
12 | const session = await getServerSession(authOptions);
13 |
14 | const user = session?.user as UserWithKeys;
15 |
16 | return (
17 | <>
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {user?.publicKey && (
30 |
31 | )}
32 |
33 | >
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import { authOptions } from "~/auth";
3 |
4 | const handler = NextAuth(authOptions);
5 |
6 | export { handler as GET, handler as POST };
7 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Geist, Geist_Mono } from "next/font/google";
3 | import "~/styles/globals.css";
4 | import { ThemeProvider } from "~/providers/theme-provider";
5 | import AuthProvider from "~/providers/auth-provider";
6 | import QueryClientProviderWrapper from "~/providers/query-client-provider";
7 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
8 |
9 | import { Toaster } from "sonner";
10 | import { TooltipProvider } from "~/components/ui/tooltip";
11 |
12 | const geistSans = Geist({
13 | variable: "--font-geist-sans",
14 | subsets: ["latin"],
15 | });
16 |
17 | const geistMono = Geist_Mono({
18 | variable: "--font-geist-mono",
19 | subsets: ["latin"],
20 | });
21 |
22 | export const metadata: Metadata = {
23 | title: "Notebin.io | Code Sharing Platform",
24 | description:
25 | "A modern, fast, and secure platform for developers to share code snippets.",
26 | keywords: [
27 | "code sharing",
28 | "code snippets",
29 | "developer tools",
30 | "collaboration",
31 | "programming",
32 | "code review",
33 | ],
34 | authors: [{ name: "Notebin.io" }],
35 | creator: "Notebin.io",
36 | publisher: "Notebin.io",
37 | robots: "index, follow",
38 | metadataBase: new URL("https://notebin.io"),
39 | openGraph: {
40 | type: "website",
41 | locale: "en_US",
42 | url: "https://notebin.io",
43 | title: "Notebin.io - Modern Code Sharing Platform",
44 | description:
45 | "Share and collaborate on code snippets easily with Notebin.io. A modern, fast, and secure platform for developers.",
46 | siteName: "Notebin.io",
47 | images: [
48 | {
49 | url: "/og-image.png",
50 | width: 1200,
51 | height: 630,
52 | alt: "Notebin.io - Code Sharing Platform",
53 | },
54 | ],
55 | },
56 | twitter: {
57 | card: "summary_large_image",
58 | title: "Notebin.io - Modern Code Sharing Platform",
59 | description:
60 | "Share and collaborate on code snippets easily with Notebin.io",
61 | images: ["/og-image.png"],
62 | creator: "@notebinio",
63 | },
64 | viewport: {
65 | width: "device-width",
66 | initialScale: 1,
67 | maximumScale: 1,
68 | },
69 | };
70 |
71 | export default function RootLayout({
72 | children,
73 | }: Readonly<{
74 | children: React.ReactNode;
75 | }>) {
76 | return (
77 |
78 |
81 |
87 |
88 |
89 |
90 | {children}
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/src/auth/index.ts:
--------------------------------------------------------------------------------
1 | import type { TokenWithKeys, UserWithKeys } from "~/types";
2 | import type { AuthOptions } from "next-auth";
3 | import CredentialsProvider from "next-auth/providers/credentials";
4 |
5 | export const authOptions: AuthOptions = {
6 | providers: [
7 | CredentialsProvider({
8 | name: "nostr",
9 | credentials: {
10 | publicKey: {
11 | label: "Public Key",
12 | type: "text",
13 | placeholder: "npub...",
14 | },
15 | secretKey: {
16 | label: "Secret Key",
17 | type: "text",
18 | placeholder: "nsec...",
19 | },
20 | },
21 | async authorize(credentials, _) {
22 | // no credentials
23 | if (!credentials) return null;
24 |
25 | // no publicKey and no secretKey
26 | if (!credentials?.publicKey && !credentials.secretKey) {
27 | return null;
28 | }
29 |
30 | // publicKey and no secretKey
31 | if (credentials.publicKey && !credentials.secretKey) {
32 | const user = {
33 | id: credentials.publicKey,
34 | publicKey: credentials.publicKey,
35 | secretKey: "",
36 | };
37 | return user;
38 | }
39 |
40 | // publicKey and secretKey
41 | if (credentials.publicKey && credentials.secretKey) {
42 | return {
43 | id: credentials.publicKey,
44 | publicKey: credentials.publicKey,
45 | secretKey: credentials.secretKey,
46 | };
47 | }
48 |
49 | // no publicKey and secretKey
50 | return null;
51 | },
52 | }),
53 | ],
54 | pages: {
55 | signIn: "/login",
56 | // signOut: "/signout",
57 | error: "/error", // Error code passed in query string as ?error=
58 | // verifyRequest: "/auth/verify-request", // (used for check email message)
59 | newUser: "/register", // New users will be directed here on first sign in (leave the property out if not of interest)
60 | },
61 | session: {
62 | strategy: "jwt",
63 | },
64 | callbacks: {
65 | async jwt({ token, user }) {
66 | // If the user object exists, it means this is the initial token creation.
67 | if (user) {
68 | token.publicKey = (user as UserWithKeys).publicKey;
69 | token.secretKey = (user as UserWithKeys).secretKey;
70 | }
71 | return token;
72 | },
73 |
74 | async session({ session, token }) {
75 | // Extract the publicKey from the JWT token and add it to the session object
76 | const user = session.user as UserWithKeys;
77 | user.publicKey = (token as TokenWithKeys).publicKey;
78 | user.secretKey = (token as TokenWithKeys).secretKey;
79 | return session;
80 | },
81 | },
82 | debug: process.env.NODE_ENV === "development",
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | function Avatar({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | function AvatarImage({
25 | className,
26 | ...props
27 | }: React.ComponentProps) {
28 | return (
29 |
34 | )
35 | }
36 |
37 | function AvatarFallback({
38 | className,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
50 | )
51 | }
52 |
53 | export { Avatar, AvatarImage, AvatarFallback }
54 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import type * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "~/lib/utils";
6 |
7 | const buttonVariants = cva(
8 | "inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 dark:bg-destructive/60",
16 | outline:
17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 | ghost:
21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 | sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 | icon: "size-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | },
36 | );
37 |
38 | function Button({
39 | className,
40 | variant,
41 | size,
42 | asChild = false,
43 | ...props
44 | }: React.ComponentProps<"button"> &
45 | VariantProps & {
46 | asChild?: boolean;
47 | }) {
48 | const Comp = asChild ? Slot : "button";
49 |
50 | return (
51 |
56 | );
57 | }
58 |
59 | export { Button, buttonVariants };
60 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "~/lib/utils"
4 |
5 | function Card({ className, ...props }: React.ComponentProps<"div">) {
6 | return (
7 |
15 | )
16 | }
17 |
18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32 | return (
33 |
38 | )
39 | }
40 |
41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42 | return (
43 |
48 | )
49 | }
50 |
51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52 | return (
53 |
61 | )
62 | }
63 |
64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65 | return (
66 |
71 | )
72 | }
73 |
74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75 | return (
76 |
81 | )
82 | }
83 |
84 | export {
85 | Card,
86 | CardHeader,
87 | CardFooter,
88 | CardTitle,
89 | CardAction,
90 | CardDescription,
91 | CardContent,
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { CheckIcon } from "lucide-react"
6 |
7 | import { cn } from "~/lib/utils"
8 |
9 | function Checkbox({
10 | className,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
22 |
26 |
27 |
28 |
29 | )
30 | }
31 |
32 | export { Checkbox }
33 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { XIcon } from "lucide-react"
6 |
7 | import { cn } from "~/lib/utils"
8 |
9 | function Dialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function DialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return
19 | }
20 |
21 | function DialogPortal({
22 | ...props
23 | }: React.ComponentProps) {
24 | return
25 | }
26 |
27 | function DialogClose({
28 | ...props
29 | }: React.ComponentProps) {
30 | return
31 | }
32 |
33 | function DialogOverlay({
34 | className,
35 | ...props
36 | }: React.ComponentProps) {
37 | return (
38 |
46 | )
47 | }
48 |
49 | function DialogContent({
50 | className,
51 | children,
52 | ...props
53 | }: React.ComponentProps) {
54 | return (
55 |
56 |
57 |
65 | {children}
66 |
67 |
68 | Close
69 |
70 |
71 |
72 | )
73 | }
74 |
75 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
76 | return (
77 |
82 | )
83 | }
84 |
85 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
86 | return (
87 |
95 | )
96 | }
97 |
98 | function DialogTitle({
99 | className,
100 | ...props
101 | }: React.ComponentProps) {
102 | return (
103 |
108 | )
109 | }
110 |
111 | function DialogDescription({
112 | className,
113 | ...props
114 | }: React.ComponentProps) {
115 | return (
116 |
121 | )
122 | }
123 |
124 | export {
125 | Dialog,
126 | DialogClose,
127 | DialogContent,
128 | DialogDescription,
129 | DialogFooter,
130 | DialogHeader,
131 | DialogOverlay,
132 | DialogPortal,
133 | DialogTitle,
134 | DialogTrigger,
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6 |
7 | import { cn } from "~/lib/utils"
8 |
9 | function DropdownMenu({
10 | ...props
11 | }: React.ComponentProps) {
12 | return
13 | }
14 |
15 | function DropdownMenuPortal({
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
20 | )
21 | }
22 |
23 | function DropdownMenuTrigger({
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
31 | )
32 | }
33 |
34 | function DropdownMenuContent({
35 | className,
36 | sideOffset = 4,
37 | ...props
38 | }: React.ComponentProps) {
39 | return (
40 |
41 |
50 |
51 | )
52 | }
53 |
54 | function DropdownMenuGroup({
55 | ...props
56 | }: React.ComponentProps) {
57 | return (
58 |
59 | )
60 | }
61 |
62 | function DropdownMenuItem({
63 | className,
64 | inset,
65 | variant = "default",
66 | ...props
67 | }: React.ComponentProps & {
68 | inset?: boolean
69 | variant?: "default" | "destructive"
70 | }) {
71 | return (
72 |
82 | )
83 | }
84 |
85 | function DropdownMenuCheckboxItem({
86 | className,
87 | children,
88 | checked,
89 | ...props
90 | }: React.ComponentProps) {
91 | return (
92 |
101 |
102 |
103 |
104 |
105 |
106 | {children}
107 |
108 | )
109 | }
110 |
111 | function DropdownMenuRadioGroup({
112 | ...props
113 | }: React.ComponentProps) {
114 | return (
115 |
119 | )
120 | }
121 |
122 | function DropdownMenuRadioItem({
123 | className,
124 | children,
125 | ...props
126 | }: React.ComponentProps) {
127 | return (
128 |
136 |
137 |
138 |
139 |
140 |
141 | {children}
142 |
143 | )
144 | }
145 |
146 | function DropdownMenuLabel({
147 | className,
148 | inset,
149 | ...props
150 | }: React.ComponentProps & {
151 | inset?: boolean
152 | }) {
153 | return (
154 |
163 | )
164 | }
165 |
166 | function DropdownMenuSeparator({
167 | className,
168 | ...props
169 | }: React.ComponentProps) {
170 | return (
171 |
176 | )
177 | }
178 |
179 | function DropdownMenuShortcut({
180 | className,
181 | ...props
182 | }: React.ComponentProps<"span">) {
183 | return (
184 |
192 | )
193 | }
194 |
195 | function DropdownMenuSub({
196 | ...props
197 | }: React.ComponentProps) {
198 | return
199 | }
200 |
201 | function DropdownMenuSubTrigger({
202 | className,
203 | inset,
204 | children,
205 | ...props
206 | }: React.ComponentProps & {
207 | inset?: boolean
208 | }) {
209 | return (
210 |
219 | {children}
220 |
221 |
222 | )
223 | }
224 |
225 | function DropdownMenuSubContent({
226 | className,
227 | ...props
228 | }: React.ComponentProps) {
229 | return (
230 |
238 | )
239 | }
240 |
241 | export {
242 | DropdownMenu,
243 | DropdownMenuPortal,
244 | DropdownMenuTrigger,
245 | DropdownMenuContent,
246 | DropdownMenuGroup,
247 | DropdownMenuLabel,
248 | DropdownMenuItem,
249 | DropdownMenuCheckboxItem,
250 | DropdownMenuRadioGroup,
251 | DropdownMenuRadioItem,
252 | DropdownMenuSeparator,
253 | DropdownMenuShortcut,
254 | DropdownMenuSub,
255 | DropdownMenuSubTrigger,
256 | DropdownMenuSubContent,
257 | }
258 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { Slot } from "@radix-ui/react-slot"
6 | import {
7 | Controller,
8 | FormProvider,
9 | useFormContext,
10 | useFormState,
11 | type ControllerProps,
12 | type FieldPath,
13 | type FieldValues,
14 | } from "react-hook-form"
15 |
16 | import { cn } from "~/lib/utils"
17 | import { Label } from "~/components/ui/label"
18 |
19 | const Form = FormProvider
20 |
21 | type FormFieldContextValue<
22 | TFieldValues extends FieldValues = FieldValues,
23 | TName extends FieldPath = FieldPath,
24 | > = {
25 | name: TName
26 | }
27 |
28 | const FormFieldContext = React.createContext(
29 | {} as FormFieldContextValue
30 | )
31 |
32 | const FormField = <
33 | TFieldValues extends FieldValues = FieldValues,
34 | TName extends FieldPath = FieldPath,
35 | >({
36 | ...props
37 | }: ControllerProps) => {
38 | return (
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | const useFormField = () => {
46 | const fieldContext = React.useContext(FormFieldContext)
47 | const itemContext = React.useContext(FormItemContext)
48 | const { getFieldState } = useFormContext()
49 | const formState = useFormState({ name: fieldContext.name })
50 | const fieldState = getFieldState(fieldContext.name, formState)
51 |
52 | if (!fieldContext) {
53 | throw new Error("useFormField should be used within ")
54 | }
55 |
56 | const { id } = itemContext
57 |
58 | return {
59 | id,
60 | name: fieldContext.name,
61 | formItemId: `${id}-form-item`,
62 | formDescriptionId: `${id}-form-item-description`,
63 | formMessageId: `${id}-form-item-message`,
64 | ...fieldState,
65 | }
66 | }
67 |
68 | type FormItemContextValue = {
69 | id: string
70 | }
71 |
72 | const FormItemContext = React.createContext(
73 | {} as FormItemContextValue
74 | )
75 |
76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
86 |
87 | )
88 | }
89 |
90 | function FormLabel({
91 | className,
92 | ...props
93 | }: React.ComponentProps) {
94 | const { error, formItemId } = useFormField()
95 |
96 | return (
97 |
104 | )
105 | }
106 |
107 | function FormControl({ ...props }: React.ComponentProps) {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | }
124 |
125 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
126 | const { formDescriptionId } = useFormField()
127 |
128 | return (
129 |
135 | )
136 | }
137 |
138 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
139 | const { error, formMessageId } = useFormField()
140 | const body = error ? String(error?.message ?? "") : props.children
141 |
142 | if (!body) {
143 | return null
144 | }
145 |
146 | return (
147 |
153 | {body}
154 |
155 | )
156 | }
157 |
158 | export {
159 | useFormField,
160 | Form,
161 | FormItem,
162 | FormLabel,
163 | FormControl,
164 | FormDescription,
165 | FormMessage,
166 | FormField,
167 | }
168 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import type * as React from "react";
2 |
3 | import { cn } from "~/lib/utils";
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | );
19 | }
20 |
21 | export { Input };
22 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | function Label({
9 | className,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
21 | )
22 | }
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/src/components/ui/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Moon, Sun } from "lucide-react";
3 | import { useTheme } from "next-themes";
4 |
5 | import { Button } from "~/components/ui/button";
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuTrigger,
11 | } from "~/components/ui/dropdown-menu";
12 |
13 | export function ModeToggle() {
14 | const { setTheme } = useTheme();
15 |
16 | return (
17 |
18 |
19 |
28 |
29 |
30 | setTheme("light")}>
31 | Light
32 |
33 | setTheme("dark")}>
34 | Dark
35 |
36 | setTheme("system")}>
37 | System
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | function Popover({
9 | ...props
10 | }: React.ComponentProps) {
11 | return
12 | }
13 |
14 | function PopoverTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return
18 | }
19 |
20 | function PopoverContent({
21 | className,
22 | align = "center",
23 | sideOffset = 4,
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 |
38 |
39 | )
40 | }
41 |
42 | function PopoverAnchor({
43 | ...props
44 | }: React.ComponentProps) {
45 | return
46 | }
47 |
48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
49 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | function ScrollArea({
9 | className,
10 | children,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
19 |
23 | {children}
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | function ScrollBar({
32 | className,
33 | orientation = "vertical",
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
50 |
54 |
55 | )
56 | }
57 |
58 | export { ScrollArea, ScrollBar }
59 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type * as React from "react";
4 | import * as SelectPrimitive from "@radix-ui/react-select";
5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
6 |
7 | import { cn } from "~/lib/utils";
8 |
9 | function Select({
10 | ...props
11 | }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function SelectGroup({
16 | ...props
17 | }: React.ComponentProps) {
18 | return ;
19 | }
20 |
21 | function SelectValue({
22 | ...props
23 | }: React.ComponentProps) {
24 | return ;
25 | }
26 |
27 | function SelectTrigger({
28 | className,
29 | size = "default",
30 | children,
31 | ...props
32 | }: React.ComponentProps & {
33 | size?: "sm" | "default";
34 | }) {
35 | return (
36 |
45 | {children}
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | function SelectContent({
54 | className,
55 | children,
56 | position = "popper",
57 | ...props
58 | }: React.ComponentProps) {
59 | return (
60 |
61 |
72 |
73 |
80 | {children}
81 |
82 |
83 |
84 |
85 | );
86 | }
87 |
88 | function SelectLabel({
89 | className,
90 | ...props
91 | }: React.ComponentProps) {
92 | return (
93 |
98 | );
99 | }
100 |
101 | function SelectItem({
102 | className,
103 | children,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
115 |
116 |
117 |
118 |
119 |
120 | {children}
121 |
122 | );
123 | }
124 |
125 | function SelectSeparator({
126 | className,
127 | ...props
128 | }: React.ComponentProps) {
129 | return (
130 |
135 | );
136 | }
137 |
138 | function SelectScrollUpButton({
139 | className,
140 | ...props
141 | }: React.ComponentProps) {
142 | return (
143 |
151 |
152 |
153 | );
154 | }
155 |
156 | function SelectScrollDownButton({
157 | className,
158 | ...props
159 | }: React.ComponentProps) {
160 | return (
161 |
169 |
170 |
171 | );
172 | }
173 |
174 | export {
175 | Select,
176 | SelectContent,
177 | SelectGroup,
178 | SelectItem,
179 | SelectLabel,
180 | SelectScrollDownButton,
181 | SelectScrollUpButton,
182 | SelectSeparator,
183 | SelectTrigger,
184 | SelectValue,
185 | };
186 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "~/lib/utils"
7 |
8 | function Separator({
9 | className,
10 | orientation = "horizontal",
11 | decorative = true,
12 | ...props
13 | }: React.ComponentProps) {
14 | return (
15 |
25 | )
26 | }
27 |
28 | export { Separator }
29 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "~/lib/utils"
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
10 | )
11 | }
12 |
13 | export { Skeleton }
14 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Toaster as Sonner, type ToasterProps } from "sonner";
5 |
6 | const Toaster = ({ ...props }: ToasterProps) => {
7 | const { theme = "system" } = useTheme();
8 |
9 | return (
10 |
22 | );
23 | };
24 |
25 | export { Toaster };
26 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type * as React from "react";
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
5 |
6 | import { cn } from "~/lib/utils";
7 |
8 | function TooltipProvider({
9 | delayDuration = 0,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
18 | );
19 | }
20 |
21 | function Tooltip({
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | function TooltipTrigger({
32 | ...props
33 | }: React.ComponentProps) {
34 | return ;
35 | }
36 |
37 | function TooltipContent({
38 | className,
39 | sideOffset = 0,
40 | children,
41 | ...props
42 | }: React.ComponentProps) {
43 | return (
44 |
45 |
54 | {children}
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
62 |
--------------------------------------------------------------------------------
/src/features/editor/components/ActiveEditor.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useMemo, useCallback } from "react";
4 | import CodeMirror from "@uiw/react-codemirror";
5 | import { githubLight } from "@uiw/codemirror-theme-github";
6 | import { githubDark } from "@uiw/codemirror-theme-github";
7 | import { loadLanguage } from "@uiw/codemirror-extensions-langs";
8 | import { useAppState } from "~/store";
9 | import { useTheme } from "next-themes";
10 |
11 | export function ActiveEditor() {
12 | const setContent = useAppState((state) => state.setContent);
13 |
14 | const content = useAppState((state) => state.content);
15 | const lang = useAppState((state) => state.lang);
16 |
17 | const { resolvedTheme } = useTheme();
18 |
19 | const languageExtension = useMemo(() => {
20 | const extension = loadLanguage(lang);
21 | return extension ? [extension] : [];
22 | }, [lang]);
23 |
24 | const onChange = useCallback(
25 | (val: string) => {
26 | setContent(val);
27 | },
28 | [setContent],
29 | );
30 |
31 | return (
32 |
33 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/features/editor/components/CopyButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "~/components/ui/button";
4 | import { Copy } from "lucide-react";
5 | import { toast } from "sonner";
6 | import { useSnippetEvent } from "../hooks/useSnippetEvent";
7 |
8 | type Props = {
9 | eventId: string;
10 | kind?: number;
11 | author?: string;
12 | relays?: string[];
13 | };
14 |
15 | export function CopyButton({ eventId, kind, author, relays }: Props) {
16 | const { data: snippet } = useSnippetEvent(eventId, kind, author, relays);
17 |
18 | const handleCopy = async () => {
19 | if (!snippet?.content) return;
20 |
21 | try {
22 | await navigator.clipboard.writeText(snippet.content);
23 | toast("Copied to clipboard", {
24 | description: "The snippet has been copied to your clipboard.",
25 | });
26 | } catch (error) {
27 | toast("Failed to copy", {
28 | description: "There was an error copying to your clipboard.",
29 | });
30 | }
31 | };
32 |
33 | return (
34 |
37 | );
38 | }
--------------------------------------------------------------------------------
/src/features/editor/components/Description.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { getTagValue } from "~/lib/nostr/getTagValue";
3 | import { useSnippetEvent } from "../hooks/useSnippetEvent";
4 |
5 | type DescriptionProps = {
6 | eventId: string;
7 | kind?: number;
8 | author?: string;
9 | relays?: string[];
10 | };
11 |
12 | export function Description({
13 | eventId,
14 | kind,
15 | author,
16 | relays,
17 | }: DescriptionProps) {
18 | const { data: snippet } = useSnippetEvent(eventId, kind, author, relays);
19 |
20 | return (
21 |
22 |
23 | {getTagValue(snippet, "description")}
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/src/features/editor/components/DescriptionInput.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "~/components/ui/input";
4 | import { useAppState } from "~/store";
5 |
6 | export function DescriptionInput() {
7 | const setDescription = useAppState((state) => state.setDescription);
8 | const description = useAppState((state) => state.description);
9 |
10 | return (
11 | ) =>
16 | setDescription(e.target.value)
17 | }
18 | />
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/features/editor/components/Filename.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { getTagValue } from "~/lib/nostr/getTagValue";
4 | import { useSnippetEvent } from "../hooks/useSnippetEvent";
5 |
6 | type FilenameProps = {
7 | eventId: string;
8 | kind?: number;
9 | author?: string;
10 | relays?: string[];
11 | };
12 |
13 | export function Filename({ eventId, kind, author, relays }: FilenameProps) {
14 | const { data: snippet } = useSnippetEvent(eventId, kind, author, relays);
15 |
16 | return (
17 |
18 | {getTagValue(snippet, "name")}
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/features/editor/components/FilenameInput.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Input } from "~/components/ui/input";
4 | import { useAppState } from "~/store";
5 |
6 | export function FilenameInput() {
7 | const setFilename = useAppState((state) => state.setFilename);
8 | const filename = useAppState((state) => state.filename);
9 |
10 | return (
11 | ) =>
16 | setFilename(e.target.value)
17 | }
18 | />
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/features/editor/components/InputTagList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { XIcon } from "lucide-react";
4 | import { useAppState } from "~/store";
5 |
6 | export function InputTagList() {
7 | const tags = useAppState((state) => state.tags);
8 | const setTags = useAppState((state) => state.setTags);
9 |
10 | const handleRemoveTag = (tagToRemove: string) => {
11 | const newTags = tags.filter((tag) => tag !== tagToRemove);
12 | console.log("Removing tag:", tagToRemove);
13 | console.log("Current tags:", tags);
14 | console.log("New tags:", newTags);
15 | setTags(newTags);
16 | };
17 |
18 | return (
19 | <>
20 | {tags.length > 0 && (
21 |
22 | {tags.map((tag) => (
23 |
27 | {tag}
28 |
36 |
37 | ))}
38 |
39 | )}
40 | >
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/src/features/editor/components/LanguageSelect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectItem,
7 | SelectTrigger,
8 | SelectValue,
9 | } from "~/components/ui/select";
10 | import type { LanguageName } from "@uiw/codemirror-extensions-langs";
11 | import { languages } from "~/lib/languages";
12 | import { useAppState } from "~/store";
13 |
14 | export function LanguageSelect() {
15 | const setLang = useAppState((state) => state.setLang);
16 | const lang = useAppState((state) => state.lang);
17 | return (
18 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/features/editor/components/ReadEditor.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useMemo } from "react";
4 | import CodeMirror from "@uiw/react-codemirror";
5 | import { githubLight } from "@uiw/codemirror-theme-github";
6 | import { githubDark } from "@uiw/codemirror-theme-github";
7 | import {
8 | type LanguageName,
9 | loadLanguage,
10 | } from "@uiw/codemirror-extensions-langs";
11 | import { useTheme } from "next-themes";
12 | import { useSnippetEvent } from "../hooks/useSnippetEvent";
13 | import { getTagValue } from "~/lib/nostr/getTagValue";
14 |
15 | type ReadEditorProps = {
16 | kind?: number;
17 | eventId: string;
18 | author?: string;
19 | relays?: string[];
20 | };
21 |
22 | export function ReadEditor({ kind, eventId, author, relays }: ReadEditorProps) {
23 | const { resolvedTheme } = useTheme();
24 |
25 | const { data: snippet } = useSnippetEvent(eventId, kind, author, relays);
26 |
27 | const languageExtension = useMemo(() => {
28 | const extension = loadLanguage(
29 | (getTagValue(snippet, "l") as LanguageName) || "markdown",
30 | );
31 | return extension ? [extension] : [];
32 | }, [snippet]);
33 |
34 | return (
35 |
36 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/features/editor/components/TagList.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useSnippetEvent } from "../hooks/useSnippetEvent";
4 |
5 | type TagListProps = {
6 | eventId: string;
7 | kind?: number;
8 | author?: string;
9 | relays?: string[];
10 | };
11 |
12 | export function TagList({ eventId, kind, author, relays }: TagListProps) {
13 | const { data: snippet } = useSnippetEvent(eventId, kind, author, relays);
14 |
15 | const tags = snippet?.tags
16 | .filter((tag) => tag[0] === "t")
17 | .map((tag) => tag[1]);
18 |
19 | if (!tags) {
20 | return null;
21 | }
22 |
23 | return (
24 | <>
25 | {tags.length > 0 && (
26 |
27 | {tags.map((tag) => (
28 |
32 | {tag}
33 |
34 | ))}
35 |
36 | )}
37 | >
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/features/editor/components/TagsInput.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { Input } from "~/components/ui/input";
5 | import { useAppState } from "~/store";
6 |
7 | export function TagsInput() {
8 | const setTags = useAppState((state) => state.setTags);
9 | const tags = useAppState((state) => state.tags);
10 |
11 | const [inputValue, setInputValue] = useState("");
12 |
13 | function handleChange(e: React.ChangeEvent) {
14 | const value = e.target.value;
15 | setInputValue(value);
16 | }
17 |
18 | function handleKeyDown(e: React.KeyboardEvent) {
19 | if (e.key === "Enter") {
20 | const trimmedValue = inputValue.trim();
21 | if (trimmedValue && !tags.includes(trimmedValue)) {
22 | setTags([...tags, trimmedValue]);
23 | setInputValue("");
24 | }
25 | }
26 | }
27 |
28 | return (
29 |
30 | ) => handleChange(e)}
35 | onKeyDown={(e: React.KeyboardEvent) =>
36 | handleKeyDown(e)
37 | }
38 | />
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/features/editor/hooks/useSnippetEvent.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { DEFAULT_RELAYS } from "~/lib/constants";
3 | import { SimplePool } from "nostr-tools";
4 |
5 | export async function getSnippet(
6 | eventId: string,
7 | relays?: string[],
8 | kind?: number,
9 | author?: string | undefined
10 | ) {
11 | if (!author) {
12 | return null;
13 | }
14 |
15 | const pool = new SimplePool();
16 |
17 | const snippetEvent = await pool.get(relays ?? DEFAULT_RELAYS, {
18 | kinds: [kind ?? 1337],
19 | authors: [author],
20 | ids: [eventId],
21 | });
22 |
23 | pool.close(relays ?? DEFAULT_RELAYS);
24 |
25 | return snippetEvent;
26 | }
27 |
28 | export const useSnippetEvent = (
29 | eventId: string,
30 | kind?: number,
31 | author?: string | undefined,
32 | relays?: string[]
33 | ) => {
34 | return useQuery({
35 | queryKey: ["snippet", kind, eventId, author],
36 | refetchOnWindowFocus: false,
37 | refetchOnMount: true,
38 | gcTime: Number.POSITIVE_INFINITY,
39 | staleTime: Number.POSITIVE_INFINITY,
40 | queryFn: () => getSnippet(eventId, relays, kind, author),
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/src/features/editor/index.ts:
--------------------------------------------------------------------------------
1 | export { FilenameInput } from "./components/FilenameInput";
2 | export { LanguageSelect } from "./components/LanguageSelect";
3 | export { DescriptionInput } from "./components/DescriptionInput";
4 | export { Filename } from "./components/Filename";
5 | export { Description } from "./components/Description";
6 | export { CopyButton } from "./components/CopyButton";
7 | export { ActiveEditor } from "./components/ActiveEditor";
8 | export { ReadEditor } from "./components/ReadEditor";
9 | export { TagsInput } from "./components/TagsInput";
10 | export { InputTagList } from "./components/InputTagList";
11 |
--------------------------------------------------------------------------------
/src/features/login/components/Login.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "~/components/ui/button";
4 | import { signIn } from "next-auth/react";
5 |
6 | type LoginProps = {
7 | children: React.ReactNode;
8 | };
9 |
10 | export function Login({ children }: LoginProps) {
11 | return (
12 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/features/login/components/UserDropdown.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "~/components/ui/button";
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuSeparator,
9 | DropdownMenuTrigger,
10 | } from "~/components/ui/dropdown-menu";
11 | import { Skeleton } from "~/components/ui/skeleton";
12 | import { useNostrProfile } from "~/hooks/useNostrProfile";
13 | import { signOut } from "next-auth/react";
14 | import Image from "next/image";
15 | import { getAvatar } from "~/lib/utils";
16 | import { shortenNpub } from "~/lib/nostr/shortNpub";
17 |
18 | type Props = {
19 | publicKey: string;
20 | };
21 |
22 | export function UserDropdown({ publicKey }: Props) {
23 | const nostrProfile = useNostrProfile(publicKey, true);
24 |
25 | console.log(nostrProfile.data);
26 |
27 | return (
28 |
29 |
30 |
48 |
49 |
50 |
51 | {/* */}
52 |
53 | {nostrProfile.data?.name ?? shortenNpub(publicKey)}
54 |
55 | {/* */}
56 |
57 |
58 | {/*
59 | Settings
60 | */}
61 | {/*
62 | Relays
63 | */}
64 | {/* */}
65 | {/* Stacks */}
66 | {/* */}
67 | {/* */}
68 | signOut()}>Logout
69 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/features/login/index.ts:
--------------------------------------------------------------------------------
1 | export { Login } from "./components/Login";
2 | export { UserDropdown } from "./components/UserDropdown";
3 |
--------------------------------------------------------------------------------
/src/features/navigation/components/ArchiveNavButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ArchiveIcon } from "lucide-react";
4 | import { Button } from "~/components/ui/button";
5 | import { useQueryClient } from "@tanstack/react-query";
6 | import { useRouter } from "next/navigation";
7 | import { useAppState } from "~/store";
8 |
9 | export function ArchiveNavButton() {
10 | const queryClient = useQueryClient();
11 | const router = useRouter();
12 | const setPageHistory = useAppState((state) => state.setPageHistory);
13 | const setCurrentPageIndex = useAppState((state) => state.setCurrentPageIndex);
14 |
15 | const handleClick = async () => {
16 | setPageHistory([]);
17 | setCurrentPageIndex(0);
18 | await queryClient.invalidateQueries({
19 | queryKey: ["snippets"],
20 | });
21 | router.push("/archive");
22 | };
23 |
24 | return (
25 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/features/navigation/components/CreateNavButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { PlusIcon } from "lucide-react";
4 | import { Button } from "~/components/ui/button";
5 | import { usePathname, useRouter } from "next/navigation";
6 | import { useAppState } from "~/store";
7 | import { TooltipContent, TooltipTrigger } from "~/components/ui/tooltip";
8 | import { Tooltip } from "~/components/ui/tooltip";
9 |
10 | export function CreateNavButton() {
11 | const router = useRouter();
12 | const pathname = usePathname();
13 |
14 | const content = useAppState((state) => state.content);
15 | const filename = useAppState((state) => state.filename);
16 | const description = useAppState((state) => state.description);
17 |
18 | function handleClick() {
19 | if (content !== "" || filename !== "" || description !== "") {
20 | if (pathname === "/") {
21 | const confirmed = window.confirm("Your changes will be lost.");
22 | if (!confirmed) {
23 | return;
24 | }
25 |
26 | useAppState.getState().setContent("");
27 | useAppState.getState().setFilename("");
28 | useAppState.getState().setDescription("");
29 | useAppState.getState().setLang("typescript");
30 |
31 | return;
32 | }
33 |
34 | return router.push("/");
35 | }
36 |
37 | return router.push("/");
38 | }
39 |
40 | return (
41 |
42 |
43 |
46 |
47 |
48 | Create a new snippet
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/features/navigation/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./components/ArchiveNavButton";
2 | export * from "./components/CreateNavButton";
3 |
--------------------------------------------------------------------------------
/src/features/post/components/PostButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { toast } from "sonner";
4 | import { Button } from "~/components/ui/button";
5 | import { cn } from "~/lib/utils";
6 | import { useAppState } from "~/store";
7 | import { usePostMutation } from "../hooks/usePostMutation";
8 | import { useNostrProfile } from "~/hooks/useNostrProfile";
9 |
10 | type PostButtonProps = {
11 | publicKey: string;
12 | secretKey?: string;
13 | };
14 |
15 | export function PostButton({ publicKey, secretKey }: PostButtonProps) {
16 | const content = useAppState((state) => state.content);
17 | const filename = useAppState((state) => state.filename);
18 | const lang = useAppState((state) => state.lang);
19 | const description = useAppState((state) => state.description);
20 | const tags = useAppState((state) => state.tags);
21 |
22 | const nostrProfile = useNostrProfile(publicKey, true);
23 | const { mutate: postSnippet, isPending } = usePostMutation();
24 |
25 | const handlePost = async () => {
26 | if (!filename) {
27 | toast.error("Please enter a filename");
28 | return;
29 | }
30 |
31 | postSnippet({
32 | content,
33 | filename,
34 | lang,
35 | description,
36 | publicKey,
37 | secretKey,
38 | writeRelays: nostrProfile.data?.writeRelays,
39 | tags,
40 | });
41 | };
42 |
43 | return (
44 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/src/features/post/hooks/usePostMutation.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from "@tanstack/react-query";
2 | import type { LanguageName } from "@uiw/codemirror-extensions-langs";
3 | import { type EventTemplate, nip19 } from "nostr-tools";
4 | import type { EventPointer } from "nostr-tools/nip19";
5 | import { redirectToSnippet } from "~/actions/redirectToSnippet";
6 | import { DEFAULT_RELAYS } from "~/lib/constants";
7 | import { finishEvent } from "~/lib/nostr/finishEvent";
8 | import { parseUint8Array } from "~/lib/nostr/parseUint8Array";
9 | import { publish } from "~/lib/nostr/publish";
10 | import { getExtension } from "~/lib/utils";
11 | import { useAppState } from "~/store";
12 |
13 | interface PostSnippetPayload {
14 | content: string;
15 | filename: string;
16 | lang: LanguageName;
17 | description?: string;
18 | publicKey: string;
19 | secretKey?: string;
20 | writeRelays?: string[];
21 | tags?: string[];
22 | }
23 |
24 | async function postSnippet(payload: PostSnippetPayload) {
25 | const tags: string[][] = [
26 | ["l", payload.lang],
27 | ["name", payload.filename],
28 | ["description", payload.description ?? ""],
29 | ];
30 |
31 | if (payload.tags) {
32 | tags.push(...payload.tags.map((tag) => ["t", tag]));
33 | }
34 |
35 | const extension = getExtension(payload.filename);
36 |
37 | if (extension) {
38 | tags.push(["extension", extension]);
39 | }
40 |
41 | const eventTemplate: EventTemplate = {
42 | kind: 1337,
43 | content: payload.content,
44 | tags: tags,
45 | created_at: Math.floor(Date.now() / 1000),
46 | };
47 |
48 | let relays: string[] = DEFAULT_RELAYS;
49 |
50 | if (payload.writeRelays) {
51 | relays = payload.writeRelays;
52 | }
53 |
54 | const secretKey = parseUint8Array(payload.secretKey);
55 |
56 | const event = await finishEvent(eventTemplate, secretKey);
57 |
58 | if (relays.length > 0 && event) {
59 | await publish(event, relays);
60 |
61 | const eventPointer: EventPointer = {
62 | id: event.id,
63 | relays: relays,
64 | author: event.pubkey,
65 | kind: event.kind,
66 | };
67 |
68 | const nevent = nip19.neventEncode(eventPointer);
69 | redirectToSnippet(nevent);
70 | useAppState.getState().setContent("");
71 | useAppState.getState().setFilename("");
72 | useAppState.getState().setDescription("");
73 | useAppState.getState().setLang("typescript");
74 | useAppState.getState().setTags([]);
75 | }
76 | }
77 |
78 | export function usePostMutation() {
79 | return useMutation({
80 | mutationFn: postSnippet,
81 | onError: (error) => {
82 | console.error("Failed to post snippet:", error);
83 | },
84 | });
85 | }
86 |
--------------------------------------------------------------------------------
/src/features/post/index.ts:
--------------------------------------------------------------------------------
1 | export { PostButton } from "./components/PostButton";
--------------------------------------------------------------------------------
/src/features/snippet-feed/components/SnippetCard.tsx:
--------------------------------------------------------------------------------
1 | import { githubLight } from "@uiw/codemirror-theme-github";
2 |
3 | import CodeMirror from "@uiw/react-codemirror";
4 | import { useTheme } from "next-themes";
5 | import { githubDark } from "@uiw/codemirror-theme-github";
6 | import type { NostrSnippet } from "~/lib/nostr/createNostrSnippet";
7 | import { useMemo } from "react";
8 | import {
9 | type LanguageName,
10 | loadLanguage,
11 | } from "@uiw/codemirror-extensions-langs";
12 | import Link from "next/link";
13 | import { createNevent } from "~/lib/nostr/createNevent";
14 | import { DEFAULT_RELAYS } from "~/lib/constants";
15 |
16 | type SnippetCardProps = {
17 | snippet: NostrSnippet;
18 | };
19 |
20 | export function SnippetCard({ snippet }: SnippetCardProps) {
21 | const { resolvedTheme } = useTheme();
22 |
23 | const languageExtension = useMemo(() => {
24 | const extension = loadLanguage(
25 | (snippet.language as LanguageName) || "markdown",
26 | );
27 | return extension ? [extension] : [];
28 | }, [snippet]);
29 |
30 | return (
31 |
32 |
33 |
34 | {snippet.name || "Untitled"}
35 |
36 |
37 |
38 |
39 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/src/features/snippet-feed/components/SnippetCardSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "~/components/ui/skeleton";
2 |
3 | export function SnippetCardSkeleton() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/features/snippet-feed/components/SnippetFeed.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useNostrSnippets } from "~/hooks/useNostrSnippets";
3 | import { Button } from "~/components/ui/button";
4 | import { SnippetCardSkeleton } from "./SnippetCardSkeleton";
5 | import { SnippetCard } from "./SnippetCard";
6 | import { useAppState } from "~/store";
7 | import { useEffect } from "react";
8 | import { useSearchParams } from "next/navigation";
9 | export function SnippetFeed() {
10 | const until = useAppState((state) => state.until);
11 | const setUntil = useAppState((state) => state.setUntil);
12 | const searchParams = useSearchParams();
13 |
14 | useEffect(() => {
15 | const untilParam = searchParams.get("until");
16 | if (untilParam) {
17 | setUntil(Number.parseInt(untilParam, 10));
18 | } else {
19 | setUntil(Math.floor(Date.now() / 1000));
20 | }
21 | }, [setUntil, searchParams]);
22 |
23 | const {
24 | data,
25 | isPending,
26 | isError,
27 | error,
28 | loadOlderEvents,
29 | loadNewerEvents,
30 | resetToFirstPage,
31 | hasOlderEvents,
32 | hasNewerEvents,
33 | currentPageIndex,
34 | } = useNostrSnippets();
35 | return (
36 |
37 | {isPending ? (
38 | <>
39 | {Array.from({ length: 10 }).map((_, index) => (
40 |
41 |
42 |
43 | ))}
44 | >
45 | ) : (
46 | data?.map((snippet) => (
47 |
48 |
49 |
50 | ))
51 | )}
52 |
53 | {hasNewerEvents ? (
54 |
61 | ) : (
62 |
65 | )}
66 |
67 | Page: {currentPageIndex + 1}
68 |
69 |
77 |
78 |
79 | );
80 | }
81 |
--------------------------------------------------------------------------------
/src/features/snippet-feed/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./components/SnippetCard";
2 | export * from "./components/SnippetFeed";
3 |
--------------------------------------------------------------------------------
/src/features/zap/components/ZapButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Button } from "~/components/ui/button";
4 | import { Zap } from "lucide-react";
5 | import { ZapDialog } from "./ZapDialog";
6 | import { useNostrProfile } from "~/hooks/useNostrProfile";
7 |
8 | type Props = {
9 | eventId: string;
10 | author: string;
11 | senderPubkey: string;
12 | };
13 |
14 | export function ZapButton({ eventId, author, senderPubkey }: Props) {
15 | const nostrProfile = useNostrProfile(author, false);
16 |
17 | if (!nostrProfile?.data?.lud16 || !senderPubkey) {
18 | return null;
19 | }
20 |
21 | return (
22 |
27 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/features/zap/components/ZapDialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 |
5 | import { Button } from "~/components/ui/button";
6 | import {
7 | Dialog,
8 | DialogContent,
9 | DialogFooter,
10 | DialogHeader,
11 | DialogTitle,
12 | DialogTrigger,
13 | } from "~/components/ui/dialog";
14 | import { Input } from "~/components/ui/input";
15 | import { Label } from "~/components/ui/label";
16 | import { DEFAULT_RELAYS } from "~/lib/constants";
17 | import { sendZap, type ZapRequest } from "../lib/zap";
18 | import type { Event } from "nostr-tools";
19 | import { toast } from "sonner";
20 |
21 | type Props = {
22 | children: React.ReactNode;
23 | recipientProfileEvent: Event | null | undefined;
24 | senderPubkey: string | null | undefined;
25 | eventId?: string;
26 | address?: string;
27 | };
28 |
29 | export function ZapDialog({
30 | children,
31 | recipientProfileEvent,
32 | senderPubkey,
33 | eventId,
34 | address,
35 | }: Props) {
36 | // create state to hold the amount of satoshis to send
37 | const [amount, setAmount] = useState("");
38 | const [message, setMessage] = useState("");
39 | const [open, setOpen] = useState(false);
40 |
41 | if (!recipientProfileEvent) {
42 | return null;
43 | }
44 |
45 | if (!senderPubkey) {
46 | return null;
47 | }
48 |
49 | // create function to set the amount of satoshis to send
50 | function setAmountToTip(amount: number) {
51 | setAmount(amount.toString());
52 | }
53 |
54 | async function handleSubmit() {
55 | if (amount === "" || Number.parseInt(amount, 10) === 0) {
56 | setAmount("");
57 | setMessage("");
58 | toast("Amount must be greater than 0", {
59 | description: "Please enter a valid amount.",
60 | });
61 | return;
62 | }
63 |
64 | if (!recipientProfileEvent) {
65 | toast("Error sending zap", {
66 | description: "There was an error sending your zap.",
67 | });
68 | return;
69 | }
70 |
71 | if (!senderPubkey) {
72 | toast("Error sending zap", {
73 | description: "There was an error sending your zap.",
74 | });
75 | return;
76 | }
77 |
78 | const zapRequest: ZapRequest = {
79 | recipientPubkey: recipientProfileEvent.pubkey,
80 | amount: Number.parseInt(amount, 10) * 1000,
81 | relays: DEFAULT_RELAYS,
82 | comment: message,
83 | senderPubkey,
84 | eventId: eventId,
85 | address: address,
86 | };
87 |
88 | try {
89 | await sendZap(zapRequest, recipientProfileEvent);
90 | toast("Zap sent", {
91 | description: "Your zap has been sent.",
92 | });
93 | } catch (e) {
94 | console.error("error sending zap", e);
95 | toast(" Error sending zap", {
96 | description: "There was an error sending your zap.",
97 | });
98 | return;
99 | }
100 | setOpen(false);
101 | setAmount("");
102 | setMessage("");
103 | }
104 |
105 | // Reset the fields when the dialog is closed
106 | function handleDialogOpenChange(isOpen: boolean) {
107 | if (!isOpen) {
108 | setAmount("");
109 | setMessage("");
110 | }
111 | setOpen(isOpen);
112 | }
113 |
114 | return (
115 |
189 | );
190 | }
191 |
--------------------------------------------------------------------------------
/src/features/zap/index.ts:
--------------------------------------------------------------------------------
1 | export { ZapButton } from "./components/ZapButton";
--------------------------------------------------------------------------------
/src/features/zap/lib/zap.ts:
--------------------------------------------------------------------------------
1 | import { getUser } from "~/actions/auth";
2 | import { finishEventWithSecretKey } from "~/lib/nostr/finishEvent";
3 | import type { Event, EventTemplate } from "nostr-tools";
4 | import { getZapEndpoint } from "nostr-tools/nip57";
5 |
6 | import { finishEventWithExtension } from "~/lib/nostr/finishEvent";
7 | import { getTagValue } from "~/lib/nostr/getTagValue";
8 | import { parseUint8Array } from "~/lib/nostr/parseUint8Array";
9 | export interface InvoiceResponse {
10 | pr: string;
11 | }
12 |
13 | export type ZapRequest = {
14 | recipientPubkey: string;
15 | amount: number;
16 | relays: string[];
17 | comment?: string;
18 | senderPubkey?: string;
19 | eventId?: string | null;
20 | address?: string;
21 | lnurl?: string;
22 | };
23 |
24 | export function makeZapRequest(zapRequest: ZapRequest) {
25 | const {
26 | recipientPubkey,
27 | amount,
28 | relays,
29 | comment,
30 | senderPubkey,
31 | eventId,
32 | address,
33 | lnurl,
34 | } = zapRequest;
35 |
36 | if (!amount) throw new Error("amount not given");
37 | if (!recipientPubkey) throw new Error("profile not given");
38 | if (!relays) throw new Error("relays not given");
39 |
40 | const zr: EventTemplate = {
41 | kind: 9734,
42 | created_at: Math.round(Date.now() / 1000),
43 | content: comment ?? "",
44 | tags: [
45 | ["p", recipientPubkey],
46 | ["amount", amount.toString()],
47 | ["relays", ...relays],
48 | ],
49 | };
50 |
51 | if (eventId) {
52 | zr.tags.push(["e", eventId]);
53 | }
54 |
55 | if (address) {
56 | zr.tags.push(["a", address]);
57 | }
58 |
59 | if (lnurl) {
60 | zr.tags.push(["lnurl", lnurl]);
61 | }
62 |
63 | if (senderPubkey) {
64 | zr.tags.push(["P", senderPubkey]);
65 | }
66 |
67 | return zr;
68 | }
69 |
70 | const weblnConnect = async () => {
71 | try {
72 | if (typeof window.webln !== "undefined") {
73 | await window.webln.enable();
74 | return true;
75 | }
76 | return false;
77 | } catch (e) {
78 | console.error(e);
79 | return false;
80 | }
81 | };
82 |
83 | const fetchInvoice = async (zapEndpoint: string, zapRequestEvent: Event) => {
84 | const comment = zapRequestEvent.content;
85 | const amount = getTagValue(zapRequestEvent, "amount");
86 | if (!amount) throw new Error("amount not found");
87 |
88 | let url = `${zapEndpoint}?amount=${amount}&nostr=${encodeURIComponent(
89 | JSON.stringify(zapRequestEvent)
90 | )}`;
91 |
92 | if (comment) {
93 | url = `${url}&comment=${encodeURIComponent(comment)}`;
94 | }
95 |
96 | const res = await fetch(url);
97 | const { pr: invoice } = (await res.json()) as InvoiceResponse;
98 |
99 | return invoice;
100 | };
101 |
102 | const payInvoice = async (invoice: string) => {
103 | const weblnConnected = await weblnConnect();
104 | if (!weblnConnected) throw new Error("webln not available");
105 |
106 | const webln = window.webln;
107 |
108 | if (!webln) throw new Error("webln not available");
109 |
110 | const paymentRequest = invoice;
111 |
112 | const paymentResponse = await webln.sendPayment(paymentRequest);
113 |
114 | if (!paymentResponse) throw new Error("payment response not found");
115 |
116 | return paymentResponse;
117 | };
118 |
119 | export const sendZap = async (zapRequest: ZapRequest, profileEvent: Event) => {
120 | const zapRequestEventTemplate = makeZapRequest(zapRequest);
121 |
122 | let zapRequestEvent: Event | null = null;
123 |
124 | const user = await getUser();
125 |
126 | if (user?.secretKey === "0") {
127 | zapRequestEvent = await finishEventWithExtension(zapRequestEventTemplate);
128 | } else {
129 | if (user?.secretKey) {
130 | const secretKey = parseUint8Array(user.secretKey);
131 | if (!secretKey) throw new Error("secret key not found");
132 | zapRequestEvent = await finishEventWithSecretKey(
133 | zapRequestEventTemplate,
134 | secretKey
135 | );
136 | } else {
137 | console.error("User not found");
138 | throw new Error("user not found");
139 | }
140 | }
141 |
142 | if (!zapRequestEvent) throw new Error("zap request event not created");
143 |
144 | // this needs to be a profile event
145 | const zapEndpoint = await getZapEndpoint(profileEvent);
146 |
147 | if (!zapEndpoint) throw new Error("zap endpoint not found");
148 |
149 | const invoice = await fetchInvoice(zapEndpoint, zapRequestEvent);
150 |
151 | if (!invoice) throw new Error("invoice not found");
152 |
153 | try {
154 | return await payInvoice(invoice);
155 | } catch (err) {
156 | console.error(err);
157 | throw new Error("zap failed");
158 | }
159 | };
160 |
--------------------------------------------------------------------------------
/src/hooks/useNostrProfile.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { DEFAULT_RELAYS } from "~/lib/constants";
3 | import { SimplePool } from "nostr-tools";
4 | import { createNostrProfile } from "~/lib/nostr/createNostrProfile";
5 | import type { NostrProfile } from "~/lib/nostr/createNostrProfile";
6 | import { createNostrRelayMetadata } from "~/lib/nostr/createNostrRelayMetadata";
7 |
8 | export async function getNostrProfile(
9 | publicKey?: string,
10 | checkMetadata?: boolean,
11 | ) {
12 | if (!publicKey) return null;
13 |
14 | const pool = new SimplePool();
15 |
16 | let relays: string[] = [];
17 |
18 | let writeRelays: string[] = [];
19 | let readRelays: string[] = [];
20 |
21 | if (checkMetadata) {
22 | const relayEvent = await pool.get(DEFAULT_RELAYS, {
23 | kinds: [10002],
24 | authors: [publicKey],
25 | });
26 |
27 | const nostrRelayMetadata = createNostrRelayMetadata(relayEvent);
28 | // make sure the relays are unique
29 | const uniqueRelays = [
30 | ...new Set([...DEFAULT_RELAYS, ...nostrRelayMetadata.writeRelays]),
31 | ];
32 | relays = uniqueRelays;
33 | writeRelays = nostrRelayMetadata.writeRelays;
34 | readRelays = nostrRelayMetadata.readRelays;
35 | }
36 |
37 | if (relays.length === 0) {
38 | relays = DEFAULT_RELAYS;
39 | }
40 |
41 | const profileEvent = await pool.get(relays, {
42 | kinds: [0],
43 | authors: [publicKey],
44 | });
45 |
46 | pool.close(relays);
47 |
48 | if (!profileEvent) return null;
49 |
50 | const nostrProfile = createNostrProfile(
51 | profileEvent,
52 | writeRelays,
53 | readRelays,
54 | );
55 |
56 | return nostrProfile;
57 | }
58 |
59 | export const useNostrProfile = (publicKey: string, checkMetadata = false) => {
60 | return useQuery({
61 | queryKey: ["profile", publicKey, checkMetadata],
62 | refetchOnWindowFocus: false,
63 | refetchOnMount: true,
64 | staleTime: Number.POSITIVE_INFINITY,
65 | gcTime: Number.POSITIVE_INFINITY,
66 | enabled: !!publicKey,
67 | queryFn: () => getNostrProfile(publicKey, checkMetadata),
68 | });
69 | };
70 |
--------------------------------------------------------------------------------
/src/hooks/useNostrRelayMetaData.ts:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@tanstack/react-query";
2 | import { DEFAULT_RELAYS } from "~/lib/constants";
3 | import { SimplePool } from "nostr-tools";
4 | import {
5 | createNostrRelayMetadata,
6 | type NostrRelayMetadata,
7 | } from "~/lib/nostr/createNostrRelayMetadata";
8 |
9 | export async function getNostrRelayMetadata(
10 | publicKey: string,
11 | relays?: string[],
12 | ) {
13 | const pool = new SimplePool();
14 |
15 | const relayEvent = await pool.get(relays ?? DEFAULT_RELAYS, {
16 | kinds: [10002],
17 | authors: [publicKey],
18 | });
19 |
20 | pool.close(relays ?? DEFAULT_RELAYS);
21 |
22 | return createNostrRelayMetadata(relayEvent);
23 | }
24 |
25 | export const useNostrRelayMetadata = (publicKey: string, relays?: string[]) => {
26 | return useQuery({
27 | queryKey: ["nostrRelayMetadata", publicKey],
28 | refetchOnWindowFocus: false,
29 | refetchOnMount: true,
30 | gcTime: Number.POSITIVE_INFINITY,
31 | staleTime: Number.POSITIVE_INFINITY,
32 | queryFn: () => getNostrRelayMetadata(publicKey, relays),
33 | enabled: !!publicKey,
34 | });
35 | };
36 |
--------------------------------------------------------------------------------
/src/hooks/useNostrSnippets.ts:
--------------------------------------------------------------------------------
1 | import { useQuery, useQueryClient } from "@tanstack/react-query";
2 | import { DEFAULT_RELAYS } from "~/lib/constants";
3 | import { SimplePool } from "nostr-tools";
4 | import { createNostrSnippet } from "~/lib/nostr/createNostrSnippet";
5 | import { useAppState } from "~/store";
6 | import { useRouter, useSearchParams } from "next/navigation";
7 |
8 | export const useNostrSnippets = () => {
9 | const queryClient = useQueryClient();
10 | const router = useRouter();
11 | const searchParams = useSearchParams();
12 | const limit = 10;
13 |
14 | const pageHistory = useAppState((state) => state.pageHistory);
15 | const setPageHistory = useAppState((state) => state.setPageHistory);
16 | const currentPageIndex = useAppState((state) => state.currentPageIndex);
17 | const setCurrentPageIndex = useAppState((state) => state.setCurrentPageIndex);
18 | const until = useAppState((state) => state.until);
19 | const setUntil = useAppState((state) => state.setUntil);
20 |
21 | const fetchPage = async () => {
22 | if (!until) {
23 | return null;
24 | }
25 | const pool = new SimplePool();
26 |
27 | // Query for events
28 | const events = await pool.querySync(DEFAULT_RELAYS, {
29 | kinds: [1337],
30 | limit,
31 | until,
32 | });
33 |
34 | // Convert events to snippets
35 | let snippets = events.map((event) => createNostrSnippet(event));
36 |
37 | // Close the pool connection
38 | pool.close(DEFAULT_RELAYS);
39 |
40 | // Limit to requested number of events
41 | snippets = snippets.slice(0, limit);
42 |
43 | // Sort the events by created_at in descending order (newest first)
44 | snippets.sort((a, b) => b.createdAt - a.createdAt);
45 |
46 | return snippets;
47 | };
48 |
49 | const queryResult = useQuery({
50 | queryKey: ["snippets", until, pageHistory, currentPageIndex],
51 | queryFn: fetchPage,
52 | refetchOnWindowFocus: false,
53 | refetchOnMount: true,
54 | });
55 |
56 | // Function to load the next (older) page of events
57 | const loadOlderEvents = () => {
58 | if (queryResult.data && queryResult.data.length > 0) {
59 | // Get the oldest timestamp from the current page
60 | const oldestSnippet = queryResult.data[queryResult.data.length - 1];
61 | const newUntil = oldestSnippet.createdAt - 1; // -1 to avoid getting the same event again
62 |
63 | if (until) {
64 | setPageHistory([...pageHistory, until]);
65 | }
66 | setUntil(newUntil);
67 | const params = new URLSearchParams(searchParams.toString());
68 | params.set("until", newUntil.toString());
69 | router.push(`?${params.toString()}`);
70 | setCurrentPageIndex(currentPageIndex + 1);
71 | }
72 | };
73 |
74 | // Function to load newer events (previous page)
75 | const loadNewerEvents = () => {
76 | if (currentPageIndex > 0) {
77 | // Move back one page in history
78 | setCurrentPageIndex(currentPageIndex - 1);
79 |
80 | const previousPageUntil =
81 | currentPageIndex === 0 ? undefined : pageHistory[currentPageIndex - 1];
82 |
83 | if (previousPageUntil) {
84 | setPageHistory(pageHistory.slice(0, currentPageIndex - 1));
85 | setUntil(previousPageUntil);
86 | const params = new URLSearchParams(searchParams.toString());
87 | params.set("until", previousPageUntil.toString());
88 | router.push(`?${params.toString()}`);
89 | }
90 | }
91 | };
92 |
93 | // Reset to first page
94 | const resetToFirstPage = () => {
95 | setUntil(Math.floor(Date.now() / 1000));
96 | setPageHistory([]);
97 | setCurrentPageIndex(0);
98 | queryClient.invalidateQueries({ queryKey: ["snippets"] });
99 | };
100 |
101 | return {
102 | ...queryResult,
103 | loadOlderEvents,
104 | loadNewerEvents,
105 | resetToFirstPage,
106 | hasOlderEvents: queryResult.data && queryResult.data.length === limit,
107 | hasNewerEvents: currentPageIndex > 0,
108 | currentPageIndex,
109 | };
110 | };
111 |
--------------------------------------------------------------------------------
/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_RELAYS = ["wss://relay.notebin.io"];
2 |
--------------------------------------------------------------------------------
/src/lib/languages.ts:
--------------------------------------------------------------------------------
1 | import type { LanguageName } from "@uiw/codemirror-extensions-langs";
2 |
3 | export const languages: {
4 | displayName: string;
5 | value: LanguageName;
6 | }[] = [
7 | { displayName: "Bash", value: "shell" },
8 | { displayName: "C", value: "c" },
9 | { displayName: "C#", value: "csharp" },
10 | { displayName: "CSS", value: "css" },
11 | { displayName: "Docker", value: "dockerfile" },
12 | { displayName: "Elm", value: "elm" },
13 | { displayName: "Erlang", value: "erlang" },
14 | { displayName: "Go", value: "go" },
15 | { displayName: "Haskell", value: "haskell" },
16 | { displayName: "HTML", value: "html" },
17 | { displayName: "Java", value: "java" },
18 | { displayName: "JavaScript", value: "javascript" },
19 | { displayName: "JSON", value: "json" },
20 | { displayName: "JSX", value: "jsx" },
21 | { displayName: "Kotlin", value: "kotlin" },
22 | { displayName: "Lua", value: "lua" },
23 | { displayName: "Markdown", value: "markdown" },
24 | { displayName: "Powershell", value: "powershell" },
25 | { displayName: "PHP", value: "php" },
26 | { displayName: "Python", value: "python" },
27 | { displayName: "R", value: "r" },
28 | { displayName: "Ruby", value: "ruby" },
29 | { displayName: "Rust", value: "rust" },
30 | { displayName: "Scala", value: "scala" },
31 | { displayName: "Solidity", value: "solidity" },
32 | { displayName: "SQL", value: "sql" },
33 | { displayName: "Swift", value: "swift" },
34 | { displayName: "Svelte", value: "svelte" },
35 | { displayName: "TOML", value: "toml" },
36 | { displayName: "TypeScript", value: "typescript" },
37 | { displayName: "TSX", value: "tsx" },
38 | { displayName: "Vue", value: "vue" },
39 | { displayName: "XML", value: "xml" },
40 | { displayName: "YAML", value: "yaml" },
41 | ];
42 |
--------------------------------------------------------------------------------
/src/lib/nostr/createNevent.ts:
--------------------------------------------------------------------------------
1 | import { nip19 } from "nostr-tools";
2 | import type { Event } from "nostr-tools";
3 | import type { EventPointer } from "nostr-tools/nip19";
4 |
5 | export function createNevent(event: Event, relays: string[]) {
6 | const eventPointer: EventPointer = {
7 | id: event.id,
8 | relays: relays,
9 | author: event.pubkey,
10 | kind: event.kind,
11 | };
12 |
13 | const nevent = nip19.neventEncode(eventPointer);
14 |
15 | return nevent;
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/nostr/createNostrProfile.ts:
--------------------------------------------------------------------------------
1 | import type { Event } from "nostr-tools";
2 | import { nip19 } from "nostr-tools";
3 | import { getAvatar } from "~/lib/utils";
4 |
5 | export interface NostrProfile {
6 | event: Event;
7 | pubkey: string;
8 | npub?: string;
9 | shortNpub?: string;
10 | writeRelays?: string[];
11 | readRelays?: string[];
12 | relay?: string;
13 | about?: string;
14 | lud06?: string;
15 | lud16?: string;
16 | name?: string;
17 | nip05?: string;
18 | picture?: string;
19 | website?: string;
20 | banner?: string;
21 | location?: string;
22 | github?: string;
23 | twitter?: string;
24 | [key: string]: unknown;
25 | }
26 |
27 | export const createNostrProfile = (
28 | event: Event,
29 | writeRelays?: string[],
30 | readRelays?: string[],
31 | ): NostrProfile => {
32 | if (!event) {
33 | throw new Error("Event is required");
34 | }
35 |
36 | const npub = nip19.npubEncode(event.pubkey);
37 | const shortNpub = `${npub.slice(0, 4)}..${npub.slice(-4)}`;
38 |
39 | // Initialize base profile with required fields
40 | const profileEvent: NostrProfile = {
41 | event,
42 | pubkey: event.pubkey,
43 | npub,
44 | shortNpub,
45 | writeRelays,
46 | readRelays,
47 | };
48 |
49 | try {
50 | // Parse the content and ensure it's an object
51 | const content = event.content ? JSON.parse(event.content) : {};
52 | if (typeof content !== "object" || content === null) {
53 | throw new Error("Invalid profile content format");
54 | }
55 |
56 | // Safely merge the parsed content with the base profile
57 | for (const [key, value] of Object.entries(content)) {
58 | if (value !== undefined && value !== null) {
59 | profileEvent[key] = value;
60 | }
61 | }
62 |
63 | if (!profileEvent.picture) {
64 | profileEvent.picture = getAvatar(event.pubkey);
65 | }
66 | return profileEvent;
67 | } catch (err) {
68 | console.error(
69 | "Error parsing profile content:",
70 | err instanceof Error ? err.message : "Unknown error",
71 | );
72 | if (!profileEvent.picture) {
73 | profileEvent.picture = getAvatar(event.pubkey);
74 | }
75 | return profileEvent;
76 | }
77 | };
78 |
--------------------------------------------------------------------------------
/src/lib/nostr/createNostrRelayMetadata.ts:
--------------------------------------------------------------------------------
1 | import type { Event } from "nostr-tools";
2 |
3 | export interface NostrRelayMetadata {
4 | event: Event | null;
5 | relays: string[];
6 | readRelays: string[];
7 | writeRelays: string[];
8 | }
9 |
10 | export function createNostrRelayMetadata(event: Event | null) {
11 | if (!event) {
12 | return {
13 | event: null,
14 | relays: [],
15 | readRelays: [],
16 | writeRelays: [],
17 | };
18 | }
19 |
20 | const relays = event?.tags?.map((tag) => tag[1] ?? "").filter(Boolean) ?? [];
21 |
22 | const readRelays =
23 | event?.tags
24 | ?.filter((tag) => tag[2] !== "write")
25 | .map((tag) => tag[1] ?? "")
26 | .filter(Boolean) ?? [];
27 |
28 | const writeRelays =
29 | event?.tags
30 | ?.filter((tag) => tag[2] !== "read")
31 | .map((tag) => tag[1] ?? "")
32 | .filter(Boolean) ?? [];
33 |
34 | return { event, relays, readRelays, writeRelays };
35 | }
36 |
--------------------------------------------------------------------------------
/src/lib/nostr/createNostrSnippet.ts:
--------------------------------------------------------------------------------
1 | import type { Event } from "nostr-tools";
2 |
3 | export interface NostrSnippet {
4 | event: Event;
5 | content: string;
6 | createdAt: number;
7 | language?: string;
8 | name?: string;
9 | description?: string;
10 | extension?: string;
11 | runtime?: string;
12 | license?: string;
13 | dependencies?: string[];
14 | repo?: string;
15 | }
16 |
17 | export function createNostrSnippet(event: Event) {
18 | const snippet: NostrSnippet = {
19 | event,
20 | content: event.content,
21 | createdAt: event.created_at,
22 | language: event.tags.find((tag) => tag[0] === "l")?.[1],
23 | name: event.tags.find((tag) => tag[0] === "name")?.[1],
24 | description: event.tags.find((tag) => tag[0] === "description")?.[1],
25 | extension: event.tags.find((tag) => tag[0] === "extension")?.[1],
26 | runtime: event.tags.find((tag) => tag[0] === "runtime")?.[1],
27 | license: event.tags.find((tag) => tag[0] === "license")?.[1],
28 | };
29 |
30 | return snippet;
31 | }
32 |
--------------------------------------------------------------------------------
/src/lib/nostr/finishEvent.ts:
--------------------------------------------------------------------------------
1 | import {
2 | finalizeEvent,
3 | type EventTemplate,
4 | type Event,
5 | getEventHash,
6 | } from "nostr-tools";
7 |
8 | export const finishEventWithSecretKey = (
9 | eventTemplate: EventTemplate,
10 | secretKey: Uint8Array
11 | ) => {
12 | const event = finalizeEvent(eventTemplate, secretKey);
13 |
14 | return event;
15 | };
16 |
17 | export async function finishEventWithExtension(t: EventTemplate) {
18 | let event = t as Event;
19 | try {
20 | if (nostr) {
21 | event.pubkey = await nostr.getPublicKey();
22 | event.id = getEventHash(event);
23 | event = (await nostr.signEvent(event)) as Event;
24 | return event;
25 | }
26 | console.error("nostr not defined");
27 | return null;
28 | } catch (err) {
29 | console.error("Error signing event", err);
30 | return null;
31 | }
32 | }
33 |
34 | export async function finishEvent(
35 | eventTemplate: EventTemplate,
36 | secretKey?: Uint8Array
37 | ) {
38 | let event: Event | null = null;
39 | console.log("secretKey", secretKey);
40 | if (secretKey) {
41 | event = finishEventWithSecretKey(eventTemplate, secretKey);
42 | } else {
43 | event = await finishEventWithExtension(eventTemplate);
44 | }
45 |
46 | return event;
47 | }
48 |
--------------------------------------------------------------------------------
/src/lib/nostr/getTagValue.ts:
--------------------------------------------------------------------------------
1 | import type { Event } from "nostr-tools";
2 |
3 | export function getTagValue(event?: Event | null, tag?: string | null) {
4 | if (!event || !tag) {
5 | return null;
6 | }
7 |
8 | return event.tags.find((t) => t[0] === tag)?.[1];
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/nostr/parseUint8Array.ts:
--------------------------------------------------------------------------------
1 | export const parseUint8Array = (secretKeyString: string | undefined) => {
2 | console.log("secretKeyString", secretKeyString);
3 | if (!secretKeyString) {
4 | return undefined;
5 | }
6 |
7 | if (secretKeyString === "0") {
8 | return undefined;
9 | }
10 |
11 | const numbersArray = secretKeyString.split(",").map((num) =>
12 | Number.parseInt(num, 10)
13 | );
14 | const uint8Array = new Uint8Array(numbersArray);
15 |
16 | console.log("secretKey parsed", uint8Array);
17 |
18 | return uint8Array;
19 | };
20 |
--------------------------------------------------------------------------------
/src/lib/nostr/publish.ts:
--------------------------------------------------------------------------------
1 | import { SimplePool } from "nostr-tools";
2 | import type { Event } from "nostr-tools";
3 |
4 | export async function publish(event: Event, relays: string[]) {
5 | if (!event) {
6 | return false;
7 | }
8 |
9 | const pool = new SimplePool();
10 |
11 | await Promise.any(pool.publish(relays, event));
12 |
13 | const retrievedEvent = await pool.get(relays, {
14 | ids: [event.id],
15 | });
16 |
17 | pool.close(relays);
18 |
19 | if (!retrievedEvent) {
20 | return false;
21 | }
22 |
23 | return true;
24 | }
25 |
--------------------------------------------------------------------------------
/src/lib/nostr/shortNpub.ts:
--------------------------------------------------------------------------------
1 | import { nip19 } from "nostr-tools";
2 |
3 | export const shortenNpub = (pubkey: string | undefined, length = 4) => {
4 | console.log("pubkey", pubkey);
5 | if (!pubkey) return undefined;
6 |
7 | try {
8 | const npub = nip19.npubEncode(pubkey);
9 | return `npub..${npub.substring(npub.length - length)}`;
10 | } catch (error) {
11 | console.error("Failed to encode npub:", error);
12 | }
13 |
14 | return undefined;
15 | };
16 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { botttsNeutral } from "@dicebear/collection";
2 | import { createAvatar } from "@dicebear/core";
3 | import { clsx, type ClassValue } from "clsx";
4 | import { twMerge } from "tailwind-merge";
5 |
6 | export function cn(...inputs: ClassValue[]) {
7 | return twMerge(clsx(inputs));
8 | }
9 |
10 | export function getAvatar(seed: string | undefined) {
11 | return createAvatar(botttsNeutral, {
12 | seed: seed ?? "",
13 | }).toDataUri();
14 | }
15 |
16 | export function getExtension(filename: string) {
17 | // Find the position of the first dot
18 | const firstDotPos = filename.indexOf(".");
19 |
20 | // No extension
21 | if (firstDotPos === -1) {
22 | return undefined;
23 | }
24 |
25 | // Return everything after the first dot
26 | return filename.substring(firstDotPos + 1);
27 | }
28 |
--------------------------------------------------------------------------------
/src/providers/auth-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SessionProvider } from "next-auth/react";
4 |
5 | export default function AuthProvider({
6 | children,
7 | }: {
8 | children: React.ReactNode;
9 | }) {
10 | return {children};
11 | }
12 |
--------------------------------------------------------------------------------
/src/providers/query-client-provider.tsx:
--------------------------------------------------------------------------------
1 | // In Next.js, this file would be called: app/providers.jsx
2 | "use client";
3 |
4 | // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top
5 | import {
6 | isServer,
7 | QueryClient,
8 | QueryClientProvider,
9 | } from "@tanstack/react-query";
10 |
11 | function makeQueryClient() {
12 | return new QueryClient({
13 | defaultOptions: {
14 | queries: {
15 | // With SSR, we usually want to set some default staleTime
16 | // above 0 to avoid refetching immediately on the client
17 | staleTime: 60 * 1000,
18 | },
19 | },
20 | });
21 | }
22 |
23 | let browserQueryClient: QueryClient | undefined = undefined;
24 |
25 | function getQueryClient() {
26 | if (isServer) {
27 | // Server: always make a new query client
28 | return makeQueryClient();
29 | }
30 | // Browser: make a new query client if we don't already have one
31 | // This is very important, so we don't re-make a new client if React
32 | // suspends during the initial render. This may not be needed if we
33 | // have a suspense boundary BELOW the creation of the query client
34 | if (!browserQueryClient) browserQueryClient = makeQueryClient();
35 | return browserQueryClient;
36 | }
37 |
38 | export default function QueryClientProviderWrapper({
39 | children,
40 | }: {
41 | children: React.ReactNode;
42 | }) {
43 | // NOTE: Avoid useState when initializing the query client if you don't
44 | // have a suspense boundary between this and the code that may
45 | // suspend because React will throw away the client on the initial
46 | // render if it suspends and there is no boundary
47 | const queryClient = getQueryClient();
48 |
49 | return (
50 | {children}
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 |
6 | export function ThemeProvider({
7 | children,
8 | ...props
9 | }: React.ComponentProps) {
10 | return {children};
11 | }
12 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { createJSONStorage, persist } from "zustand/middleware";
3 | import type { LanguageName } from "@uiw/codemirror-extensions-langs";
4 |
5 | interface State {
6 | content: string;
7 | setContent: (content: string) => void;
8 |
9 | filename: string;
10 | setFilename: (filename: string) => void;
11 |
12 | lang: LanguageName;
13 | setLang: (lang: LanguageName) => void;
14 |
15 | description: string;
16 | setDescription: (description: string) => void;
17 |
18 | tags: string[];
19 | setTags: (tags: string[]) => void;
20 |
21 | until: number | undefined;
22 | setUntil: (until: number) => void;
23 |
24 | pageHistory: number[];
25 | setPageHistory: (pageHistory: number[]) => void;
26 |
27 | currentPageIndex: number;
28 | setCurrentPageIndex: (currentPageIndex: number) => void;
29 | }
30 |
31 | export const useAppState = create()(
32 | persist(
33 | (set) => ({
34 | content: "",
35 | setContent: (content) => set({ content }),
36 |
37 | filename: "",
38 | setFilename: (filename) => set({ filename }),
39 |
40 | lang: "typescript",
41 | setLang: (lang) => set({ lang }),
42 |
43 | description: "",
44 | setDescription: (description) => set({ description }),
45 |
46 | tags: [],
47 | setTags: (tags) => set({ tags }),
48 |
49 | until: undefined,
50 | setUntil: (until) => set({ until }),
51 |
52 | pageHistory: [],
53 | setPageHistory: (pageHistory) => set({ pageHistory }),
54 |
55 | currentPageIndex: 0,
56 | setCurrentPageIndex: (currentPageIndex) => set({ currentPageIndex }),
57 | }),
58 | {
59 | name: "notebin-storage",
60 | storage: createJSONStorage(() => localStorage),
61 | partialize: (state) => ({
62 | content: state.content,
63 | filename: state.filename,
64 | lang: state.lang,
65 | description: state.description,
66 | tags: state.tags,
67 | }),
68 | }
69 | )
70 | );
71 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "tw-animate-css";
3 |
4 | @custom-variant dark (&:is(.dark *));
5 |
6 | @theme inline {
7 | --color-background: var(--background);
8 | --color-foreground: var(--foreground);
9 | --font-sans: var(--font-geist-sans);
10 | --font-mono: var(--font-geist-mono);
11 | --color-sidebar-ring: var(--sidebar-ring);
12 | --color-sidebar-border: var(--sidebar-border);
13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14 | --color-sidebar-accent: var(--sidebar-accent);
15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16 | --color-sidebar-primary: var(--sidebar-primary);
17 | --color-sidebar-foreground: var(--sidebar-foreground);
18 | --color-sidebar: var(--sidebar);
19 | --color-chart-5: var(--chart-5);
20 | --color-chart-4: var(--chart-4);
21 | --color-chart-3: var(--chart-3);
22 | --color-chart-2: var(--chart-2);
23 | --color-chart-1: var(--chart-1);
24 | --color-ring: var(--ring);
25 | --color-input: var(--input);
26 | --color-border: var(--border);
27 | --color-destructive: var(--destructive);
28 | --color-accent-foreground: var(--accent-foreground);
29 | --color-accent: var(--accent);
30 | --color-muted-foreground: var(--muted-foreground);
31 | --color-muted: var(--muted);
32 | --color-secondary-foreground: var(--secondary-foreground);
33 | --color-secondary: var(--secondary);
34 | --color-primary-foreground: var(--primary-foreground);
35 | --color-primary: var(--primary);
36 | --color-popover-foreground: var(--popover-foreground);
37 | --color-popover: var(--popover);
38 | --color-card-foreground: var(--card-foreground);
39 | --color-card: var(--card);
40 | --radius-sm: calc(var(--radius) - 4px);
41 | --radius-md: calc(var(--radius) - 2px);
42 | --radius-lg: var(--radius);
43 | --radius-xl: calc(var(--radius) + 4px);
44 | }
45 |
46 | :root {
47 | --radius: 0.625rem;
48 | --background: oklch(1 0 0);
49 | --foreground: oklch(0.141 0.005 285.823);
50 | --card: oklch(1 0 0);
51 | --card-foreground: oklch(0.141 0.005 285.823);
52 | --popover: oklch(1 0 0);
53 | --popover-foreground: oklch(0.141 0.005 285.823);
54 | --primary: oklch(0.623 0.214 259.815);
55 | --primary-foreground: oklch(0.985 0 0);
56 | --secondary: oklch(0.967 0.001 286.375);
57 | --secondary-foreground: oklch(0.21 0.006 285.885);
58 | --muted: oklch(0.967 0.001 286.375);
59 | --muted-foreground: oklch(0.552 0.016 285.938);
60 | --accent: oklch(0.967 0.001 286.375);
61 | --accent-foreground: oklch(0.21 0.006 285.885);
62 | --destructive: oklch(0.577 0.245 27.325);
63 | --border: oklch(0.92 0.004 286.32);
64 | --input: oklch(0.92 0.004 286.32);
65 | --ring: oklch(0.705 0.015 286.067);
66 | --chart-1: oklch(0.646 0.222 41.116);
67 | --chart-2: oklch(0.6 0.118 184.704);
68 | --chart-3: oklch(0.398 0.07 227.392);
69 | --chart-4: oklch(0.828 0.189 84.429);
70 | --chart-5: oklch(0.769 0.188 70.08);
71 | --sidebar: oklch(0.985 0 0);
72 | --sidebar-foreground: oklch(0.141 0.005 285.823);
73 | --sidebar-primary: oklch(0.21 0.006 285.885);
74 | --sidebar-primary-foreground: oklch(0.985 0 0);
75 | --sidebar-accent: oklch(0.967 0.001 286.375);
76 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
77 | --sidebar-border: oklch(0.92 0.004 286.32);
78 | --sidebar-ring: oklch(0.705 0.015 286.067);
79 | }
80 |
81 | .dark {
82 | --background: oklch(0.141 0.005 285.823);
83 | --foreground: oklch(0.985 0 0);
84 | --card: oklch(0.21 0.006 285.885);
85 | --card-foreground: oklch(0.985 0 0);
86 | --popover: oklch(0.21 0.006 285.885);
87 | --popover-foreground: oklch(0.985 0 0);
88 | --primary: oklch(0.546 0.245 262.881);
89 | --primary-foreground: oklch(0.985 0 0);
90 | --secondary: oklch(0.274 0.006 286.033);
91 | --secondary-foreground: oklch(0.985 0 0);
92 | --muted: oklch(0.274 0.006 286.033);
93 | --muted-foreground: oklch(0.705 0.015 286.067);
94 | --accent: oklch(0.274 0.006 286.033);
95 | --accent-foreground: oklch(0.985 0 0);
96 | --destructive: oklch(0.704 0.191 22.216);
97 | --border: oklch(1 0 0 / 10%);
98 | --input: oklch(1 0 0 / 15%);
99 | --ring: oklch(0.552 0.016 285.938);
100 | --chart-1: oklch(0.488 0.243 264.376);
101 | --chart-2: oklch(0.696 0.17 162.48);
102 | --chart-3: oklch(0.769 0.188 70.08);
103 | --chart-4: oklch(0.627 0.265 303.9);
104 | --chart-5: oklch(0.645 0.246 16.439);
105 | --sidebar: oklch(0.21 0.006 285.885);
106 | --sidebar-foreground: oklch(0.985 0 0);
107 | --sidebar-primary: oklch(0.488 0.243 264.376);
108 | --sidebar-primary-foreground: oklch(0.985 0 0);
109 | --sidebar-accent: oklch(0.274 0.006 286.033);
110 | --sidebar-accent-foreground: oklch(0.985 0 0);
111 | --sidebar-border: oklch(1 0 0 / 10%);
112 | --sidebar-ring: oklch(0.552 0.016 285.938);
113 | }
114 |
115 | @layer base {
116 | * {
117 | @apply border-border outline-ring/50;
118 | }
119 | body {
120 | @apply bg-background text-foreground;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import type { User } from "next-auth";
2 |
3 | export type TokenWithKeys = {
4 | secretKey: string;
5 | publicKey: string;
6 | };
7 |
8 | export type UserWithKeys = User & {
9 | secretKey?: string;
10 | publicKey?: string;
11 | };
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "~/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/types.d.ts:
--------------------------------------------------------------------------------
1 | interface Window {
2 | nostr: Nostr;
3 | webln: WebLN;
4 | }
5 |
6 | interface Nip04 {
7 | encrypt(pubkey: string, plaintext: string): Promise;
8 | decrypt(pubkey, ciphertext): Promise;
9 | }
10 |
11 | // https://github.com/nostr-protocol/nips/blob/master/07.md
12 | interface Nostr {
13 | getPublicKey(): Promise;
14 | signEvent(event: unknown): Promise;
15 | getRelays(): Promise>;
16 | nip04: Nip04;
17 | }
18 |
19 | interface GetInfoResponse {
20 | node: {
21 | alias: string;
22 | pubkey: string;
23 | color?: string;
24 | };
25 | // Not supported by all connectors (see webln.request for more info)
26 | methods: string[];
27 | }
28 |
29 | interface KeysendArgs {
30 | destination: string;
31 | amount: string | number;
32 | customRecords?: Record;
33 | }
34 |
35 | interface SendPaymentResponse {
36 | preimage: string;
37 | paymentHash: string;
38 | }
39 |
40 | interface RequestInvoiceArgs {
41 | amount?: string | number;
42 | defaultAmount?: string | number;
43 | minimumAmount?: string | number;
44 | maximumAmount?: string | number;
45 | defaultMemo?: string;
46 | }
47 |
48 | interface SignMessageResponse {
49 | message: string;
50 | signature: string;
51 | }
52 |
53 | // https://www.webln.guide/introduction/readme
54 | interface WebLN {
55 | enable(): Promise;
56 | getInfo(): Promise;
57 | keysend(args: KeysendArgs): Promise;
58 | makeInvoice(args: RequestInvoiceArgs): Promise;
59 | sendPayment(paymentRequest: string): Promise;
60 | signMessage(message: string): Promise;
61 | // request(method: string, params: Object): RequestResponse;
62 | // lnurl(lnurl: string): LNURLResponse;
63 | }
64 |
65 | declare const webln: WebLN;
66 | declare const nostr: Nostr;
67 |
--------------------------------------------------------------------------------