feat: initialize markr nostr bookmark client

- Add project structure with TypeScript, React, and Vite
- Implement nostr authentication using browser extension (NIP-07)
- Add NIP-51 compliant bookmark fetching and display
- Create minimal UI with login and bookmark components
- Integrate applesauce-core and applesauce-react libraries
- Add responsive styling with dark/light mode support
- Include comprehensive README with setup instructions

This is a minimal MVP for a nostr bookmark client that allows users to
view their bookmarks according to NIP-51 specification.
This commit is contained in:
Gigi
2025-10-02 07:17:07 +02:00
commit 5d53a827e0
11194 changed files with 1827829 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
import { Link } from "applesauce-content/nast";
export type LinkRenderer = (url: URL, node: Link) => JSX.Element | false | null;
export declare function buildLinkRenderer(handlers: LinkRenderer[]): import("react").NamedExoticComponent<import("./nast.js").ExtraProps<Link>>;

View File

@@ -0,0 +1,23 @@
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
import { memo, useMemo } from "react";
export function buildLinkRenderer(handlers) {
const LinkRenderer = ({ node }) => {
const content = useMemo(() => {
try {
const url = new URL(node.href);
for (const handler of handlers) {
try {
const content = handler(url, node);
if (content)
return content;
}
catch (e) { }
}
}
catch (error) { }
return null;
}, [node.href, node.value]);
return content || _jsx(_Fragment, { children: node.value });
};
return memo(LinkRenderer);
}

View File

@@ -0,0 +1,2 @@
export * from "./nast.js";
export * from "./build-link-renderer.js";

2
node_modules/applesauce-react/dist/helpers/index.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
export * from "./nast.js";
export * from "./build-link-renderer.js";

12
node_modules/applesauce-react/dist/helpers/nast.d.ts generated vendored Normal file
View File

@@ -0,0 +1,12 @@
import React from "react";
import { Content, ContentMap, Root } from "applesauce-content/nast";
type Component<ComponentProps> = React.FunctionComponent<ComponentProps> | React.ComponentClass;
export type ExtraProps<T extends Content> = {
node: T;
};
export type ComponentMap = Partial<{
[k in keyof ContentMap]: Component<ExtraProps<ContentMap[k]>>;
}>;
/** Render a nostr syntax tree to JSX components */
export declare function renderNast(root: Root, components: ComponentMap): import("react/jsx-runtime").JSX.Element;
export {};

15
node_modules/applesauce-react/dist/helpers/nast.js generated vendored Normal file
View File

@@ -0,0 +1,15 @@
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
/** Render a nostr syntax tree to JSX components */
export function renderNast(root, components) {
const indexes = {};
return (_jsx(_Fragment, { children: root.children.map((node) => {
indexes[node.type] = indexes[node.type] ?? 0;
const index = indexes[node.type];
indexes[node.type]++;
const Component = components[node.type];
if (!Component)
return null;
// @ts-expect-error
return _jsx(Component, { node: node }, node.type + "-" + index);
}) }));
}

12
node_modules/applesauce-react/dist/hooks/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,12 @@
export * from "./use-account-manager.js";
export * from "./use-accounts.js";
export * from "./use-action-hub.js";
export * from "./use-action.js";
export * from "./use-active-account.js";
export * from "./use-event-factory.js";
export * from "./use-event-model.js";
export * from "./use-event-store.js";
export * from "./use-observable-memo.js";
export * from "./use-observable.js";
export * from "./use-render-nast.js";
export * from "./use-rendered-content.js";

12
node_modules/applesauce-react/dist/hooks/index.js generated vendored Normal file
View File

@@ -0,0 +1,12 @@
export * from "./use-account-manager.js";
export * from "./use-accounts.js";
export * from "./use-action-hub.js";
export * from "./use-action.js";
export * from "./use-active-account.js";
export * from "./use-event-factory.js";
export * from "./use-event-model.js";
export * from "./use-event-store.js";
export * from "./use-observable-memo.js";
export * from "./use-observable.js";
export * from "./use-render-nast.js";
export * from "./use-rendered-content.js";

View File

@@ -0,0 +1,4 @@
import { AccountManager } from "applesauce-accounts";
export declare function useAccountManager(): AccountManager;
export declare function useAccountManager(require: false): AccountManager | undefined;
export declare function useAccountManager(require: true): AccountManager;

View File

@@ -0,0 +1,8 @@
import { useContext } from "react";
import { AccountsContext } from "../providers/accounts-provider.js";
export function useAccountManager(require = true) {
const manager = useContext(AccountsContext);
if (!manager && require)
throw new Error("Missing AccountsProvider");
return manager;
}

View File

@@ -0,0 +1,2 @@
import { IAccount } from "applesauce-accounts";
export declare function useAccounts(): IAccount[];

View File

@@ -0,0 +1,6 @@
import { useObservableEagerState } from "observable-hooks";
import { useAccountManager } from "./use-account-manager.js";
export function useAccounts() {
const manager = useAccountManager();
return useObservableEagerState(manager.accounts$);
}

View File

@@ -0,0 +1 @@
export declare function useActionHub(): import("applesauce-actions").ActionHub;

View File

@@ -0,0 +1,8 @@
import { useContext } from "react";
import { ActionsContext } from "../providers/actions-provider.js";
export function useActionHub() {
const hub = useContext(ActionsContext);
if (!hub)
throw new Error("Missing ActionsProvider");
return hub;
}

View File

@@ -0,0 +1,6 @@
import { ActionConstructor } from "applesauce-actions";
export declare function useAction<Args extends Array<any>>(Action: ActionConstructor<Args>, args: Args | undefined): {
loading: boolean;
run: () => Promise<void>;
exec: () => import("rxjs").Observable<import("nostr-tools").Event> | undefined;
};

37
node_modules/applesauce-react/dist/hooks/use-action.js generated vendored Normal file
View File

@@ -0,0 +1,37 @@
import { useCallback, useRef, useState } from "react";
import { finalize } from "rxjs";
import { useActionHub } from "./use-action-hub.js";
export function useAction(Action, args) {
const [loading, setLoading] = useState(false);
const ref = useRef(args);
ref.current = args;
const hub = useActionHub();
const run = useCallback(async () => {
if (args === undefined)
return;
setLoading(true);
try {
await hub.run(Action, ...args);
setLoading(false);
}
catch (error) {
setLoading(false);
throw error;
}
}, [Action]);
const exec = useCallback(() => {
if (args === undefined)
return;
setLoading(true);
try {
return hub.exec(Action, ...args).pipe(finalize(() => {
setLoading(false);
}));
}
catch (error) {
setLoading(false);
throw error;
}
}, [Action]);
return { loading, run, exec };
}

View File

@@ -0,0 +1,2 @@
import { IAccount } from "applesauce-accounts";
export declare function useActiveAccount(): IAccount | undefined;

View File

@@ -0,0 +1,6 @@
import { useObservableEagerState } from "observable-hooks";
import { useAccountManager } from "./use-account-manager.js";
export function useActiveAccount() {
const manager = useAccountManager();
return useObservableEagerState(manager.active$);
}

View File

@@ -0,0 +1,4 @@
import { EventFactory } from "applesauce-factory";
export declare function useEventFactory(require: false): EventFactory | undefined;
export declare function useEventFactory(require: true): EventFactory;
export declare function useEventFactory(): EventFactory;

View File

@@ -0,0 +1,8 @@
import { useContext } from "react";
import { FactoryContext } from "../providers/factory-provider.js";
export function useEventFactory(require = true) {
const factory = useContext(FactoryContext);
if (!require && !factory)
throw new Error("Missing EventFactoryProvider");
return factory;
}

View File

@@ -0,0 +1,3 @@
import { ModelConstructor } from "applesauce-core";
/** Runs and subscribes to a model on the event store */
export declare function useEventModel<T extends unknown, Args extends Array<any>>(factory: ModelConstructor<T, Args>, args?: Args | null): T | undefined;

View File

@@ -0,0 +1,15 @@
import { withImmediateValueOrDefault } from "applesauce-core";
import hash_sum from "hash-sum";
import { of } from "rxjs";
import { useEventStore } from "./use-event-store.js";
import { useObservableEagerMemo } from "./use-observable-memo.js";
/** Runs and subscribes to a model on the event store */
export function useEventModel(factory, args) {
const store = useEventStore();
return useObservableEagerMemo(() => {
if (args)
return store.model(factory, ...args).pipe(withImmediateValueOrDefault(undefined));
else
return of(undefined);
}, [hash_sum(args), store, factory]);
}

View File

@@ -0,0 +1,6 @@
import { IEventStore } from "applesauce-core/event-store";
/**
* Gets the EventStore from a parent {@link EventStoreProvider} component
* If there is none it throws an error
*/
export declare function useEventStore(): IEventStore;

View File

@@ -0,0 +1,12 @@
import { useContext } from "react";
import { EventStoreContext } from "../providers/store-provider.js";
/**
* Gets the EventStore from a parent {@link EventStoreProvider} component
* If there is none it throws an error
*/
export function useEventStore() {
const store = useContext(EventStoreContext);
if (!store)
throw new Error("Missing EventStoreProvider");
return store;
}

View File

@@ -0,0 +1,7 @@
import { BehaviorSubject, Observable } from "rxjs";
/** A hook that recreates an observable when the dependencies change */
export declare function useObservableMemo<T>(factory: () => BehaviorSubject<T>, deps: any[]): T;
export declare function useObservableMemo<T>(factory: () => Observable<T> | undefined, deps: any[]): T | undefined;
/** A hook that recreates a synchronous observable when the dependencies change */
export declare function useObservableEagerMemo<T>(factory: () => Observable<T>, deps: any[]): T;
export declare function useObservableEagerMemo<T>(factory: () => Observable<T> | undefined, deps: any[]): T | undefined;

View File

@@ -0,0 +1,9 @@
import { useObservableEagerState, useObservableState } from "observable-hooks";
import { useMemo } from "react";
import { of } from "rxjs";
export function useObservableMemo(factory, deps) {
return useObservableState(useMemo(() => factory() || of(undefined), deps));
}
export function useObservableEagerMemo(factory, deps) {
return useObservableEagerState(useMemo(() => factory() || of(undefined), deps));
}

View File

@@ -0,0 +1 @@
export * from "observable-hooks";

View File

@@ -0,0 +1,2 @@
// NOTE: re-export hooks from observable-hooks to avoid confusion
export * from "observable-hooks";

View File

@@ -0,0 +1,5 @@
import { Root } from "applesauce-content/nast";
import { ComponentMap } from "../helpers/nast.js";
export { ComponentMap };
/** A hook to get the rendered output of a nostr syntax tree */
export declare function useRenderNast(root: Root | undefined, components: ComponentMap): JSX.Element | null;

View File

@@ -0,0 +1,6 @@
import { useMemo } from "react";
import { renderNast } from "../helpers/nast.js";
/** A hook to get the rendered output of a nostr syntax tree */
export function useRenderNast(root, components) {
return useMemo(() => (root ? renderNast(root, components) : null), [root, Object.keys(components).join("|")]);
}

View File

@@ -0,0 +1,19 @@
import { getParsedContent } from "applesauce-content/text";
import { EventTemplate, NostrEvent } from "nostr-tools";
import { ComponentMap } from "../helpers/nast.js";
import { LinkRenderer } from "../helpers/build-link-renderer.js";
export { ComponentMap };
type Options = {
/** The key to cache the results under, passing null will disable */
cacheKey: symbol | null;
/** Override transformers */
transformers?: Parameters<typeof getParsedContent>[2];
/** If set will use {@link buildLinkRenderer} to render links */
linkRenderers?: LinkRenderer[];
/** Override event content */
content?: string;
/** Maximum length */
maxLength?: number;
};
/** Returns the parsed and render text content for an event */
export declare function useRenderedContent(event: NostrEvent | EventTemplate | string | undefined, components: ComponentMap, opts?: Options): JSX.Element | null;

View File

@@ -0,0 +1,16 @@
import { useMemo } from "react";
import { truncateContent } from "applesauce-content/nast";
import { getParsedContent } from "applesauce-content/text";
import { useRenderNast } from "./use-render-nast.js";
import { buildLinkRenderer } from "../helpers/build-link-renderer.js";
/** Returns the parsed and render text content for an event */
export function useRenderedContent(event, components, opts) {
// if link renderers are set, override the link components
const _components = useMemo(() => (opts?.linkRenderers ? { ...components, link: buildLinkRenderer(opts.linkRenderers) } : components), [opts?.linkRenderers, components]);
// add additional transformers
const nast = useMemo(() => (event ? getParsedContent(event, opts?.content, opts?.transformers, opts?.cacheKey) : undefined), [event, opts?.content, opts?.transformers, opts?.cacheKey]);
let truncated = nast;
if (opts?.maxLength && nast)
truncated = truncateContent(nast, opts.maxLength);
return useRenderNast(truncated, _components);
}

3
node_modules/applesauce-react/dist/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,3 @@
export * as Helpers from "./helpers/index.js";
export * as Hooks from "./hooks/index.js";
export * from "./providers/index.js";

3
node_modules/applesauce-react/dist/index.js generated vendored Normal file
View File

@@ -0,0 +1,3 @@
export * as Helpers from "./helpers/index.js";
export * as Hooks from "./hooks/index.js";
export * from "./providers/index.js";

View File

@@ -0,0 +1,7 @@
import { PropsWithChildren } from "react";
import { AccountManager } from "applesauce-accounts";
export declare const AccountsContext: import("react").Context<AccountManager<any> | undefined>;
/** Provides an AccountManager to the component tree */
export declare function AccountsProvider({ manager, children }: PropsWithChildren<{
manager?: AccountManager;
}>): import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,7 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { createContext } from "react";
export const AccountsContext = createContext(undefined);
/** Provides an AccountManager to the component tree */
export function AccountsProvider({ manager, children }) {
return _jsx(AccountsContext.Provider, { value: manager, children: children });
}

View File

@@ -0,0 +1,7 @@
import { PropsWithChildren } from "react";
import { ActionHub } from "applesauce-actions";
export declare const ActionsContext: import("react").Context<ActionHub | undefined>;
/** Provides an ActionHub to the component tree */
export declare function ActionsProvider({ actionHub, children }: PropsWithChildren<{
actionHub?: ActionHub;
}>): import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,7 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { createContext } from "react";
export const ActionsContext = createContext(undefined);
/** Provides an ActionHub to the component tree */
export function ActionsProvider({ actionHub, children }) {
return _jsx(ActionsContext.Provider, { value: actionHub, children: children });
}

View File

@@ -0,0 +1,7 @@
import { EventFactory } from "applesauce-factory";
import { PropsWithChildren } from "react";
export declare const FactoryContext: import("react").Context<EventFactory | undefined>;
/** Provides an {@link EventFactory} to the component tree */
export declare function FactoryProvider({ factory, children }: PropsWithChildren<{
factory?: EventFactory;
}>): import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,7 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { createContext } from "react";
export const FactoryContext = createContext(undefined);
/** Provides an {@link EventFactory} to the component tree */
export function FactoryProvider({ factory, children }) {
return _jsx(FactoryContext.Provider, { value: factory, children: children });
}

View File

@@ -0,0 +1,4 @@
export * from "./factory-provider.js";
export * from "./store-provider.js";
export * from "./accounts-provider.js";
export * from "./actions-provider.js";

View File

@@ -0,0 +1,4 @@
export * from "./factory-provider.js";
export * from "./store-provider.js";
export * from "./accounts-provider.js";
export * from "./actions-provider.js";

View File

@@ -0,0 +1,7 @@
import { PropsWithChildren } from "react";
import { IEventStore } from "applesauce-core";
export declare const EventStoreContext: import("react").Context<IEventStore | null>;
/** Provides a EventStore to the component tree */
export declare function EventStoreProvider({ eventStore, children }: PropsWithChildren<{
eventStore: IEventStore;
}>): import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,7 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { createContext } from "react";
export const EventStoreContext = createContext(null);
/** Provides a EventStore to the component tree */
export function EventStoreProvider({ eventStore, children }) {
return _jsx(EventStoreContext.Provider, { value: eventStore, children: children });
}