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-transformer-remark": "^4.0.0", | ||||
|         "gatsby-transformer-sharp": "^3.3.0", | ||||
|         "nostr-tools": "^2.7.0", | ||||
|         "react": "^17.0.1", | ||||
|         "react-dom": "^17.0.1", | ||||
|         "react-helmet": "^6.1.0", | ||||
|         "react-icons": "^4.2.0", | ||||
|         "react-share": "^4.4.0", | ||||
|         "react-use-flexsearch": "^0.1.1", | ||||
|         "styled-components": "^5.2.3" | ||||
|         "styled-components": "^5.2.3", | ||||
|         "tseep": "^1.2.1" | ||||
|       }, | ||||
|       "devDependencies": { | ||||
|         "@popperjs/core": "^2.9.2", | ||||
| @@ -3114,6 +3116,47 @@ | ||||
|         "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": { | ||||
|       "version": "2.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", | ||||
| @@ -3215,6 +3258,53 @@ | ||||
|         "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": { | ||||
|       "version": "4.1.5", | ||||
|       "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", | ||||
| @@ -15854,6 +15944,36 @@ | ||||
|         "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": { | ||||
|       "version": "0.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/not/-/not-0.1.0.tgz", | ||||
| @@ -21030,6 +21150,11 @@ | ||||
|         "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": { | ||||
|       "version": "1.14.1", | ||||
|       "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", | ||||
|   | ||||
| @@ -38,13 +38,15 @@ | ||||
|     "gatsby-source-filesystem": "^3.3.0", | ||||
|     "gatsby-transformer-remark": "^4.0.0", | ||||
|     "gatsby-transformer-sharp": "^3.3.0", | ||||
|     "nostr-tools": "^2.7.0", | ||||
|     "react": "^17.0.1", | ||||
|     "react-dom": "^17.0.1", | ||||
|     "react-helmet": "^6.1.0", | ||||
|     "react-icons": "^4.2.0", | ||||
|     "react-share": "^4.4.0", | ||||
|     "react-use-flexsearch": "^0.1.1", | ||||
|     "styled-components": "^5.2.3" | ||||
|     "styled-components": "^5.2.3", | ||||
|     "tseep": "^1.2.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@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 React from 'react' | ||||
| import React, { useState } from 'react' | ||||
|  | ||||
| import Layout from '../components/layout' | ||||
| import Seo from '../components/seo' | ||||
| @@ -20,6 +20,7 @@ import { | ||||
| import contactBg from '../images/contact_bg.jpg' | ||||
|  | ||||
| import '../styledComponents/contact.css' | ||||
| import { NostrController } from '../controllers' | ||||
|  | ||||
| type DataProps = { | ||||
|   site: { | ||||
| @@ -32,6 +33,17 @@ type DataProps = { | ||||
| } | ||||
|  | ||||
| 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 ( | ||||
|     <Layout | ||||
|       location={location} | ||||
| @@ -52,8 +64,35 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | ||||
|  | ||||
|             <form | ||||
|               className="kwes-form" | ||||
|               method="POST" | ||||
|               action="https://kwes.io/api/foreign/forms/mxKuyK4lxZWnG2WNH3ga" | ||||
|               onSubmit={async (evt) => { | ||||
|                 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"> | ||||
|                 <StyledLabel htmlFor="name" className="form-label"> | ||||
| @@ -65,6 +104,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | ||||
|                   id="name" | ||||
|                   name="name" | ||||
|                   rules="required|max:50" | ||||
|                   onChange={(evt) => { | ||||
|                     setName(evt.target.value) | ||||
|                     setNotification(undefined) | ||||
|                   }} | ||||
|                   style={getBorderStyle(name)} | ||||
|                 /> | ||||
|               </div> | ||||
|               <div className="mb-3"> | ||||
| @@ -78,6 +122,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | ||||
|                   name="email" | ||||
|                   rules="required|email" | ||||
|                   aria-describedby="emailHelp" | ||||
|                   onChange={(evt) => { | ||||
|                     setEmail(evt.target.value) | ||||
|                     setNotification(undefined) | ||||
|                   }} | ||||
|                   style={getBorderStyle(email)} | ||||
|                 /> | ||||
|                 <div id="emailHelp" className="form-text"> | ||||
|                   We'll never share your email with anyone else. | ||||
| @@ -93,6 +142,11 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | ||||
|                   id="subject" | ||||
|                   name="subject" | ||||
|                   rules="required|max:50" | ||||
|                   onChange={(evt) => { | ||||
|                     setSubject(evt.target.value) | ||||
|                     setNotification(undefined) | ||||
|                   }} | ||||
|                   style={getBorderStyle(subject)} | ||||
|                 /> | ||||
|               </div> | ||||
|               <div className="mb-3"> | ||||
| @@ -105,12 +159,18 @@ const Contact: React.FC<PageProps<DataProps>> = ({ data, location }) => { | ||||
|                   name="message" | ||||
|                   rows="5" | ||||
|                   rules="required|max:200" | ||||
|                   onChange={(evt) => { | ||||
|                     setMessage(evt.target.value) | ||||
|                     setNotification(undefined) | ||||
|                   }} | ||||
|                   style={getBorderStyle(message)} | ||||
|                 ></textarea> | ||||
|               </div> | ||||
|               <div className="mb-3"> | ||||
|                 <SolidButton theme="dark">Submit</SolidButton> | ||||
|               </div> | ||||
|             </form> | ||||
|             {notification && <span>{notification}</span>} | ||||
|           </div> | ||||
|           <div className="col-md-6"> | ||||
|             <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