├── .eslintrc.json
├── .gitattributes
├── .github
└── workflows
│ ├── main.yml
│ └── size.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── package.json
├── src
├── core.tsx
├── index.tsx
├── useProfile.tsx
└── utils.ts
├── tsconfig.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:react/jsx-runtime",
9 | "plugin:@typescript-eslint/recommended",
10 | "prettier"
11 | ],
12 | "overrides": [],
13 | "parser": "@typescript-eslint/parser",
14 | "parserOptions": {
15 | "ecmaVersion": "latest",
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "react",
20 | "@typescript-eslint",
21 | "prettier"
22 | ],
23 | "rules": {
24 | "prettier/prettier": "warn"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
2 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push]
3 | jobs:
4 | build:
5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
6 |
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | node: ['14.x', '16.x']
11 | os: [ubuntu-latest, windows-latest, macOS-latest]
12 |
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v3
16 |
17 | - name: Use Node ${{ matrix.node }}
18 | uses: actions/setup-node@v3
19 | with:
20 | node-version: ${{ matrix.node }}
21 |
22 | - name: Install deps and build (with cache)
23 | uses: bahmutov/npm-install@v1
24 |
25 | - name: Lint
26 | run: yarn lint
27 |
28 | - name: Test
29 | run: yarn test --ci --coverage --maxWorkers=2
30 |
31 | - name: Build
32 | run: yarn build
33 |
--------------------------------------------------------------------------------
/.github/workflows/size.yml:
--------------------------------------------------------------------------------
1 | name: size
2 | on: [pull_request]
3 | jobs:
4 | size:
5 | runs-on: ubuntu-latest
6 | env:
7 | CI_JOB_NUMBER: 1
8 | steps:
9 | - uses: actions/checkout@v3
10 | - uses: andresz1/size-limit-action@v1
11 | with:
12 | github_token: ${{ secrets.GITHUB_TOKEN }}
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "tabWidth": 2,
4 | "trailingComma": "all",
5 | "arrowParens": "always"
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Tristan Edwards
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | nostr-react
3 |
4 |
5 | React Hooks for Nostr ✨
6 |
7 |
8 | ## Installation
9 |
10 | ```
11 | npm install nostr-react
12 | ```
13 |
14 | ## Example usage:
15 |
16 | Wrap your app in the NostrProvider:
17 |
18 | ```tsx
19 | import { NostrProvider } from "nostr-react";
20 |
21 | const relayUrls = [
22 | "wss://nostr-pub.wellorder.net",
23 | "wss://relay.nostr.ch",
24 | ];
25 |
26 | function MyApp() {
27 | return (
28 |
29 |
30 |
31 | );
32 | };
33 | ```
34 |
35 | You can now use the `useNostr` and `useNostrEvents` hooks in your components!
36 |
37 | **Fetching all `text_note` events starting now:**
38 |
39 | ```tsx
40 | import { useRef } from "react";
41 | import { useNostrEvents, dateToUnix } from "nostr-react";
42 |
43 | const GlobalFeed = () => {
44 | const now = useRef(new Date()); // Make sure current time isn't re-rendered
45 |
46 | const { events } = useNostrEvents({
47 | filter: {
48 | since: dateToUnix(now.current), // all new events from now
49 | kinds: [1],
50 | },
51 | });
52 |
53 | return (
54 | <>
55 | {events.map((event) => (
56 | {event.pubkey} posted: {event.content}
57 | ))}
58 | >
59 | );
60 | };
61 | ```
62 |
63 | **Fetching all `text_note` events from a specific user, since the beginning of time:**
64 |
65 | ```tsx
66 | import { useNostrEvents } from "nostr-react";
67 |
68 | const ProfileFeed = () => {
69 | const { events } = useNostrEvents({
70 | filter: {
71 | authors: [
72 | "9c2a6495b4e3de93f3e1cc254abe4078e17c64e5771abc676a5e205b62b1286c",
73 | ],
74 | since: 0,
75 | kinds: [1],
76 | },
77 | });
78 |
79 | return (
80 | <>
81 | {events.map((event) => (
82 | {event.pubkey} posted: {event.content}
83 | ))}
84 | >
85 | );
86 | };
87 | ```
88 |
89 | **Fetching user profiles**
90 |
91 | Use the `useProfile` hook to render user profiles. You can use this in multiple components at once (for example, rendering a name and avatar for each message in a chat), the hook will automatically use *batching* to prevent errors where a client sends too many requests at once. 🎉
92 |
93 | ```tsx
94 | import { useProfile } from "nostr-react";
95 |
96 | const Profile = () => {
97 | const { data: userData } = useProfile({
98 | pubkey,
99 | });
100 |
101 | return (
102 | <>
103 | Name: {userData?.name}
104 | Public key: {userData?.npub}
105 | Picture URL: {userData?.picture}
106 | >
107 | )
108 | }
109 | ```
110 |
111 | **Post a message:**
112 |
113 | ```tsx
114 | import { useNostr, dateToUnix } from "nostr-react";
115 |
116 | import {
117 | type Event as NostrEvent,
118 | getEventHash,
119 | getPublicKey,
120 | signEvent,
121 | } from "nostr-tools";
122 |
123 | export default function PostButton() {
124 | const { publish } = useNostr();
125 |
126 | const onPost = async () => {
127 | const privKey = prompt("Paste your private key:");
128 |
129 | if (!privKey) {
130 | alert("no private key provided");
131 | return;
132 | }
133 |
134 | const message = prompt("Enter the message you want to send:");
135 |
136 | if (!message) {
137 | alert("no message provided");
138 | return;
139 | }
140 |
141 | const event: NostrEvent = {
142 | content: message,
143 | kind: 1,
144 | tags: [],
145 | created_at: dateToUnix(),
146 | pubkey: getPublicKey(privKey),
147 | };
148 |
149 | event.id = getEventHash(event);
150 | event.sig = signEvent(event, privKey);
151 |
152 | publish(event);
153 | };
154 |
155 | return (
156 | Post a message!
157 | );
158 | }
159 | ```
160 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nostr-react",
3 | "version": "0.7.0",
4 | "license": "MIT",
5 | "author": "t4t5 (https://t4t5.xyz)",
6 | "main": "dist/index.js",
7 | "module": "dist/nostr-react.esm.js",
8 | "typings": "dist/index.d.ts",
9 | "files": [
10 | "dist",
11 | "src"
12 | ],
13 | "scripts": {
14 | "analyze": "size-limit --why",
15 | "build": "dts build",
16 | "lint": "dts lint",
17 | "prepare": "dts build",
18 | "size": "size-limit",
19 | "start": "dts watch",
20 | "test": "dts test --passWithNoTests"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "https://github.com/t4t5/nostr-react"
25 | },
26 | "husky": {
27 | "hooks": {
28 | "pre-commit": "dts lint"
29 | }
30 | },
31 | "jest": {
32 | "testEnvironment": "jsdom"
33 | },
34 | "peerDependencies": {
35 | "react": ">=16"
36 | },
37 | "engines": {
38 | "node": ">=12"
39 | },
40 | "size-limit": [
41 | {
42 | "path": "dist/nostr-react.cjs.production.min.js",
43 | "limit": "10 KB"
44 | },
45 | {
46 | "path": "dist/nostr-react.esm.js",
47 | "limit": "10 KB"
48 | }
49 | ],
50 | "devDependencies": {
51 | "@size-limit/preset-small-lib": "^8.1.0",
52 | "@tsconfig/create-react-app": "^1.0.3",
53 | "@tsconfig/recommended": "^1.0.1",
54 | "@types/react": "^18.0.26",
55 | "@types/react-dom": "^18.0.9",
56 | "@typescript-eslint/eslint-plugin": "^5.47.0",
57 | "@typescript-eslint/parser": "^5.47.0",
58 | "dts-cli": "^1.6.0",
59 | "eslint": "^8.30.0",
60 | "eslint-config-prettier": "^8.5.0",
61 | "eslint-plugin-prettier": "^4.2.1",
62 | "eslint-plugin-react": "^7.31.11",
63 | "husky": "^8.0.2",
64 | "react": "^18.2.0",
65 | "react-dom": "^18.2.0",
66 | "size-limit": "^8.1.0",
67 | "tslib": "^2.4.1",
68 | "typescript": "^4.9.4"
69 | },
70 | "dependencies": {
71 | "jotai": "^1.12.1",
72 | "nostr-tools": "^1.1.0"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/core.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | ReactNode,
4 | useCallback,
5 | useContext,
6 | useEffect,
7 | useRef,
8 | useState,
9 | } from "react"
10 |
11 | import { Relay, Filter, Event as NostrEvent, relayInit, Sub } from "nostr-tools"
12 |
13 | import { uniqBy } from "./utils"
14 |
15 | type OnConnectFunc = (relay: Relay) => void
16 | type OnDisconnectFunc = (relay: Relay) => void
17 | type OnEventFunc = (event: NostrEvent) => void
18 | type OnDoneFunc = () => void
19 | type OnSubscribeFunc = (sub: Sub, relay: Relay) => void
20 |
21 | interface NostrContextType {
22 | isLoading: boolean
23 | debug?: boolean
24 | connectedRelays: Relay[]
25 | onConnect: (_onConnectCallback?: OnConnectFunc) => void
26 | onDisconnect: (_onDisconnectCallback?: OnDisconnectFunc) => void
27 | publish: (event: NostrEvent) => void
28 | }
29 |
30 | const NostrContext = createContext({
31 | isLoading: true,
32 | connectedRelays: [],
33 | onConnect: () => null,
34 | onDisconnect: () => null,
35 | publish: () => null,
36 | })
37 |
38 | const log = (
39 | isOn: boolean | undefined,
40 | type: "info" | "error" | "warn",
41 | ...args: unknown[]
42 | ) => {
43 | if (!isOn) return
44 | console[type](...args)
45 | }
46 |
47 | export function NostrProvider({
48 | children,
49 | relayUrls,
50 | debug,
51 | }: {
52 | children: ReactNode
53 | relayUrls: string[]
54 | debug?: boolean
55 | }) {
56 | const [isLoading, setIsLoading] = useState(true)
57 | const [connectedRelays, setConnectedRelays] = useState([])
58 | const [relays, setRelays] = useState([])
59 | const relayUrlsRef = useRef([])
60 |
61 | let onConnectCallback: null | OnConnectFunc = null
62 | let onDisconnectCallback: null | OnDisconnectFunc = null
63 |
64 | const disconnectToRelays = useCallback(
65 | (relayUrls: string[]) => {
66 | relayUrls.forEach(async (relayUrl) => {
67 | await relays.find((relay) => relay.url === relayUrl)?.close()
68 | setRelays((prev) => prev.filter((r) => r.url !== relayUrl))
69 | })
70 | },
71 | [relays],
72 | )
73 |
74 | const connectToRelays = useCallback(
75 | (relayUrls: string[]) => {
76 | relayUrls.forEach(async (relayUrl) => {
77 | const relay = relayInit(relayUrl)
78 |
79 | if (connectedRelays.findIndex((r) => r.url === relayUrl) >= 0) {
80 | // already connected, skip
81 | return
82 | }
83 |
84 | setRelays((prev) => uniqBy([...prev, relay], "url"))
85 | relay.connect()
86 |
87 | relay.on("connect", () => {
88 | log(debug, "info", `✅ nostr (${relayUrl}): Connected!`)
89 | setIsLoading(false)
90 | onConnectCallback?.(relay)
91 | setConnectedRelays((prev) => uniqBy([...prev, relay], "url"))
92 | })
93 |
94 | relay.on("disconnect", () => {
95 | log(debug, "warn", `🚪 nostr (${relayUrl}): Connection closed.`)
96 | onDisconnectCallback?.(relay)
97 | setConnectedRelays((prev) => prev.filter((r) => r.url !== relayUrl))
98 | })
99 |
100 | relay.on("error", () => {
101 | log(debug, "error", `❌ nostr (${relayUrl}): Connection error!`)
102 | })
103 | })
104 | },
105 | [connectedRelays, debug, onConnectCallback, onDisconnectCallback],
106 | )
107 |
108 | useEffect(() => {
109 | if (relayUrlsRef.current === relayUrls) {
110 | // relayUrls isn't updated, skip
111 | return
112 | }
113 |
114 | const relayUrlsToDisconnect = relayUrlsRef.current.filter(
115 | (relayUrl) => !relayUrls.includes(relayUrl),
116 | )
117 |
118 | disconnectToRelays(relayUrlsToDisconnect)
119 | connectToRelays(relayUrls)
120 |
121 | relayUrlsRef.current = relayUrls
122 | }, [relayUrls, connectToRelays, disconnectToRelays])
123 |
124 | const publish = (event: NostrEvent) => {
125 | return connectedRelays.map((relay) => {
126 | log(debug, "info", `⬆️ nostr (${relay.url}): Sending event:`, event)
127 |
128 | return relay.publish(event)
129 | })
130 | }
131 |
132 | const value: NostrContextType = {
133 | debug,
134 | isLoading,
135 | connectedRelays,
136 | publish,
137 | onConnect: (_onConnectCallback?: OnConnectFunc) => {
138 | if (_onConnectCallback) {
139 | onConnectCallback = _onConnectCallback
140 | }
141 | },
142 | onDisconnect: (_onDisconnectCallback?: OnDisconnectFunc) => {
143 | if (_onDisconnectCallback) {
144 | onDisconnectCallback = _onDisconnectCallback
145 | }
146 | },
147 | }
148 |
149 | return {children}
150 | }
151 |
152 | export function useNostr() {
153 | return useContext(NostrContext)
154 | }
155 |
156 | export function useNostrEvents({
157 | filter,
158 | enabled = true,
159 | }: {
160 | filter: Filter
161 | enabled?: boolean
162 | }) {
163 | const {
164 | isLoading: isLoadingProvider,
165 | onConnect,
166 | debug,
167 | connectedRelays,
168 | } = useNostr()
169 |
170 | const [isLoading, setIsLoading] = useState(true)
171 | const [events, setEvents] = useState([])
172 | const [unsubscribe, setUnsubscribe] = useState<() => void | void>(() => {
173 | return
174 | })
175 |
176 | let onEventCallback: null | OnEventFunc = null
177 | let onSubscribeCallback: null | OnSubscribeFunc = null
178 | let onDoneCallback: null | OnDoneFunc = null
179 |
180 | // Lets us detect changes in the nested filter object for the useEffect hook
181 | const filterBase64 =
182 | typeof window !== "undefined" ? window.btoa(JSON.stringify(filter)) : null
183 |
184 | const _unsubscribe = (sub: Sub, relay: Relay) => {
185 | log(
186 | debug,
187 | "info",
188 | `🙉 nostr (${relay.url}): Unsubscribing from filter:`,
189 | filter,
190 | )
191 | return sub.unsub()
192 | }
193 |
194 | const subscribe = useCallback((relay: Relay, filter: Filter) => {
195 | log(
196 | debug,
197 | "info",
198 | `👂 nostr (${relay.url}): Subscribing to filter:`,
199 | filter,
200 | )
201 | const sub = relay.sub([filter])
202 |
203 | setIsLoading(true)
204 |
205 | const unsubscribeFunc = () => {
206 | _unsubscribe(sub, relay)
207 | }
208 |
209 | setUnsubscribe(() => unsubscribeFunc)
210 |
211 | sub.on("event", (event: NostrEvent) => {
212 | log(debug, "info", `⬇️ nostr (${relay.url}): Received event:`, event)
213 | onEventCallback?.(event)
214 | setEvents((_events) => {
215 | return [event, ..._events]
216 | })
217 | })
218 |
219 | sub.on("eose", () => {
220 | setIsLoading(false)
221 | onDoneCallback?.()
222 | })
223 |
224 | return sub
225 | }, [])
226 |
227 | useEffect(() => {
228 | if (!enabled) return
229 |
230 | const relaySubs = connectedRelays.map((relay) => {
231 | const sub = subscribe(relay, filter)
232 |
233 | onSubscribeCallback?.(sub, relay)
234 |
235 | return {
236 | sub,
237 | relay,
238 | }
239 | })
240 |
241 | return () => {
242 | relaySubs.forEach(({ sub, relay }) => {
243 | _unsubscribe(sub, relay)
244 | })
245 | }
246 | }, [connectedRelays, filterBase64, enabled])
247 |
248 | const uniqEvents = events.length > 0 ? uniqBy(events, "id") : []
249 | const sortedEvents = uniqEvents.sort((a, b) => b.created_at - a.created_at)
250 |
251 | return {
252 | isLoading: isLoading || isLoadingProvider,
253 | events: sortedEvents,
254 | onConnect,
255 | connectedRelays,
256 | unsubscribe,
257 | onSubscribe: (_onSubscribeCallback: OnSubscribeFunc) => {
258 | if (_onSubscribeCallback) {
259 | onSubscribeCallback = _onSubscribeCallback
260 | }
261 | },
262 | onEvent: (_onEventCallback: OnEventFunc) => {
263 | if (_onEventCallback) {
264 | onEventCallback = _onEventCallback
265 | }
266 | },
267 | onDone: (_onDoneCallback: OnDoneFunc) => {
268 | if (_onDoneCallback) {
269 | onDoneCallback = _onDoneCallback
270 | }
271 | },
272 | }
273 | }
274 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./utils"
2 | export * from "./core"
3 | export * from "./useProfile"
4 |
--------------------------------------------------------------------------------
/src/useProfile.tsx:
--------------------------------------------------------------------------------
1 | import { atom, useAtom } from "jotai"
2 | import { nip19 } from "nostr-tools"
3 | import { useEffect, useState } from "react"
4 |
5 | import { useNostrEvents } from "./core"
6 | import { uniqValues } from "./utils"
7 |
8 | export interface Metadata {
9 | name?: string
10 | username?: string
11 | display_name?: string
12 | picture?: string
13 | banner?: string
14 | about?: string
15 | website?: string
16 | lud06?: string
17 | lud16?: string
18 | nip05?: string
19 | }
20 |
21 | const QUEUE_DEBOUNCE_DURATION = 100
22 |
23 | let timer: NodeJS.Timeout | undefined = undefined
24 |
25 | const queuedPubkeysAtom = atom([])
26 | const requestedPubkeysAtom = atom([])
27 | const fetchedProfilesAtom = atom>({})
28 |
29 | function useProfileQueue({ pubkey }: { pubkey: string }) {
30 | const [isReadyToFetch, setIsReadyToFetch] = useState(false)
31 |
32 | const [queuedPubkeys, setQueuedPubkeys] = useAtom(queuedPubkeysAtom)
33 |
34 | const [requestedPubkeys] = useAtom(requestedPubkeysAtom)
35 | const alreadyRequested = !!requestedPubkeys.includes(pubkey)
36 |
37 | useEffect(() => {
38 | if (alreadyRequested) {
39 | return
40 | }
41 |
42 | clearTimeout(timer)
43 |
44 | timer = setTimeout(() => {
45 | setIsReadyToFetch(true)
46 | }, QUEUE_DEBOUNCE_DURATION)
47 |
48 | setQueuedPubkeys((_pubkeys: string[]) => {
49 | // Unique values only:
50 | const arr = [..._pubkeys, pubkey].filter(uniqValues).filter((_pubkey) => {
51 | return !requestedPubkeys.includes(_pubkey)
52 | })
53 |
54 | return arr
55 | })
56 | }, [pubkey, setQueuedPubkeys, alreadyRequested, requestedPubkeys])
57 |
58 | return {
59 | pubkeysToFetch: isReadyToFetch ? queuedPubkeys : [],
60 | }
61 | }
62 |
63 | export function useProfile({
64 | pubkey,
65 | enabled: _enabled = true,
66 | }: {
67 | pubkey: string
68 | enabled?: boolean
69 | }) {
70 | const [, setRequestedPubkeys] = useAtom(requestedPubkeysAtom)
71 | const { pubkeysToFetch } = useProfileQueue({ pubkey })
72 | const enabled = _enabled && !!pubkeysToFetch.length
73 |
74 | const [fetchedProfiles, setFetchedProfiles] = useAtom(fetchedProfilesAtom)
75 |
76 | const { onEvent, onSubscribe, isLoading, onDone } = useNostrEvents({
77 | filter: {
78 | kinds: [0],
79 | authors: pubkeysToFetch,
80 | },
81 | enabled,
82 | })
83 |
84 | onSubscribe(() => {
85 | // Reset list
86 | // (We've already opened a subscription to these pubkeys now)
87 | setRequestedPubkeys((_pubkeys) => {
88 | return [..._pubkeys, ...pubkeysToFetch].filter(uniqValues)
89 | })
90 | })
91 |
92 | onEvent((rawMetadata) => {
93 | try {
94 | const metadata: Metadata = JSON.parse(rawMetadata.content)
95 | const metaPubkey = rawMetadata.pubkey
96 |
97 | if (metadata) {
98 | setFetchedProfiles((_profiles: Record) => {
99 | return {
100 | ..._profiles,
101 | [metaPubkey]: metadata,
102 | }
103 | })
104 | }
105 | } catch (err) {
106 | console.error(err, rawMetadata)
107 | }
108 | })
109 |
110 | const metadata = fetchedProfiles[pubkey]
111 | const npub = nip19.npubEncode(pubkey)
112 |
113 | return {
114 | isLoading,
115 | onDone,
116 | data: metadata
117 | ? {
118 | ...metadata,
119 | npub,
120 | }
121 | : undefined,
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const uniqBy = (arr: T[], key: keyof T): T[] => {
2 | return Object.values(
3 | arr.reduce(
4 | (map, item) => ({
5 | ...map,
6 | [`${item[key]}`]: item,
7 | }),
8 | {},
9 | ),
10 | )
11 | }
12 |
13 | export const uniqValues = (value: string, index: number, self: string[]) => {
14 | return self.indexOf(value) === index
15 | }
16 |
17 | export const dateToUnix = (_date?: Date) => {
18 | const date = _date || new Date()
19 |
20 | return Math.floor(date.getTime() / 1000)
21 | }
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
3 | "extends": "@tsconfig/create-react-app/tsconfig.json",
4 | "include": [
5 | "src",
6 | "types"
7 | ],
8 | "compilerOptions": {
9 | "jsx": "react-jsx"
10 | },
11 | }
12 |
--------------------------------------------------------------------------------