feat: used sendDM method in contact form #1
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | { | ||||||
|  |   "cSpell.words": ["Nostr"] | ||||||
|  | } | ||||||
							
								
								
									
										127
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										127
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -29,13 +29,15 @@ | |||||||
|         "gatsby-source-filesystem": "^3.3.0", |         "gatsby-source-filesystem": "^3.3.0", | ||||||
|         "gatsby-transformer-remark": "^4.0.0", |         "gatsby-transformer-remark": "^4.0.0", | ||||||
|         "gatsby-transformer-sharp": "^3.3.0", |         "gatsby-transformer-sharp": "^3.3.0", | ||||||
|  |         "nostr-tools": "^2.7.0", | ||||||
|         "react": "^17.0.1", |         "react": "^17.0.1", | ||||||
|         "react-dom": "^17.0.1", |         "react-dom": "^17.0.1", | ||||||
|         "react-helmet": "^6.1.0", |         "react-helmet": "^6.1.0", | ||||||
|         "react-icons": "^4.2.0", |         "react-icons": "^4.2.0", | ||||||
|         "react-share": "^4.4.0", |         "react-share": "^4.4.0", | ||||||
|         "react-use-flexsearch": "^0.1.1", |         "react-use-flexsearch": "^0.1.1", | ||||||
|         "styled-components": "^5.2.3" |         "styled-components": "^5.2.3", | ||||||
|  |         "tseep": "^1.2.1" | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@popperjs/core": "^2.9.2", |         "@popperjs/core": "^2.9.2", | ||||||
| @@ -3114,6 +3116,47 @@ | |||||||
|         "eslint-scope": "5.1.1" |         "eslint-scope": "5.1.1" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@noble/ciphers": { | ||||||
|  |       "version": "0.5.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz", | ||||||
|  |       "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==", | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://paulmillr.com/funding/" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@noble/curves": { | ||||||
|  |       "version": "1.2.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", | ||||||
|  |       "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@noble/hashes": "1.3.2" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://paulmillr.com/funding/" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@noble/curves/node_modules/@noble/hashes": { | ||||||
|  |       "version": "1.3.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", | ||||||
|  |       "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 16" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://paulmillr.com/funding/" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@noble/hashes": { | ||||||
|  |       "version": "1.3.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", | ||||||
|  |       "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==", | ||||||
|  |       "engines": { | ||||||
|  |         "node": ">= 16" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://paulmillr.com/funding/" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@nodelib/fs.scandir": { |     "node_modules/@nodelib/fs.scandir": { | ||||||
|       "version": "2.1.5", |       "version": "2.1.5", | ||||||
|       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", |       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | ||||||
| @@ -3215,6 +3258,53 @@ | |||||||
|         "url": "https://opencollective.com/popperjs" |         "url": "https://opencollective.com/popperjs" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@scure/base": { | ||||||
|  |       "version": "1.1.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", | ||||||
|  |       "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", | ||||||
|  |       "funding": [ | ||||||
|  |         { | ||||||
|  |           "type": "individual", | ||||||
|  |           "url": "https://paulmillr.com/funding/" | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|  |     "node_modules/@scure/bip32": { | ||||||
|  |       "version": "1.3.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.3.1.tgz", | ||||||
|  |       "integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@noble/curves": "~1.1.0", | ||||||
|  |         "@noble/hashes": "~1.3.1", | ||||||
|  |         "@scure/base": "~1.1.0" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://paulmillr.com/funding/" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@scure/bip32/node_modules/@noble/curves": { | ||||||
|  |       "version": "1.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz", | ||||||
|  |       "integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@noble/hashes": "1.3.1" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://paulmillr.com/funding/" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/@scure/bip39": { | ||||||
|  |       "version": "1.2.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.2.1.tgz", | ||||||
|  |       "integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@noble/hashes": "~1.3.0", | ||||||
|  |         "@scure/base": "~1.1.0" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "url": "https://paulmillr.com/funding/" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@sideway/address": { |     "node_modules/@sideway/address": { | ||||||
|       "version": "4.1.5", |       "version": "4.1.5", | ||||||
|       "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", |       "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", | ||||||
| @@ -15854,6 +15944,36 @@ | |||||||
|         "url": "https://github.com/sponsors/sindresorhus" |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/nostr-tools": { | ||||||
|  |       "version": "2.7.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.7.0.tgz", | ||||||
|  |       "integrity": "sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==", | ||||||
|  |       "dependencies": { | ||||||
|  |         "@noble/ciphers": "^0.5.1", | ||||||
|  |         "@noble/curves": "1.2.0", | ||||||
|  |         "@noble/hashes": "1.3.1", | ||||||
|  |         "@scure/base": "1.1.1", | ||||||
|  |         "@scure/bip32": "1.3.1", | ||||||
|  |         "@scure/bip39": "1.2.1" | ||||||
|  |       }, | ||||||
|  |       "optionalDependencies": { | ||||||
|  |         "nostr-wasm": "v0.1.0" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "typescript": ">=5.0.0" | ||||||
|  |       }, | ||||||
|  |       "peerDependenciesMeta": { | ||||||
|  |         "typescript": { | ||||||
|  |           "optional": true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "node_modules/nostr-wasm": { | ||||||
|  |       "version": "0.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", | ||||||
|  |       "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", | ||||||
|  |       "optional": true | ||||||
|  |     }, | ||||||
|     "node_modules/not": { |     "node_modules/not": { | ||||||
|       "version": "0.1.0", |       "version": "0.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", |       "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", | ||||||
| @@ -21030,6 +21150,11 @@ | |||||||
|         "json5": "lib/cli.js" |         "json5": "lib/cli.js" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/tseep": { | ||||||
|  |       "version": "1.2.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/tseep/-/tseep-1.2.1.tgz", | ||||||
|  |       "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==" | ||||||
|  |     }, | ||||||
|     "node_modules/tslib": { |     "node_modules/tslib": { | ||||||
|       "version": "1.14.1", |       "version": "1.14.1", | ||||||
|       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", |       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", | ||||||
|   | |||||||
| @@ -38,13 +38,15 @@ | |||||||
|     "gatsby-source-filesystem": "^3.3.0", |     "gatsby-source-filesystem": "^3.3.0", | ||||||
|     "gatsby-transformer-remark": "^4.0.0", |     "gatsby-transformer-remark": "^4.0.0", | ||||||
|     "gatsby-transformer-sharp": "^3.3.0", |     "gatsby-transformer-sharp": "^3.3.0", | ||||||
|  |     "nostr-tools": "^2.7.0", | ||||||
|     "react": "^17.0.1", |     "react": "^17.0.1", | ||||||
|     "react-dom": "^17.0.1", |     "react-dom": "^17.0.1", | ||||||
|     "react-helmet": "^6.1.0", |     "react-helmet": "^6.1.0", | ||||||
|     "react-icons": "^4.2.0", |     "react-icons": "^4.2.0", | ||||||
|     "react-share": "^4.4.0", |     "react-share": "^4.4.0", | ||||||
|     "react-use-flexsearch": "^0.1.1", |     "react-use-flexsearch": "^0.1.1", | ||||||
|     "styled-components": "^5.2.3" |     "styled-components": "^5.2.3", | ||||||
|  |     "tseep": "^1.2.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@popperjs/core": "^2.9.2", |     "@popperjs/core": "^2.9.2", | ||||||
|   | |||||||
							
								
								
									
										205
									
								
								src/controllers/NostrController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								src/controllers/NostrController.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | |||||||
|  | import { | ||||||
|  |   Event, | ||||||
|  |   EventTemplate, | ||||||
|  |   SimplePool, | ||||||
|  |   UnsignedEvent, | ||||||
|  |   finalizeEvent, | ||||||
|  |   nip04, | ||||||
|  |   nip19, | ||||||
|  |   verifyEvent, | ||||||
|  |   generateSecretKey, | ||||||
|  |   getPublicKey | ||||||
|  | } from 'nostr-tools' | ||||||
|  | import { EventEmitter } from 'tseep' | ||||||
|  | import { SignedEvent, Keys } from '../types' | ||||||
|  |  | ||||||
|  | export class NostrController extends EventEmitter { | ||||||
|  |   private static instance: NostrController | ||||||
|  |  | ||||||
|  |   private generatedKeys: Keys | undefined | ||||||
|  |  | ||||||
|  |   private constructor() { | ||||||
|  |     super() | ||||||
|  |  | ||||||
|  |     this.generatedKeys = this.generateKeys() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static getInstance(): NostrController { | ||||||
|  |     if (!NostrController.instance) { | ||||||
|  |       NostrController.instance = new NostrController() | ||||||
|  |     } | ||||||
|  |     return NostrController.instance | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Function will publish provided event to the provided relays | ||||||
|  |    */ | ||||||
|  |   publishEvent = async (event: Event, relays: string[]) => { | ||||||
|  |     const simplePool = new SimplePool() | ||||||
|  |     const promises = simplePool.publish(relays, event) | ||||||
|  |  | ||||||
|  |     const results = await Promise.allSettled(promises) | ||||||
|  |  | ||||||
|  |     const publishedRelays: string[] = [] | ||||||
|  |  | ||||||
|  |     results.forEach((result, index) => { | ||||||
|  |       if (result.status === 'fulfilled') publishedRelays.push(relays[index]) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     if (publishedRelays.length === 0) { | ||||||
|  |       const failedPublishes: any[] = [] | ||||||
|  |       const fallbackRejectionReason = | ||||||
|  |         'Attempt to publish an event has been rejected with unknown reason.' | ||||||
|  |  | ||||||
|  |       results.forEach((res, index) => { | ||||||
|  |         if (res.status === 'rejected') { | ||||||
|  |           failedPublishes.push({ | ||||||
|  |             relay: relays[index], | ||||||
|  |             error: res.reason | ||||||
|  |               ? res.reason.message || fallbackRejectionReason | ||||||
|  |               : fallbackRejectionReason | ||||||
|  |           }) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |  | ||||||
|  |       throw failedPublishes | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return publishedRelays | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Signs an event with private key (if it is present in local storage) or | ||||||
|  |    * with browser extension (if it is present) or | ||||||
|  |    * with nSecBunker instance. | ||||||
|  |    * @param event - unsigned nostr event. | ||||||
|  |    * @returns - a promised that is resolved with signed nostr event. | ||||||
|  |    */ | ||||||
|  |   signEvent = async ( | ||||||
|  |     event: UnsignedEvent | EventTemplate | ||||||
|  |   ): Promise<SignedEvent> => { | ||||||
|  |     if (!this.generatedKeys) { | ||||||
|  |       throw new Error(`Private & public key pair is not found.`) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const { private: nsec } = this.generatedKeys | ||||||
|  |     const privateKey = nip19.decode(nsec).data as Uint8Array | ||||||
|  |  | ||||||
|  |     const signedEvent = finalizeEvent(event, privateKey) | ||||||
|  |  | ||||||
|  |     this.verifySignedEvent(signedEvent) | ||||||
|  |  | ||||||
|  |     return Promise.resolve(signedEvent) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   nip04Encrypt = async (receiver: string, content: string) => { | ||||||
|  |     if (!this.generatedKeys) { | ||||||
|  |       throw new Error(`Private & public key pair is not found.`) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const { private: nsec } = this.generatedKeys | ||||||
|  |     const privateKey = nip19.decode(nsec).data as Uint8Array | ||||||
|  |  | ||||||
|  |     const encrypted = await nip04.encrypt(privateKey, receiver, content) | ||||||
|  |  | ||||||
|  |     return encrypted | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Sends a Direct Message (DM) to a recipient, encrypting the content and handling authentication. | ||||||
|  |    * @param fileUrl The URL of the encrypted zip file to be included in the DM. | ||||||
|  |    * @param encryptionKey The encryption key used to decrypt the zip file to be included in the DM. | ||||||
|  |    * @param pubkey The public key of the recipient. | ||||||
|  |    * @param isSigner Boolean indicating whether the recipient is a signer or viewer. | ||||||
|  |    * @param setAuthUrl Function to set the authentication URL in the component state. | ||||||
|  |    */ | ||||||
|  |   sendDM = async (pubkey: string, message: string) => { | ||||||
|  |     // Set up timeout promise to handle encryption timeout | ||||||
|  |     const timeoutPromise = new Promise<never>((_, reject) => { | ||||||
|  |       setTimeout(() => { | ||||||
|  |         reject(new Error('Timeout occurred')) | ||||||
|  |       }, 60000) // Timeout duration = 60 seconds | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     // Encrypt the DM content, with timeout | ||||||
|  |     const encrypted = await Promise.race([ | ||||||
|  |       this.nip04Encrypt(this.npubToHex(pubkey) as string, message), | ||||||
|  |       timeoutPromise | ||||||
|  |     ]) | ||||||
|  |  | ||||||
|  |     // Return if encryption failed | ||||||
|  |     if (!encrypted) throw new Error('Message was not encrypted.') | ||||||
|  |  | ||||||
|  |     // Construct event metadata for the DM | ||||||
|  |     const event: EventTemplate = { | ||||||
|  |       kind: 4, // DM event type | ||||||
|  |       content: encrypted, // Encrypted DM content | ||||||
|  |       created_at: Math.floor(Date.now() / 1000), // Current timestamp | ||||||
|  |       tags: [['p', this.npubToHex(pubkey) as string]] // Tag with recipient's public key | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Sign the DM event | ||||||
|  |     const signedEvent = await this.signEvent(event) | ||||||
|  |  | ||||||
|  |     // Return if event signing failed | ||||||
|  |     if (!signedEvent) throw new Error('Message was not signed.') | ||||||
|  |  | ||||||
|  |     // These relay will be used to send a DM. Recipient has to read from these relays to receive a DM. | ||||||
|  |     const relays = [ | ||||||
|  |       'wss://relay.damus.io/', | ||||||
|  |       'wss://nos.lol/', | ||||||
|  |       'wss://relay.snort.social' | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     // Publish the signed DM event to the recipient's read relays | ||||||
|  |     return await this.publishEvent(signedEvent, relays) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * @param hexKey hex private or public key | ||||||
|  |    * @returns whether or not is key valid | ||||||
|  |    */ | ||||||
|  |   validateHex = (hexKey: string) => { | ||||||
|  |     return hexKey.match(/^[a-f0-9]{64}$/) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * NPUB provided - it will convert NPUB to HEX | ||||||
|  |    * HEX provided - it will return HEX | ||||||
|  |    * | ||||||
|  |    * @param pubKey in NPUB, HEX format | ||||||
|  |    * @returns HEX format | ||||||
|  |    */ | ||||||
|  |   npubToHex = (pubKey: string): string | null => { | ||||||
|  |     // If key is NPUB | ||||||
|  |     if (pubKey.startsWith('npub1')) { | ||||||
|  |       try { | ||||||
|  |         return nip19.decode(pubKey).data as string | ||||||
|  |       } catch (error) { | ||||||
|  |         return null | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // valid hex key | ||||||
|  |     if (this.validateHex(pubKey)) return pubKey | ||||||
|  |  | ||||||
|  |     // Not a valid hex key | ||||||
|  |     return null | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   generateKeys = (): Keys => { | ||||||
|  |     const nsec = generateSecretKey() | ||||||
|  |  | ||||||
|  |     return { private: nip19.nsecEncode(nsec), public: getPublicKey(nsec) } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   verifySignedEvent = (event: SignedEvent) => { | ||||||
|  |     const isGood = verifyEvent(event) | ||||||
|  |  | ||||||
|  |     if (!isGood) { | ||||||
|  |       throw new Error( | ||||||
|  |         'Signed event did not pass verification. Check sig, id and pubkey.' | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								src/controllers/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/controllers/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export * from './NostrController' | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { PageProps, Link, graphql } from 'gatsby' | import { PageProps, Link, graphql } from 'gatsby' | ||||||
| import React from 'react' | import React, { useState } from 'react' | ||||||
|  |  | ||||||
| import Layout from '../components/layout' | import Layout from '../components/layout' | ||||||
| import Seo from '../components/seo' | import Seo from '../components/seo' | ||||||
| @@ -20,6 +20,7 @@ import { | |||||||
| import contactBg from '../images/contact_bg.jpg' | import contactBg from '../images/contact_bg.jpg' | ||||||
|  |  | ||||||
| import '../styledComponents/contact.css' | import '../styledComponents/contact.css' | ||||||
|  | import { NostrController } from '../controllers' | ||||||
|  |  | ||||||
| type DataProps = { | type DataProps = { | ||||||
|   site: { |   site: { | ||||||
| @@ -32,6 +33,17 @@ type DataProps = { | |||||||
| } | } | ||||||
|  |  | ||||||
| const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | ||||||
|  |   const nostrController = NostrController.getInstance() | ||||||
|  |  | ||||||
|  |   const [name, setName] = useState<string>() | ||||||
|  |   const [email, setEmail] = useState<string>() | ||||||
|  |   const [subject, setSubject] = useState<string>() | ||||||
|  |   const [message, setMessage] = useState<string>() | ||||||
|  |   const [notification, setNotification] = useState<string>() | ||||||
|  |  | ||||||
|  |   const getBorderStyle = (value: string | undefined) => | ||||||
|  |     value === undefined ? {} : value ? {} : { border: '1px solid red' } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Layout |     <Layout | ||||||
|       location={location} |       location={location} | ||||||
| @@ -52,8 +64,35 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | |||||||
|  |  | ||||||
|             <form |             <form | ||||||
|               className="kwes-form" |               className="kwes-form" | ||||||
|               method="POST" |               onSubmit={async (evt) => { | ||||||
|               action="https://kwes.io/api/foreign/forms/mxKuyK4lxZWnG2WNH3ga" |                 evt.preventDefault() | ||||||
|  |  | ||||||
|  |                 if (name && email && subject && message) { | ||||||
|  |                   const res = await nostrController | ||||||
|  |                     .sendDM( | ||||||
|  |                       'npub1dc0000002dtkw7et06sztc9nvk79r6yju8gk69sr88rgrg0e8cvsnptgyv', | ||||||
|  |                       `Name: ${name} | ||||||
|  | Email: ${email} | ||||||
|  | Subject: ${subject} | ||||||
|  | Message: ${message}` | ||||||
|  |                     ) | ||||||
|  |                     .catch((err) => { | ||||||
|  |                       setNotification( | ||||||
|  |                         `Something went wrong. Please check the console for more information. Please try one more time.` | ||||||
|  |                       ) | ||||||
|  |  | ||||||
|  |                       console.log(`Sending message error: `, err) | ||||||
|  |                     }) | ||||||
|  |  | ||||||
|  |                   if (res && res.length) { | ||||||
|  |                     setNotification(`Message sent. We'll contact you shortly.`) | ||||||
|  |                   } else { | ||||||
|  |                     setNotification( | ||||||
|  |                       `Something went wrong. Please try one more time.` | ||||||
|  |                     ) | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               }} | ||||||
|             > |             > | ||||||
|               <div className="mb-3"> |               <div className="mb-3"> | ||||||
|                 <StyledLabel htmlFor="name" className="form-label"> |                 <StyledLabel htmlFor="name" className="form-label"> | ||||||
| @@ -65,6 +104,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | |||||||
|                   id="name" |                   id="name" | ||||||
|                   name="name" |                   name="name" | ||||||
|                   rules="required|max:50" |                   rules="required|max:50" | ||||||
|  |                   onChange={(evt) => { | ||||||
|  |                     setName(evt.target.value) | ||||||
|  |                     setNotification(undefined) | ||||||
|  |                   }} | ||||||
|  |                   style={getBorderStyle(name)} | ||||||
|                 /> |                 /> | ||||||
|               </div> |               </div> | ||||||
|               <div className="mb-3"> |               <div className="mb-3"> | ||||||
| @@ -78,6 +122,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | |||||||
|                   name="email" |                   name="email" | ||||||
|                   rules="required|email" |                   rules="required|email" | ||||||
|                   aria-describedby="emailHelp" |                   aria-describedby="emailHelp" | ||||||
|  |                   onChange={(evt) => { | ||||||
|  |                     setEmail(evt.target.value) | ||||||
|  |                     setNotification(undefined) | ||||||
|  |                   }} | ||||||
|  |                   style={getBorderStyle(email)} | ||||||
|                 /> |                 /> | ||||||
|                 <div id="emailHelp" className="form-text"> |                 <div id="emailHelp" className="form-text"> | ||||||
|                   We'll never share your email with anyone else. |                   We'll never share your email with anyone else. | ||||||
| @@ -93,6 +142,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | |||||||
|                   id="subject" |                   id="subject" | ||||||
|                   name="subject" |                   name="subject" | ||||||
|                   rules="required|max:50" |                   rules="required|max:50" | ||||||
|  |                   onChange={(evt) => { | ||||||
|  |                     setSubject(evt.target.value) | ||||||
|  |                     setNotification(undefined) | ||||||
|  |                   }} | ||||||
|  |                   style={getBorderStyle(subject)} | ||||||
|                 /> |                 /> | ||||||
|               </div> |               </div> | ||||||
|               <div className="mb-3"> |               <div className="mb-3"> | ||||||
| @@ -105,12 +159,18 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | |||||||
|                   name="message" |                   name="message" | ||||||
|                   rows="5" |                   rows="5" | ||||||
|                   rules="required|max:200" |                   rules="required|max:200" | ||||||
|  |                   onChange={(evt) => { | ||||||
|  |                     setMessage(evt.target.value) | ||||||
|  |                     setNotification(undefined) | ||||||
|  |                   }} | ||||||
|  |                   style={getBorderStyle(message)} | ||||||
|                 ></textarea> |                 ></textarea> | ||||||
|               </div> |               </div> | ||||||
|               <div className="mb-3"> |               <div className="mb-3"> | ||||||
|                 <SolidButton theme="dark">Submit</SolidButton> |                 <SolidButton theme="dark">Submit</SolidButton> | ||||||
|               </div> |               </div> | ||||||
|             </form> |             </form> | ||||||
|  |             {notification && <span>{notification}</span>} | ||||||
|           </div> |           </div> | ||||||
|           <div className="col-md-6"> |           <div className="col-md-6"> | ||||||
|             <ContactBackground src={contactBg} info="Book a Demo" /> |             <ContactBackground src={contactBg} info="Book a Demo" /> | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								src/types/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/types/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | export * from './nostr' | ||||||
							
								
								
									
										14
									
								
								src/types/nostr.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/types/nostr.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | export interface SignedEvent { | ||||||
|  |   kind: number | ||||||
|  |   tags: string[][] | ||||||
|  |   content: string | ||||||
|  |   created_at: number | ||||||
|  |   pubkey: string | ||||||
|  |   id: string | ||||||
|  |   sig: string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface Keys { | ||||||
|  |   private: string | ||||||
|  |   public: string | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user