├── .prettierrc.json ├── src ├── react-app-env.d.ts ├── assets │ ├── favicon.ico │ ├── icon-delete.png │ ├── nostr-logo.png │ ├── bitcoin-icon.png │ ├── nostr-icon-user.avif │ ├── profile-icon-dm.svg │ ├── profile-icon-lg.svg │ ├── notification-icon-dm.svg │ ├── notification-icon-lg.svg │ ├── home-icon-dm.svg │ ├── home-icon-lg.svg │ ├── copy-icon-dm.svg │ ├── copy-icon-lg.svg │ ├── delete-icon.svg │ ├── edit-icon-dm.svg │ ├── edit-icon-lg.svg │ ├── create-icon-dm.svg │ ├── create-icon-lg.svg │ ├── external-link-icon-dm.svg │ ├── external-link-icon-lg.svg │ ├── website-icon-dm.svg │ ├── website-icon-lg.svg │ ├── server-icon-dm.svg │ ├── server-icon-lg.svg │ ├── share-icon-dm.svg │ ├── share-icon-lg.svg │ ├── github-icon-dm.svg │ ├── github-icon-lg.svg │ ├── status-active.svg │ ├── close-icon-dm.svg │ ├── close-icon-lg.svg │ ├── add-icon-dm.svg │ └── check-mark.svg ├── components │ ├── errors │ │ ├── bountiesNotFound.tsx │ │ ├── couldNotShare.tsx │ │ ├── isNotLogged.tsx │ │ ├── extensionError.tsx │ │ └── emptyFields.tsx │ ├── profileCard │ │ ├── profileStats │ │ │ ├── profileBountiesAddedReward.tsx │ │ │ ├── profileActivity.tsx │ │ │ ├── profileBountiesPaid.tsx │ │ │ └── profileBountiesProgress.tsx │ │ └── profileCard.tsx │ ├── bounty │ │ ├── bountyApplicantsBox │ │ │ ├── bountyApplicantsBoxStatus.tsx │ │ │ └── bountyApplicantsBox.tsx │ │ ├── bountyApplication │ │ │ └── bountyApplicationCard.tsx │ │ ├── bountyStatus │ │ │ └── bountyStatus.tsx │ │ ├── bountyCardShortInfo │ │ │ └── bountyCardShortInfo.tsx │ │ ├── bountyEditor │ │ │ └── bountyEditor.tsx │ │ └── bountyLargeInfo │ │ │ └── bountyLargeInfoOpen.tsx │ ├── notifications │ │ └── notificationBox.tsx │ ├── menus │ │ ├── sidebarMenu │ │ │ └── sidebarMenu.tsx │ │ └── mobileMenu │ │ │ └── mobileMenu.tsx │ └── payment │ │ └── LNInvoice.tsx ├── const.tsx ├── App.css ├── pages │ ├── error.tsx │ ├── editBounty.tsx │ ├── relays.tsx │ ├── notifications.tsx │ ├── tags │ │ ├── designBounties.tsx │ │ ├── writingBounties.tsx │ │ ├── debugginBounties.tsx │ │ ├── marketingBounties.tsx │ │ ├── developmentBounties.tsx │ │ └── cybersecurityBounties.tsx │ ├── bountyFullInfo.tsx │ └── profile.tsx ├── index.css ├── index.tsx ├── App.tsx └── utils.tsx ├── public ├── logo.png ├── favicon.ico ├── robots.txt ├── manifest.json └── index.html ├── .prettierignore ├── postcss.config.js ├── README.md ├── .gitignore ├── tsconfig.json ├── tailwind.config.js ├── LICENSE ├── package.json └── EventStructure.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "singleQuote": false 4 | } -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diamsa/nostrbounties/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diamsa/nostrbounties/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | node_modules 5 | /.pnp 6 | .pnp.js -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diamsa/nostrbounties/HEAD/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/icon-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diamsa/nostrbounties/HEAD/src/assets/icon-delete.png -------------------------------------------------------------------------------- /src/assets/nostr-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diamsa/nostrbounties/HEAD/src/assets/nostr-logo.png -------------------------------------------------------------------------------- /src/assets/bitcoin-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diamsa/nostrbounties/HEAD/src/assets/bitcoin-icon.png -------------------------------------------------------------------------------- /src/assets/nostr-icon-user.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/diamsa/nostrbounties/HEAD/src/assets/nostr-icon-user.avif -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/assets/profile-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/profile-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/notification-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/notification-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/home-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/home-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/errors/bountiesNotFound.tsx: -------------------------------------------------------------------------------- 1 | function BountiesNotFound() { 2 | return ( 3 |

4 | bounties not found, try again with different relays 5 |

6 | ); 7 | } 8 | 9 | export default BountiesNotFound; 10 | -------------------------------------------------------------------------------- /src/components/errors/couldNotShare.tsx: -------------------------------------------------------------------------------- 1 | function CouldNotShare() { 2 | return ( 3 |
4 |

Link could not be copied

5 |
6 | ); 7 | } 8 | 9 | export default CouldNotShare; 10 | -------------------------------------------------------------------------------- /src/assets/copy-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/copy-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/delete-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/edit-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/edit-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/profileCard/profileStats/profileBountiesAddedReward.tsx: -------------------------------------------------------------------------------- 1 | function SatsAdded({ amount }: any) { 2 | return ( 3 |
4 |
{amount}
5 | Sats contributed to bounties 6 |
7 | ); 8 | } 9 | 10 | export default SatsAdded; 11 | -------------------------------------------------------------------------------- /src/assets/create-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/create-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/external-link-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/external-link-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Nostrbounties 2 | 3 | Nostrbounties is a nostr client used to post, manage and find bounties. Check https://nostrbounties.com 4 | 5 | ## Prerequisites 6 | 7 | - Node.js 8 | - NPM 9 | - A nostr extension to sign events 10 | 11 | ## How to setup the client? 12 | 13 | 1. `git clone https://github.com/diamsa/nostrbounties.git` 14 | 2. ` cd nostrbounties` 15 | 3. `npm install` 16 | 4. `npm start` 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/assets/website-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/website-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/errors/isNotLogged.tsx: -------------------------------------------------------------------------------- 1 | function IsNotLogged({ hideElement }: any) { 2 | setTimeout(() => { 3 | hideElement(false); 4 | }, 2000); 5 | 6 | return ( 7 |
8 |

Please sign in first

9 |
10 | ); 11 | } 12 | 13 | export default IsNotLogged; 14 | -------------------------------------------------------------------------------- /src/assets/server-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/server-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/share-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/share-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/github-icon-dm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/github-icon-lg.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/profileCard/profileStats/profileActivity.tsx: -------------------------------------------------------------------------------- 1 | function profileActivity({ activity }: any) { 2 | return ( 3 |
4 |
5 | {activity < 10 ? "Low" : null} 6 | {activity > 11 && activity < 20 ? "Medium" : null} 7 | {activity >= 21 ? "High" : null} 8 |
9 | Nostr activity, last 30 days 10 |
11 | ); 12 | } 13 | 14 | export default profileActivity; 15 | -------------------------------------------------------------------------------- /src/assets/status-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/profileCard/profileStats/profileBountiesPaid.tsx: -------------------------------------------------------------------------------- 1 | function BountiesPaid({ bountiesPaid }: any) { 2 | function bountyQuantity() { 3 | let arrBountiesPaid = Object.values(bountiesPaid); 4 | let itemAmount = arrBountiesPaid.filter((item: any) => item[0] === "paid"); 5 | return itemAmount.length; 6 | } 7 | 8 | return ( 9 |
10 |
{bountyQuantity()}
11 | Bounties paid 12 |
13 | ); 14 | } 15 | 16 | export default BountiesPaid; 17 | -------------------------------------------------------------------------------- /src/const.tsx: -------------------------------------------------------------------------------- 1 | export let defaultRelaysToPublish = [ 2 | "wss://nostr-pub.wellorder.net/", 3 | "wss://relay.nostr.scot", 4 | ]; 5 | 6 | export let defaultRelays = [ 7 | "wss://nos.lol", 8 | "wss://relay.damus.io", 9 | "wss://relay.snort.social", 10 | "wss://nostr.pleb.network", 11 | "wss://nostr01.opencult.com", 12 | ]; 13 | 14 | export let allRelays = [ 15 | "wss://nos.lol", 16 | "wss://relay.damus.io", 17 | "wss://nostr.pleb.network", 18 | "wss://relay.snort.social", 19 | "wss://nostr-pub.wellorder.net/", 20 | "wss://relay.nostr.scot", 21 | "wss://nostr01.opencult.com", 22 | ]; 23 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/profileCard/profileStats/profileBountiesProgress.tsx: -------------------------------------------------------------------------------- 1 | function BountiesProgress({ bountiesProgress }: any) { 2 | function bountyQuantity() { 3 | let arrBountiesInProgress = Object.values(bountiesProgress); 4 | let itemAmount = arrBountiesInProgress.filter( 5 | (item: any) => item[0] === "in progress" 6 | ); 7 | return itemAmount.length; 8 | } 9 | 10 | return ( 11 |
12 |
{bountyQuantity()}
13 | Bounties In progress 14 |
15 | ); 16 | } 17 | 18 | export default BountiesProgress; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "lib": ["dom", "dom.iterable", "esnext", "ES6"], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react-jsx" 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "dist"] 23 | } 24 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/close-icon-dm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/close-icon-lg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/pages/error.tsx: -------------------------------------------------------------------------------- 1 | import SideBarMenu from "../components/menus/sidebarMenu/sidebarMenu"; 2 | 3 | import MobileMenu from "../components/menus/mobileMenu/mobileMenu"; 4 | 5 | function Error404() { 6 | return ( 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 |

17 | Oops, page wasn't found 18 |

19 |
20 |
21 | ); 22 | } 23 | 24 | export default Error404; 25 | -------------------------------------------------------------------------------- /src/components/errors/extensionError.tsx: -------------------------------------------------------------------------------- 1 | function ExtensionError() { 2 | return ( 3 |
7 | 20 | Info 21 |
You need an extension to post
22 |
23 | ); 24 | } 25 | 26 | export default ExtensionError; 27 | -------------------------------------------------------------------------------- /src/components/errors/emptyFields.tsx: -------------------------------------------------------------------------------- 1 | function EmptyFields() { 2 | return ( 3 |
7 | 20 | Info 21 | Title, description and reward fields are required 22 |
23 | ); 24 | } 25 | 26 | export default EmptyFields; 27 | -------------------------------------------------------------------------------- /src/assets/add-icon-dm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | module.exports = { 4 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 5 | theme: { 6 | extend: { 7 | screens: { 8 | sm: { max: "640px" }, 9 | }, 10 | colors: { 11 | "gray-1": "#bababe", 12 | "gray-2": "#DDE1E6", 13 | "dark-text": "#121619", 14 | "blue-1": "#0043CE", 15 | "alert-1": "#da1e28", 16 | "alert-2": "#fa4d56", 17 | "status-open": "#a7f0ba", 18 | "status-open-text": "#0e6027", 19 | "status-in-progress": "#f1c21b", 20 | "status-in-progress-text": "#4f410d", 21 | "status-paid": "#0043ce", 22 | "status-paid-text": "#D0E2FF", 23 | "current-tab": "#9dacc4", 24 | "background-dark-mode": "#0c0c0c", 25 | "background-component-dm": "#001b52", 26 | "sidebar-bg": "#131314", 27 | "input-bg-dm": "#1d1d1f", 28 | "sidebar-gray": "#F2F4F8", 29 | }, 30 | }, 31 | }, 32 | plugins: [require("@tailwindcss/typography")], 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 diamsa 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nostrbounties", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@headlessui/react": "^1.7.11", 7 | "@heroicons/react": "^2.0.16", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "@types/jest": "^27.5.2", 12 | "@types/node": "^16.18.12", 13 | "@types/react": "^18.0.28", 14 | "@types/react-dom": "^18.0.11", 15 | "bech32": "^2.0.0", 16 | "nostr-relaypool": "^0.5.3", 17 | "nostr-tools": "^1.7.4", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-markdown": "^8.0.5", 21 | "react-qr-code": "^2.0.11", 22 | "react-router-dom": "^6.8.1", 23 | "react-scripts": "5.0.1", 24 | "timestamp-conv": "^3.0.0", 25 | "web-vitals": "^2.1.4", 26 | "websocket-polyfill": "^0.0.3" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject" 33 | }, 34 | "eslintConfig": { 35 | "extends": [ 36 | "react-app", 37 | "react-app/jest" 38 | ] 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "@tailwindcss/typography": "^0.5.9", 54 | "autoprefixer": "^10.4.13", 55 | "postcss": "^8.4.21", 56 | "prettier": "2.8.7", 57 | "tailwindcss": "^3.2.7", 58 | "typescript": "^4.9.5" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/assets/check-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @layer components { 5 | .markdown > h1 { 6 | @apply font-medium font-sans text-xl mb-2 mr-2 text-dark-text dark:text-gray-2; 7 | } 8 | .markdown > h2 { 9 | @apply font-normal font-sans text-base mb-2 mr-2 text-dark-text dark:text-gray-2; 10 | } 11 | .markdown > ul { 12 | @apply font-light font-sans text-sm ml-5 my-2 text-dark-text list-disc dark:text-gray-2; 13 | } 14 | .markdown > ol { 15 | @apply font-light font-sans text-sm ml-5 my-2 text-dark-text list-decimal dark:text-gray-2; 16 | } 17 | 18 | .markdown > p { 19 | @apply font-normal font-sans text-sm mt-2 mb-2 mr-2 text-dark-text dark:text-gray-2; 20 | } 21 | 22 | .markdown > blockquote { 23 | @apply border-l-8 bg-sidebar-gray py-3 pl-2 border-blue-1 text-dark-text font-normal text-base italic dark:text-gray-2 dark:bg-input-bg-dm; 24 | } 25 | .markdown > hr { 26 | @apply border-l-2 border-dark-text dark:text-gray-2 dark:border-gray-2; 27 | } 28 | .markdown > pre { 29 | @apply bg-sidebar-gray text-sm px-3 py-2 whitespace-pre-wrap text-dark-text dark:text-gray-2 dark:bg-input-bg-dm; 30 | } 31 | .markdown > p > code { 32 | @apply bg-sidebar-gray text-sm italic px-2 py-1 whitespace-pre-wrap text-dark-text dark:text-gray-2 dark:bg-input-bg-dm; 33 | } 34 | } 35 | @layer utilities { 36 | @variants responsive { 37 | /* Hide scrollbar for Chrome, Safari and Opera */ 38 | .no-scrollbar::-webkit-scrollbar { 39 | display: none; 40 | } 41 | 42 | /* Hide scrollbar for IE, Edge and Firefox */ 43 | .no-scrollbar { 44 | -ms-overflow-style: none; /* IE and Edge */ 45 | scrollbar-width: none; /* Firefox */ 46 | } 47 | } 48 | } 49 | 50 | .Profile-Stat-Card { 51 | @apply flex flex-col basis-1/5 items-center bg-white border border-blue-1 rounded-lg p-3 m-2 shadow-md dark:bg-sidebar-bg; 52 | } 53 | 54 | .Profile-Stat-Subtext { 55 | @apply text-xs text-center text-gray-500 dark:text-gray-1; 56 | } 57 | 58 | .Profile-Stat-Stat { 59 | @apply mb-1 text-2xl font-medium text-dark-text dark:text-gray-1; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/profileCard/profileCard.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | import AvatarImage from "../../assets/nostr-icon-user.avif"; 4 | import checkMark from "../../assets/check-mark.svg"; 5 | 6 | function profileCard({ metaData, userNip05, npub }: any) { 7 | return ( 8 |
9 |
10 | avatar 19 |
20 | {metaData.name === undefined ? metaData.display_name : metaData.name} 21 |
22 |
23 | 24 | {metaData.nip05 === undefined || null 25 | ? "nip05 not found" 26 | : metaData.nip05} 27 | 28 | {userNip05 ? ( 29 | avatar 34 | ) : null} 35 |
36 | 37 | 38 | ⚡{" "} 39 | {metaData.LnAddress === undefined 40 | ? "LN address not found" 41 | : metaData.LnAddress} 42 | 43 | 44 | {metaData.about === undefined ? "about not found" : metaData.about} 45 | 46 | 50 | snort social profile 51 | 52 |
53 |
54 | ); 55 | } 56 | 57 | export default profileCard; 58 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 21 | 22 | 23 | 27 | 28 | 33 | 34 | 43 | NostrBounties: complete tasks and get paid with Bitcoin ₿ 44 | 45 | 46 | 47 |
48 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import { createBrowserRouter, RouterProvider } from "react-router-dom"; 6 | 7 | import Profile from "./pages/profile"; 8 | import CreateBounty from "./pages/createBounty"; 9 | import BountyFullInfo from "./pages/bountyFullInfo"; 10 | import EditBounty from "./pages/editBounty"; 11 | import DesignBounties from "./pages/tags/designBounties"; 12 | import WritingBounties from "./pages/tags/writingBounties"; 13 | import DebuggingBounties from "./pages/tags/debugginBounties"; 14 | import DevelopmentBounties from "./pages/tags/developmentBounties"; 15 | import MarketingBounties from "./pages/tags/marketingBounties"; 16 | import CybersecurityBounties from "./pages/tags/cybersecurityBounties"; 17 | import Error404 from "./pages/error"; 18 | import Relays from "./pages/relays"; 19 | import Notifications from "./pages/notifications"; 20 | 21 | const router = createBrowserRouter([ 22 | { 23 | errorElement: , 24 | children: [ 25 | { 26 | path: "/", 27 | element: , 28 | }, 29 | { 30 | path: "profile/:id", 31 | element: , 32 | }, 33 | { 34 | path: "create", 35 | element: , 36 | }, 37 | { 38 | path: "/b/:id", 39 | element: , 40 | }, 41 | { 42 | path: "/edit/:id", 43 | element: , 44 | }, 45 | 46 | { 47 | path: "/tag/design", 48 | element: , 49 | }, 50 | { 51 | path: "/tag/writing", 52 | element: , 53 | }, 54 | { 55 | path: "/tag/debugging", 56 | element: , 57 | }, 58 | { 59 | path: "/tag/cybersecurity", 60 | element: , 61 | }, 62 | { 63 | path: "/tag/development", 64 | element: , 65 | }, 66 | { 67 | path: "/tag/marketing", 68 | element: , 69 | }, 70 | { 71 | path: "/relays", 72 | element: , 73 | }, 74 | { 75 | path: "/notifications", 76 | element: , 77 | }, 78 | ], 79 | }, 80 | ]); 81 | 82 | const root = ReactDOM.createRoot( 83 | document.getElementById("root") as HTMLElement 84 | ); 85 | root.render(); 86 | 87 | // If you want to start measuring performance in your app, pass a function 88 | // to log results (for example: reportWebVitals(console.log)) 89 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 90 | -------------------------------------------------------------------------------- /src/pages/editBounty.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { RelayPool } from "nostr-relaypool"; 4 | import { defaultRelaysToPublish } from "../const"; 5 | import { nip19 } from "nostr-tools"; 6 | 7 | import SideBarMenu from "../components/menus/sidebarMenu/sidebarMenu"; 8 | import BountyEditor from "../components/bounty/bountyEditor/bountyEditor"; 9 | import MobileMenu from "../components/menus/mobileMenu/mobileMenu"; 10 | 11 | function EditBounty() { 12 | let relays = defaultRelaysToPublish; 13 | let params = useParams(); 14 | let naddrData = nip19.decode(params.id!); 15 | let subFilter = [ 16 | { 17 | // @ts-ignore 18 | "#d": [`${naddrData.data.identifier}`], 19 | kinds: [30023], 20 | }, 21 | ]; 22 | 23 | let [oldEvent, setOldEvent] = useState({}); 24 | let [loaded, setLoaded] = useState(false); 25 | 26 | useEffect(() => { 27 | let relayPool = new RelayPool(relays); 28 | 29 | relayPool.onerror((err, relayUrl) => { 30 | console.log("RelayPool error", err, " from relay ", relayUrl); 31 | }); 32 | relayPool.onnotice((relayUrl, notice) => { 33 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 34 | }); 35 | 36 | relayPool.subscribe( 37 | subFilter, 38 | relays, 39 | (event, isAfterEose, relayURL) => { 40 | let tags_arr: string[] = []; 41 | event.tags.map((item) => { 42 | if (item[0] === "rootId") { 43 | tags_arr.push(item[1]); 44 | } 45 | }); 46 | if (tags_arr.length === 0) { 47 | event.tags.push(["rootId", event.id]); 48 | } 49 | setOldEvent({ 50 | id: event.id, 51 | pubkey: event.pubkey, 52 | created_at: event.created_at, 53 | kind: event.kind, 54 | tags: event.tags, 55 | content: event.content, 56 | sig: event.sig, 57 | }); 58 | 59 | setLoaded(true); 60 | }, 61 | undefined, 62 | undefined, 63 | { unsubscribeOnEose: true } 64 | ); 65 | 66 | setTimeout(() => { 67 | relayPool.close().then(() => { 68 | console.log("connection closed"); 69 | }); 70 | }, 40000); 71 | }, []); 72 | 73 | return ( 74 |
75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 | {loaded ? : null} 83 |
84 |
85 | ); 86 | } 87 | 88 | export default EditBounty; 89 | -------------------------------------------------------------------------------- /EventStructure.md: -------------------------------------------------------------------------------- 1 | # Nostr Bounty Event Format 2 | 3 | `draft` 4 | 5 | The [nostrbounties.com](https://nostrbounties.com) website serves as a client for finding and posting bounties to Nostr. This document outlines the structure of the tags used by this event type, aiming to enhance collaboration and compatibility with other bounty systems. 6 | 7 | ## Event format 8 | 9 | This document specifies the use of event type `30023` (long form content) for Nostr bounty events: 10 | 11 | - `t` hashtag(s) 12 | - `title` bounty title 13 | - `reward` bounty reward (in sats) 14 | - `published_at` bounty publication Unix timestamp (seconds) 15 | - `d` timestamp (seconds) 16 | 17 | ```json 18 | { 19 | "id": "", 20 | "pubkey": "", 21 | "created_at: , 22 | "kind": <30023>, 23 | "tags": [ 24 | ["t", ], 25 | ["title", ], 26 | ["reward", ], 27 | ["published_at", ], 28 | ["d", ] 29 | ], 30 | "content": "", 31 | "sig": 32 | } 33 | ``` 34 | 35 | ## Example: 36 | ```json 37 | { 38 | "id": "aa261c71c3e551b4186ecedcde029cce01ab62af0f377e2894d06583f94d717e", 39 | "pubkey": "21b419102da8fc0ba90484aec934bf55b7abcf75eedb39124e8d75e491f41a5e", 40 | "created_at: 1700681922, 41 | "kind": 30023, 42 | "tags": [ 43 | [ 44 | "t", 45 | "bounty" 46 | ], 47 | [ 48 | "title", 49 | "NostrBounties: Document Event Structure" 50 | ], 51 | [ 52 | "reward", 53 | "10000" 54 | ], 55 | [ 56 | "published_at", 57 | "1700681922" 58 | ], 59 | [ 60 | "d", 61 | "1700681922" 62 | ], 63 | [ 64 | "t", 65 | "development-bounty" 66 | ], 67 | [ 68 | "t", 69 | "writing-bounty" 70 | ] 71 | ], 72 | "content": "The nostrbounties.com website is a nostr client for finding and posting bounties to nostr. To improve collaboration and compatibility with other bounty systems, it would be helpful if the structure of events were documented to avoid the need to reverse engineer/infer the structure\n\nTo satisfy this bounty,\n\n- a markdown file should be added to the code repository for nostr bounties (https://github.com/diamsa/nostrbounties/tree/master)\n- a section of the file should explain the event structure and provide sample json for a new nostr bounty, including the required tags for association\n- a section of the file should explain event structure and provide sample json for adding to a nostr bounty\n- an optional section can address historical/deprecated formats but this is NOT required to satisfy the bounty, 73 | "sig": "f91303eaff850ba57ac4f8dbe6733d7bed97cd370ba0be7c1d537106ed44ac05ec16816612901c66b80a480609702396b25deb8dfe2a86c49e3bf02ad75ade04" 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /src/components/bounty/bountyApplicantsBox/bountyApplicantsBoxStatus.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { nip19 } from "nostr-tools"; 3 | import { getNpub, isDarkTheme, convertTimestamp } from "../../../utils"; 4 | 5 | import avatarImage from "../../../assets/nostr-icon-user.avif"; 6 | import copyIconDm from "../../../assets/copy-icon-dm.svg"; 7 | import copyIconLg from "../../../assets/copy-icon-lg.svg"; 8 | 9 | function CommentBox({ 10 | pubkey, 11 | name, 12 | profilePic, 13 | createdAt, 14 | changedNpubValue, 15 | }: any) { 16 | let npub = nip19.npubEncode(pubkey); 17 | let npubShortened = getNpub(pubkey); 18 | let datePosted = convertTimestamp(createdAt); 19 | 20 | return ( 21 |
22 |
23 |
24 |
25 |
26 | {profilePic === "" ? ( 27 |
28 | avatar image 33 |
34 | ) : ( 35 |
36 | avatar image 41 |
42 | )} 43 |
44 | {name === "" ? ( 45 |
46 |

47 | {npubShortened} 48 |

49 |

50 | {" "} 51 | applied to this bounty {datePosted}. 52 |

53 |
54 | ) : ( 55 |
56 |

57 | {name} 58 |

59 |

60 | {" "} 61 | applied to this bounty {datePosted}. 62 |

63 |
64 | )} 65 |
66 |
67 |
68 |
69 | changedNpubValue(npub)} 73 | src={isDarkTheme() ? copyIconDm : copyIconLg} 74 | alt="delete icon" 75 | > 76 |
77 |
78 |
79 |
80 | ); 81 | } 82 | 83 | export default CommentBox; 84 | -------------------------------------------------------------------------------- /src/components/bounty/bountyApplication/bountyApplicationCard.tsx: -------------------------------------------------------------------------------- 1 | import { isDarkTheme, sendApplication } from "../../../utils"; 2 | import { useState } from "react"; 3 | 4 | import closeIconLg from "../../../assets/close-icon-lg.svg"; 5 | import closeIconDm from "../../../assets/close-icon-dm.svg"; 6 | 7 | function BountyApplicationCard({ 8 | isOpen, 9 | closeModal, 10 | dTag, 11 | updateValues, 12 | dataLoaded, 13 | }: any) { 14 | let [applicationText, setApplicationText] = useState(""); 15 | let [githubLink, setGithubLink] = useState(""); 16 | let [personalWebsite, setPersonalWebsite] = useState(""); 17 | let content = applicationText; 18 | let links = [githubLink, personalWebsite]; 19 | 20 | return ( 21 |
22 | {isOpen ? ( 23 |
24 |
25 |
26 | {" "} 27 |
28 | 31 | closeModal()} 34 | src={isDarkTheme() ? closeIconLg : closeIconDm} 35 | alt="delete icon" 36 | > 37 |
38 | 45 |
46 |
47 | 50 | setGithubLink(e.target.value)} 53 | className="peer min-h-[auto] bg-gray-50 border-y border-x border-dark-text text-dark-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-sidebar-bg dark:text-gray-1 border-0" 54 | placeholder="https://github.com/pepe" 55 | required 56 | /> 57 |
58 |
59 | 62 | setPersonalWebsite(e.target.value)} 65 | className="peer min-h-[auto] bg-gray-50 border-y border-x border-dark-text text-dark-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-sidebar-bg dark:text-gray-1 border-0" 66 | placeholder="https://yourdomain.com" 67 | required 68 | /> 69 |
70 |
71 | 82 |
83 |
84 |
85 | ) : null} 86 |
87 | ); 88 | } 89 | 90 | export default BountyApplicationCard; 91 | -------------------------------------------------------------------------------- /src/components/bounty/bountyStatus/bountyStatus.tsx: -------------------------------------------------------------------------------- 1 | import { isDarkTheme, sendReply } from "../../../utils"; 2 | import { useState } from "react"; 3 | 4 | import closeIconLg from "../../../assets/close-icon-lg.svg"; 5 | import closeIconDm from "../../../assets/close-icon-dm.svg"; 6 | import ApplicationBox from "../bountyApplicantsBox/bountyApplicantsBoxStatus"; 7 | 8 | function BountyUpdateStatusCard({ 9 | isModalOpen, 10 | closeModal, 11 | dTag, 12 | currentStatus, 13 | applicants, 14 | posterPubkey, 15 | naddr, 16 | id, 17 | updateValues, 18 | dataLoaded, 19 | }: any) { 20 | let [bountyHunterNpub, setBountyHunterNpub] = useState(""); 21 | 22 | return ( 23 |
24 | {isModalOpen ? ( 25 |
26 |
27 |
28 | {" "} 29 |
30 | 33 | closeModal()} 36 | src={isDarkTheme() ? closeIconLg : closeIconDm} 37 | alt="close icon" 38 | > 39 |
40 | setBountyHunterNpub(e.target.value)} 43 | className="peer min-h-[auto] bg-gray-50 border-y border-x border-dark-text text-dark-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-sidebar-bg dark:text-gray-1 border-0" 44 | placeholder="Enter bounty hunter's npub" 45 | value={bountyHunterNpub} 46 | required 47 | /> 48 |
49 | 52 | {applicants.map((applications: any) => { 53 | return ( 54 |
55 |
56 | 63 |
64 |
65 | ); 66 | })} 67 | {applicants < 1 ? ( 68 |

69 | No one has applied to your bounty yet. 70 |

71 | ) : null} 72 | 73 | {bountyHunterNpub.startsWith("npub1") && 74 | bountyHunterNpub.length === 63 ? ( 75 |
76 | 94 |
95 | ) : null} 96 |
97 |
98 | ) : null} 99 |
100 | ); 101 | } 102 | 103 | export default BountyUpdateStatusCard; 104 | -------------------------------------------------------------------------------- /src/components/bounty/bountyCardShortInfo/bountyCardShortInfo.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate, Link } from "react-router-dom"; 2 | import { getNpub } from "../../../utils"; 3 | import { nip19 } from "nostr-tools"; 4 | import { useEffect, useState } from "react"; 5 | 6 | import bitcoinIcon from "../../../assets/bitcoin-icon.png"; 7 | import defaultAvatar from "../../../assets/nostr-icon-user.avif"; 8 | 9 | type props = { 10 | ev: { 11 | Dtag: string; 12 | createdAt: string; 13 | name: string; 14 | profilePic: string; 15 | pubkey: string; 16 | reward: string; 17 | tags: string[]; 18 | title: string; 19 | }; 20 | }; 21 | 22 | function ShortBountyInfo({ ev, status }: props | any) { 23 | const navigate = useNavigate(); 24 | let npub = getNpub(ev.pubkey); 25 | let naddr = nip19.naddrEncode({ 26 | identifier: ev.Dtag, 27 | pubkey: ev.pubkey, 28 | kind: 30023, 29 | }); 30 | 31 | let bountyInfoPath = `/b/${naddr}`; 32 | let bountyPosterPath = `/profile/${nip19.npubEncode(ev.pubkey)}`; 33 | let [name, setName] = useState(""); 34 | let [profilePic, setProfilePic] = useState(""); 35 | let [currentStatus, setCurrentStatus] = useState(null); 36 | 37 | useEffect(() => { 38 | setProfilePic(ev.profilePic); 39 | setName(ev.name); 40 | 41 | let dTagExist = status.hasOwnProperty(ev.Dtag); 42 | 43 | if (dTagExist) { 44 | let bountyCurrentStatus = status[ev.Dtag][0]; 45 | setCurrentStatus(bountyCurrentStatus); 46 | } 47 | }, []); 48 | 49 | return ( 50 |
51 |
52 | 56 |

57 | {ev.title} 58 |

59 |
60 | bitcoin 65 |

66 | {ev.reward} sats 67 |

68 |
69 | 70 |
71 |
72 |
73 |
74 | avatar image 83 |
84 | 88 | {name === "" || name === undefined ? npub : name} 89 | 90 |
91 |
92 |
93 | 94 | {ev.createdAt} 95 | 96 |
97 | {currentStatus === null ? ( 98 |

99 | open 100 |

101 | ) : null} 102 | {currentStatus === "in progress" ? ( 103 |

104 | in progress 105 |

106 | ) : null} 107 | {currentStatus === "paid" ? ( 108 |

109 | paid 110 |

111 | ) : null} 112 |
113 |
114 |
115 | ); 116 | } 117 | 118 | export default ShortBountyInfo; 119 | -------------------------------------------------------------------------------- /src/pages/relays.tsx: -------------------------------------------------------------------------------- 1 | import SideBarMenu from "../components/menus/sidebarMenu/sidebarMenu"; 2 | import { getRelayData } from "../utils"; 3 | import { useEffect, useState } from "react"; 4 | import { defaultRelays, defaultRelaysToPublish } from "../const"; 5 | import MobileMenu from "../components/menus/mobileMenu/mobileMenu"; 6 | import active from "../assets/status-active.svg"; 7 | 8 | function Relays() { 9 | let [relaysDefaultInfo, setRelaysDefaultInfo] = useState([]); 10 | let [relaysDefaultPublish, setRelaysDefaultPublish] = useState([]); 11 | 12 | function relayId(id: string) { 13 | if (id === "nos.lol") { 14 | return "wss://nos.lol"; 15 | } 16 | if (id === "ynxRIb8tKvoGsR3DkYtrH21vkxCmHTRmehq8zyQ5y7s") { 17 | return "wss://arnostr.permadao.io"; 18 | } 19 | if (id === "damus.io ") { 20 | return "wss://relay.damus.io"; 21 | } 22 | if (id === "nostream.your-domain.com") { 23 | return "wss://nostr.pleb.network"; 24 | } 25 | } 26 | 27 | useEffect(() => { 28 | defaultRelays.map((item) => { 29 | getRelayData(item).then((item) => { 30 | setRelaysDefaultInfo((arr) => [item, ...arr]); 31 | console.log(item); 32 | }); 33 | }); 34 | 35 | defaultRelaysToPublish.map((item) => { 36 | getRelayData(item).then((item) => { 37 | setRelaysDefaultPublish((arr) => [item, ...arr]); 38 | }); 39 | }); 40 | 41 | }, []); 42 | 43 | return ( 44 |
45 |
46 | 47 |
48 |
49 | 50 |
51 | 52 |
53 |
54 |

55 | Default Relays 56 |

57 |
58 |
59 | {relaysDefaultInfo.map((item: any) => { 60 | return ( 61 |
65 |
66 | delete icon 71 |

72 | {item.hasOwnProperty("id") ? item.id : relayId(item.name)} 73 |

74 |
75 | 76 |

77 | nips supported: {item.supported_nips.join(" ")} 78 |

79 |
80 | ); 81 | })} 82 | {relaysDefaultPublish.map((item: any) => { 83 | return ( 84 |
88 |
89 | delete icon 94 |

95 | {item.hasOwnProperty("id") ? item.id : relayId(item.name)} 96 |

97 |
98 | 99 |

100 | nips supported: {item.supported_nips.join(" ")} 101 |

102 |
103 | ); 104 | })} 105 |
106 |
107 |
108 | ); 109 | } 110 | 111 | export default Relays; 112 | -------------------------------------------------------------------------------- /src/pages/notifications.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { RelayPool } from "nostr-relaypool"; 3 | import { defaultRelaysToPublish, defaultRelays } from "../const"; 4 | 5 | import SideBarMenu from "../components/menus/sidebarMenu/sidebarMenu"; 6 | import NotificationCard from "../components/notifications/notificationBox"; 7 | import MobileMenu from "../components/menus/mobileMenu/mobileMenu"; 8 | 9 | type allBountyNotifications = 10 | | [number, string, string, string, string[]] 11 | | [number, string, string, string[]]; 12 | 13 | function Notifications() { 14 | let userPubkey = sessionStorage.getItem("pubkey"); 15 | let subFilterContent = [ 16 | { 17 | kinds: [30023], 18 | "#t": ["bounty"], 19 | authors: [userPubkey!], 20 | }, 21 | ]; 22 | 23 | let [sortedElements, setSortedElements] = useState( 24 | [] 25 | ); 26 | let [dataLoaded, setDataLoaded] = useState(false); 27 | let [allNotifications, setAllNotifications] = useState< 28 | allBountyNotifications[] 29 | >([]); 30 | 31 | useEffect(() => { 32 | let relayPool = new RelayPool(defaultRelays, { useEventCache: true }); 33 | 34 | relayPool.onerror((err, relayUrl) => { 35 | console.log("RelayPool error", err, " from relay ", relayUrl); 36 | }); 37 | relayPool.onnotice((relayUrl, notice) => { 38 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 39 | }); 40 | 41 | relayPool.subscribe( 42 | subFilterContent, 43 | defaultRelaysToPublish, 44 | (event, afterEose, relayURL) => { 45 | let bountyDtag = event.tags[4][1]; 46 | let bountyTitle = event.tags[1][1]; 47 | let bountyMetaData = [ 48 | `30023:${event.pubkey}:${bountyDtag}`, 49 | bountyTitle, 50 | ]; 51 | let subFilterAddedReward = [ 52 | { 53 | "#t": ["bounty-added-reward"], 54 | "#a": [`30023:${event.pubkey}:${bountyDtag}`], 55 | kinds: [1], 56 | }, 57 | ]; 58 | 59 | relayPool.subscribe( 60 | subFilterAddedReward, 61 | defaultRelays, 62 | (event, isAfterEose, relayUrl) => { 63 | if (!parseInt(event.content)) { 64 | setAllNotifications((arr) => [ 65 | ...arr, 66 | [ 67 | event.created_at, 68 | event.pubkey, 69 | "reward", 70 | event.tags[1][1], 71 | bountyMetaData, 72 | ], 73 | ]); 74 | } else { 75 | setAllNotifications((arr) => [ 76 | ...arr, 77 | [ 78 | event.created_at, 79 | event.pubkey, 80 | "reward", 81 | event.content, 82 | bountyMetaData, 83 | ], 84 | ]); 85 | } 86 | }, 87 | undefined, 88 | undefined, 89 | { unsubscribeOnEose: true } 90 | ); 91 | 92 | let subFilterApplications = [ 93 | { 94 | kinds: [1], 95 | "#d": [bountyDtag], 96 | "#t": ["bounty-application"], 97 | }, 98 | ]; 99 | // eslint-disable-next-line 100 | relayPool.subscribe( 101 | subFilterApplications, 102 | defaultRelays, 103 | (event, isAfterEose, relayUrl) => { 104 | if (event.kind === 1) { 105 | setAllNotifications((arr) => [ 106 | ...arr, 107 | [event.created_at, event.pubkey, "application", bountyMetaData], 108 | ]); 109 | } 110 | }, 111 | undefined, 112 | undefined, 113 | { unsubscribeOnEose: true } 114 | ), 115 | undefined, 116 | undefined, 117 | { unsubscribeOnEose: true }; 118 | } 119 | ); 120 | 121 | setTimeout(() => { 122 | setDataLoaded(true); 123 | }, 2000); 124 | }, []); 125 | 126 | useEffect(() => { 127 | allNotifications.sort((a, b) => b[0] - a[0]); 128 | setSortedElements(allNotifications); 129 | }, [allNotifications]); 130 | 131 | return ( 132 |
133 |
134 | 135 |
136 |
137 | 138 |
139 |
140 | {sortedElements.length >= 1 ? ( 141 |

142 | Your notifications: 143 |

144 | ) : null} 145 | {dataLoaded 146 | ? sortedElements.map((item) => { 147 | return ( 148 |
149 | 150 |
151 | ); 152 | }) 153 | : null} 154 | {sortedElements.length === 0 ? ( 155 |

156 | You don't have notifications 157 |

158 | ) : null} 159 |
160 |
161 | ); 162 | } 163 | 164 | export default Notifications; 165 | -------------------------------------------------------------------------------- /src/components/notifications/notificationBox.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | convertTimestamp, 3 | getNpub, 4 | isDarkTheme, 5 | formatReward, 6 | } from "../../utils"; 7 | import { useState, useEffect } from "react"; 8 | import { useNavigate } from "react-router-dom"; 9 | import { nip19 } from "nostr-tools"; 10 | import { RelayPool } from "nostr-relaypool"; 11 | import { defaultRelays } from "../../const"; 12 | 13 | import defaultAvatar from "../../assets/nostr-icon-user.avif"; 14 | import externalLinkDm from "../../assets/external-link-icon-dm.svg"; 15 | import externalLinkLg from "../../assets/external-link-icon-lg.svg"; 16 | 17 | type allBountiesNotifications = { 18 | ev: 19 | | [number, string, string, string, string[]] 20 | | [number, string, string, string[]]; 21 | }; 22 | 23 | function NotificationPledgedSats({ ev }: allBountiesNotifications) { 24 | let [name, setName] = useState(""); 25 | let [profilePic, setProfilePic] = useState(""); 26 | let createdAt = convertTimestamp(ev[0]); 27 | let npub = getNpub(ev[1]); 28 | let navigate = useNavigate(); 29 | let bountyLinkPath: string; 30 | 31 | if (ev[2] === "reward") { 32 | let nadrrElements = ev[4]![0].split(":"); 33 | bountyLinkPath = nip19.naddrEncode({ 34 | identifier: nadrrElements[2], 35 | pubkey: nadrrElements[1], 36 | kind: 30023, 37 | }); 38 | } 39 | if (ev[2] === "application") { 40 | let nadrrElements = ev[3][0].split(":"); 41 | bountyLinkPath = nip19.naddrEncode({ 42 | identifier: nadrrElements[2], 43 | pubkey: nadrrElements[1], 44 | kind: 30023, 45 | }); 46 | } 47 | 48 | useEffect(() => { 49 | let relayPool = new RelayPool(defaultRelays, { useEventCache: true }); 50 | 51 | relayPool.onerror((err, relayUrl) => { 52 | console.log("RelayPool error", err, " from relay ", relayUrl); 53 | }); 54 | relayPool.onnotice((relayUrl, notice) => { 55 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 56 | }); 57 | 58 | let bountyPosterMetadataFilter = [{ kinds: [0], authors: [ev[1]] }]; 59 | relayPool.subscribe( 60 | bountyPosterMetadataFilter, 61 | defaultRelays, 62 | (event, isAfterEose, relayURL) => { 63 | let metadata = JSON.parse(event.content); 64 | setName(metadata.username); 65 | setProfilePic(metadata.picture); 66 | }, 67 | undefined, 68 | undefined, 69 | { unsubscribeOnEose: true } 70 | ); 71 | }, []); 72 | 73 | return ( 74 |
75 | {ev[2] === "reward" ? ( 76 |
77 |
78 |
79 | avatar image 84 |
85 |

86 | {/* 87 | // @ts-ignore */} 88 | {name === "" ? npub : name} pledged {formatReward(ev[3])} sats{" "} 89 | {createdAt} to: 90 |

91 | 92 |

93 | {ev[4]![1]} 94 |

95 |
96 |
97 |
98 |
99 |

100 | Pledged 101 |

102 | navigate(`/b/${bountyLinkPath}`)} 106 | src={isDarkTheme() ? externalLinkDm : externalLinkLg} 107 | alt="external link icon" 108 | > 109 |
110 |
111 | ) : null} 112 | {ev[2] === "application" ? ( 113 |
114 |
115 |
116 | avatar image 121 |
122 |

123 | {name} applied {createdAt} to: 124 |

125 |

126 | {ev[3][1]} 127 |

128 |
129 |
130 |
131 |
132 |

133 | Applied 134 |

135 | navigate(`/b/${bountyLinkPath}`)} 140 | alt="external link icon" 141 | > 142 |
143 |
144 | ) : null} 145 |
146 | ); 147 | } 148 | 149 | export default NotificationPledgedSats; 150 | -------------------------------------------------------------------------------- /src/components/menus/sidebarMenu/sidebarMenu.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import { getPubKey, isDarkTheme } from "../../../utils"; 3 | import { nip19 } from "nostr-tools"; 4 | import { useState } from "react"; 5 | 6 | import IsNotLogged from "../../errors/isNotLogged"; 7 | import homeIcon from "../../../assets/home-icon-dm.svg"; 8 | import createIconDm from "../../../assets/create-icon-dm.svg"; 9 | import profileIcon from "../../../assets/profile-icon-dm.svg"; 10 | import homeIconLg from "../../../assets/home-icon-lg.svg"; 11 | import createIconLg from "../../../assets/create-icon-lg.svg"; 12 | import profileIconLg from "../../../assets/profile-icon-lg.svg"; 13 | import relayIconLg from "../../../assets/server-icon-lg.svg"; 14 | import relayIconDm from "../../../assets/server-icon-dm.svg"; 15 | import notificationIconDm from "../../../assets/notification-icon-dm.svg"; 16 | import notificationIconLg from "../../../assets/notification-icon-lg.svg"; 17 | import logo from "../../../assets/Asset14.png"; 18 | 19 | function SideBarMenu() { 20 | const navigate = useNavigate(); 21 | const isLogged = sessionStorage.getItem("isLogged") === "true"; 22 | const [displayLogError, setDisplayLogError] = useState(false); 23 | 24 | function goToProfile() { 25 | getPubKey() 26 | .then((data) => { 27 | let npub = nip19.npubEncode(data); 28 | navigate(`/profile/${npub}`); 29 | }) 30 | .catch((error) => console.log(error)); 31 | } 32 | 33 | return ( 34 |
35 | {displayLogError ? ( 36 | 37 | ) : null} 38 |
39 |
40 |
navigate("/")} 42 | className="flex cursor-pointer px-3 py-2 hover:bg-gray-2 dark:rounded-lg dark:hover:bg-input-bg-dm" 43 | > 44 | delete icon 49 |

50 | Home 51 |

52 |
53 |
navigate("/create")} 55 | className="flex cursor-pointer px-3 py-2 hover:bg-gray-2 dark:rounded-lg dark:hover:bg-input-bg-dm" 56 | > 57 | delete icon 62 |

63 | Create 64 |

65 |
66 |
70 | delete icon 75 |

76 | My bounties 77 |

78 |
79 |
{ 81 | isLogged ? navigate("/notifications") : setDisplayLogError(true); 82 | }} 83 | className="flex cursor-pointer px-3 py-2 hover:bg-gray-2 dark:rounded-lg dark:hover:bg-input-bg-dm" 84 | > 85 | delete icon 90 |

91 | Notifications 92 |

93 |
94 |
navigate("/relays")} 96 | className="flex cursor-pointer px-3 py-2 hover:bg-gray-2 dark:rounded-lg dark:hover:bg-input-bg-dm" 97 | > 98 | delete icon 103 |

104 | Relays Information 105 |

106 |
107 |
108 |
109 |
110 |
111 |
112 | {isLogged ? ( 113 | 122 | ) : ( 123 | 136 | )} 137 |
138 | 146 |
147 |
148 |
149 |
150 | ); 151 | } 152 | 153 | export default SideBarMenu; 154 | -------------------------------------------------------------------------------- /src/components/bounty/bountyApplicantsBox/bountyApplicantsBox.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { nip19 } from "nostr-tools"; 3 | import { useState, useEffect } from "react"; 4 | import { defaultRelays } from "../../../const"; 5 | import { 6 | getNpub, 7 | deleteEvent, 8 | convertTimestamp, 9 | isDarkTheme, 10 | } from "../../../utils"; 11 | import { RelayPool } from "nostr-relaypool"; 12 | 13 | import avatarImage from "../../../assets/nostr-icon-user.avif"; 14 | import deleteIcon from "../../../assets/delete-icon.svg"; 15 | import githubIconDm from "../../../assets/github-icon-dm.svg"; 16 | import githubIconLg from "../../../assets/github-icon-lg.svg"; 17 | import websiteIconDm from "../../../assets/website-icon-dm.svg"; 18 | import websiteIconLg from "../../../assets/website-icon-lg.svg"; 19 | 20 | function CommentBox({ 21 | pubkey, 22 | content, 23 | id, 24 | links, 25 | createdAt, 26 | posterPubkey, 27 | }: any) { 28 | let [name, setName] = useState(""); 29 | let [profilePic, setProfilePic] = useState(""); 30 | let npub = nip19.npubEncode(pubkey); 31 | let isLogged = sessionStorage.getItem("pubkey"); 32 | let npubShortened = getNpub(pubkey); 33 | let datePosted = convertTimestamp(createdAt); 34 | 35 | useEffect(() => { 36 | let relayPool = new RelayPool(defaultRelays); 37 | 38 | relayPool.onerror((err, relayUrl) => { 39 | console.log("RelayPool error", err, " from relay ", relayUrl); 40 | }); 41 | relayPool.onnotice((relayUrl, notice) => { 42 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 43 | }); 44 | 45 | let userMetadataFilter = [{ kinds: [0], authors: [pubkey] }]; 46 | relayPool.subscribe( 47 | userMetadataFilter, 48 | defaultRelays, 49 | (event, isAfterEose, relayURL) => { 50 | let metadata = JSON.parse(event.content); 51 | 52 | setName(metadata.name); 53 | setProfilePic(metadata.picture); 54 | }, 55 | undefined, 56 | undefined, 57 | { unsubscribeOnEose: true } 58 | ); 59 | }, []); 60 | return ( 61 |
62 |
63 |
64 | 65 |
66 |
67 |
68 |
69 |
70 | {profilePic === "" ? ( 71 |
72 | avatar image 77 |
78 | ) : ( 79 |
80 | avatar image 85 |
86 | )} 87 |
88 | {name === "" ? ( 89 | 93 | {npubShortened} applied to this bounty {datePosted}: 94 | 95 | ) : ( 96 | 100 | {name} applied to this bounty {datePosted}: 101 | 102 | )} 103 |
104 |

105 | {content} 106 |

107 |
108 | {links.github !== "" ? ( 109 |
110 | 111 | github icon 118 | 119 |
120 | ) : null} 121 | {links.personalWebsite !== "" ? ( 122 |
123 | 127 | website icon 134 | 135 |
136 | ) : null} 137 |
138 | 139 | {isLogged === posterPubkey ? ( 140 | 145 | send a message 146 | 147 | ) : null} 148 |
149 |
150 |
151 |
152 | {isLogged === pubkey ? ( 153 | deleteEvent(id)} 156 | src={deleteIcon} 157 | alt="delete" 158 | /> 159 | ) : null} 160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 | ); 168 | } 169 | 170 | export default CommentBox; 171 | -------------------------------------------------------------------------------- /src/components/menus/mobileMenu/mobileMenu.tsx: -------------------------------------------------------------------------------- 1 | import { isDarkTheme } from "../../../utils"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { useState } from "react"; 4 | import { defaultRelays, defaultRelaysToPublish } from "../../../const"; 5 | 6 | import homeIcon from "../../../assets/home-icon-dm.svg"; 7 | import createIconDm from "../../../assets/create-icon-dm.svg"; 8 | import profileIcon from "../../../assets/profile-icon-dm.svg"; 9 | import homeIconLg from "../../../assets/home-icon-lg.svg"; 10 | import createIconLg from "../../../assets/create-icon-lg.svg"; 11 | import profileIconLg from "../../../assets/profile-icon-lg.svg"; 12 | import closeIconLg from "../../../assets/close-icon-lg.svg"; 13 | import closeIconDm from "../../../assets/close-icon-dm.svg"; 14 | import relayIconLg from "../../../assets/server-icon-lg.svg"; 15 | import relayIconDm from "../../../assets/server-icon-dm.svg"; 16 | import active from "../../../assets/status-active.svg"; 17 | 18 | function MobileMenu() { 19 | let navigate = useNavigate(); 20 | let [isActive, setIsActive] = useState(false); 21 | const [relays, setRelays] = useState(defaultRelays); 22 | const [relay, setRelay] = useState(""); 23 | 24 | function deleteRelay(relayName: string) { 25 | if (relays.length === 1) { 26 | console.log("you cant delete this relay"); 27 | } else { 28 | let newRelays = relays.filter((item: string) => { 29 | return item !== relayName; 30 | }); 31 | 32 | setRelays(newRelays); 33 | let newRelaysLocalStg = JSON.stringify(newRelays); 34 | localStorage.setItem("relays", newRelaysLocalStg); 35 | } 36 | } 37 | 38 | function addRelay(relayName: string | undefined) { 39 | if ( 40 | relayName?.includes("ws://localhost") || 41 | relayName?.includes("wss://") 42 | ) { 43 | setRelays([...relays, relayName]); 44 | let newRelay = JSON.stringify([...relays, relayName]); 45 | localStorage.setItem("relays", newRelay); 46 | } else { 47 | console.log("No valid relays!"); 48 | } 49 | } 50 | 51 | const handleNewRelay = (event: { target: { value: string } }) => { 52 | setRelay(event.target.value); 53 | }; 54 | return ( 55 |
56 | 170 |
171 | ); 172 | } 173 | 174 | export default MobileMenu; 175 | -------------------------------------------------------------------------------- /src/components/bounty/bountyEditor/bountyEditor.tsx: -------------------------------------------------------------------------------- 1 | import { editBounty } from "../../../utils"; 2 | import { useState } from "react"; 3 | import { useNavigate } from "react-router-dom"; 4 | import { nip19 } from "nostr-tools"; 5 | import { ReactMarkdown } from "react-markdown/lib/react-markdown"; 6 | 7 | type props = { 8 | oldEvent: { 9 | id: string; 10 | pubkey: string; 11 | created_at: number; 12 | kind: number; 13 | tags: string[][]; 14 | content: string; 15 | sig: string; 16 | }; 17 | }; 18 | 19 | function BountyEditor({ oldEvent }: props) { 20 | let [newContent, setNewContent] = useState(oldEvent.content); 21 | let [newTitle, setNewTitle] = useState(oldEvent.tags[1][1]); 22 | let [displayPreview, setDisplayPreview] = useState(false); 23 | oldEvent.tags[1].splice(1, 1, newTitle); 24 | 25 | let navigate = useNavigate(); 26 | let newEvent = { 27 | id: null, 28 | pubkey: oldEvent.pubkey, 29 | created_at: Math.floor(Date.now() / 1000), 30 | kind: 30023, 31 | tags: oldEvent.tags, 32 | content: newContent, 33 | sig: null, 34 | }; 35 | 36 | return ( 37 |
38 |
39 | 42 | setNewTitle(e.target.value)} 45 | className="peer min-h-[auto] bg-gray-50 border-y border-x border-dark-text text-dark-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-sidebar-bg dark:text-gray-1 border-0" 46 | placeholder="i.e. Bounty manager" 47 | value={oldEvent.tags[1][1]} 48 | required 49 | /> 50 |
51 |
52 | 55 |
56 | 64 | 70 | 76 | 82 | 88 | 94 | 100 | 106 | 112 |
113 | 123 |
124 | {displayPreview ? ( 125 |
126 | 129 | 130 | {newContent} 131 | 132 |
133 | ) : null} 134 |
135 | 136 | 152 |
153 |
154 | ); 155 | } 156 | 157 | export default BountyEditor; 158 | -------------------------------------------------------------------------------- /src/components/payment/LNInvoice.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { 3 | getLNService, 4 | getZapEvent, 5 | getLNInvoice, 6 | isDarkTheme, 7 | shortenedLNurl, 8 | sendReply, 9 | } from "../../utils"; 10 | import QRCode from "react-qr-code"; 11 | import { nip19 } from "nostr-tools"; 12 | 13 | import closeIconLg from "../../assets/close-icon-lg.svg"; 14 | import closeIconDm from "../../assets/close-icon-dm.svg"; 15 | import copyIconLg from "../../assets/copy-icon-lg.svg"; 16 | import copyIconDm from "../../assets/copy-icon-dm.svg"; 17 | import avatarImage from "../../assets/nostr-icon-user.avif"; 18 | 19 | function LNInvoice({ 20 | amount, 21 | bountyHunterMetadata, 22 | posterPubkey, 23 | naddr, 24 | closeModal, 25 | eventId, 26 | updateValues, 27 | dataLoaded, 28 | }: any) { 29 | let miliSatsAmount = amount * 1000; 30 | let miliSatsAmountStr = miliSatsAmount.toString(); 31 | let currentStatus = "in progress"; 32 | let bountyHunterNpub = nip19.npubEncode(bountyHunterMetadata.pubkey); 33 | // @ts-ignore 34 | let dTag = nip19.decode(naddr).data.identifier; 35 | let id = eventId; 36 | 37 | let [comment, setComment] = useState(""); 38 | let [LNservice, setLNservice] = useState(); 39 | let [displayLNInvoice, setDisplayLNInvoice] = useState(false); 40 | let [LNurl, setLNurl] = useState(); 41 | let [wasLNurlCopied, setWasLNurlCopied] = useState(false); 42 | 43 | function copyToClipboard() { 44 | navigator.clipboard.writeText(LNurl!); 45 | setWasLNurlCopied(true); 46 | setTimeout(() => { 47 | setWasLNurlCopied(false); 48 | }, 2500); 49 | } 50 | 51 | async function openLNExtension(LNInvoice: string) { 52 | // @ts-ignore 53 | let hasAccessToExtension = await window.webln.enable(); 54 | if (hasAccessToExtension) { 55 | // @ts-ignore 56 | await window.webln.sendPayment(LNInvoice); 57 | } 58 | } 59 | 60 | function payLNInvoice() { 61 | getZapEvent( 62 | comment, 63 | bountyHunterMetadata.pubkey, 64 | posterPubkey, 65 | miliSatsAmountStr, 66 | naddr 67 | ).then((event) => { 68 | let zapEvent = JSON.stringify(event); 69 | getLNInvoice(zapEvent, comment, LNservice, miliSatsAmountStr) 70 | .then((response) => response?.json()) 71 | .then((data) => { 72 | let LNInvoice = data.pr; 73 | setLNurl(LNInvoice); 74 | openLNExtension(LNInvoice); 75 | setDisplayLNInvoice(true); 76 | }); 77 | }); 78 | } 79 | 80 | function updateStatus() { 81 | sendReply( 82 | currentStatus, 83 | bountyHunterNpub, 84 | dTag, 85 | posterPubkey, 86 | id, 87 | naddr 88 | ).then(() => { 89 | updateValues(false); 90 | dataLoaded(false); 91 | }); 92 | } 93 | 94 | useEffect(() => { 95 | let bountyHunterLNAddress = bountyHunterMetadata.lnAddress; 96 | getLNService(bountyHunterLNAddress)?.then((data) => { 97 | setLNservice(data); 98 | }); 99 | }, []); 100 | 101 | return ( 102 |
103 |
104 | {!displayLNInvoice ? ( 105 |
106 |
107 | 110 | closeModal()} 113 | src={isDarkTheme() ? closeIconLg : closeIconDm} 114 | alt="close icon" 115 | > 116 |
117 |
118 | avatar image 127 |
128 |

129 | {bountyHunterMetadata.name} is going to get paid: 130 |

131 | 132 |

133 | {amount} sats 134 |

135 |
136 |
137 |
138 | setComment(e.target.value)} 141 | className="peer min-h-[auto] bg-gray-50 border-y border-x border-dark-text text-dark-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-sidebar-bg dark:text-gray-1 border-0" 142 | placeholder="Add a comment..." 143 | required 144 | /> 145 |
146 |
147 | 155 |
156 |
157 | ) : null} 158 | {displayLNInvoice ? ( 159 |
160 |
161 | closeModal()} 164 | src={isDarkTheme() ? closeIconLg : closeIconDm} 165 | alt="close icon" 166 | > 167 |
168 |
169 |

170 | LN invoice: 171 |

172 |
173 | 174 |
175 |
176 | 177 |
178 |
179 |
180 |

181 | {shortenedLNurl(LNurl!)} 182 |

183 | 184 | {wasLNurlCopied ? ( 185 |

186 | copied! 187 |

188 | ) : ( 189 | copyToClipboard()} 193 | src={isDarkTheme() ? copyIconDm : copyIconLg} 194 | alt="copy icon" 195 | > 196 | )} 197 |
198 |
199 | 207 |
208 |
209 | ) : null} 210 |
211 |
212 | ); 213 | } 214 | 215 | export default LNInvoice; 216 | -------------------------------------------------------------------------------- /src/pages/tags/designBounties.tsx: -------------------------------------------------------------------------------- 1 | import SideBarMenu from "../../components/menus/sidebarMenu/sidebarMenu"; 2 | import BountiesNotFound from "../../components/errors/bountiesNotFound"; 3 | import MobileMenu from "../../components/menus/mobileMenu/mobileMenu"; 4 | import CategoryList from "../../components/categoriesList/categoryList"; 5 | import BountyCard from "../../components/bounty/bountyCardShortInfo/bountyCardShortInfo"; 6 | 7 | import { useState, useEffect } from "react"; 8 | import { convertTimestamp, formatReward } from "../../utils"; 9 | import { RelayPool } from "nostr-relaypool"; 10 | import { defaultRelaysToPublish, defaultRelays } from "../../const"; 11 | 12 | type event = { 13 | Dtag: string; 14 | createdAt: string; 15 | name: string; 16 | profilePic: string; 17 | pubkey: string; 18 | reward: string; 19 | tags: string[]; 20 | title: string; 21 | timestamp: number; 22 | }; 23 | 24 | function App() { 25 | let currentTimestamp = Math.floor(Date.now() / 1000); 26 | let [eventData, setEventData] = useState([]); 27 | let [bountyNotFound, setBountyNotFound] = useState(false); 28 | let [dataLoaded, setDataLoaded] = useState(false); 29 | let [loadMore, setLoadMore] = useState(false); 30 | let [loadingMessage, setLoadingMessage] = useState(false); 31 | let [queryUntil, setQueryUntil] = useState(currentTimestamp); 32 | let [currentBountyCount, setCurrentBountyCount] = useState(); 33 | let [correctBountyCount, setCorrectBountyCount] = useState(10); 34 | let [currentStatus, setCurrentStatus] = useState({}); 35 | 36 | function loadMoreBounties() { 37 | let lastElement = eventData.length - 1; 38 | setQueryUntil(eventData[lastElement].timestamp); 39 | setLoadMore(!loadMore); 40 | setLoadingMessage(true); 41 | setCorrectBountyCount(correctBountyCount + 20); 42 | } 43 | 44 | useEffect(() => { 45 | let relays = defaultRelaysToPublish; 46 | let statuses: any = {}; 47 | let subFilter = [ 48 | { 49 | kinds: [30023], 50 | "#t": ["design-bounty"], 51 | until: queryUntil, 52 | limit: 20, 53 | }, 54 | ]; 55 | 56 | let subFilterStatus = [ 57 | { 58 | // @ts-ignore 59 | "#t": ["bounty-status"], 60 | kinds: [1], 61 | until: queryUntil, 62 | }, 63 | ]; 64 | 65 | let checkBountyExist = []; 66 | let eventLength = []; 67 | 68 | let relayPool = new RelayPool(relays, { useEventCache: true }); 69 | 70 | relayPool.onerror((err, relayUrl) => { 71 | console.log("RelayPool error", err, " from relay ", relayUrl); 72 | }); 73 | relayPool.onnotice((relayUrl, notice) => { 74 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 75 | }); 76 | 77 | relayPool.subscribe( 78 | subFilter, 79 | relays, 80 | (event, isAfterEose, relayURL) => { 81 | let parseDate = parseInt(event.tags[3][1]); 82 | let date = convertTimestamp(parseDate); 83 | let tags_arr: string[] = []; 84 | // @ts-ignore 85 | let ev: event = {}; 86 | 87 | let bountyTitle = event.tags[1][1]; 88 | let bountyReward = formatReward(event.tags[2][1]); 89 | let bountyDatePosted = date; 90 | 91 | event.tags.map((item) => { 92 | if (item[0] === "t") { 93 | switch (item[1]) { 94 | case "design-bounty": 95 | tags_arr.push("design"); 96 | break; 97 | case "development-bounty": 98 | tags_arr.push("development"); 99 | break; 100 | case "debugging-bounty": 101 | tags_arr.push("debugging"); 102 | break; 103 | case "writing-bounty": 104 | tags_arr.push("writing"); 105 | break; 106 | case "cybersecurity-bounty": 107 | tags_arr.push("cybersecurity"); 108 | break; 109 | case "marketing-bounty": 110 | tags_arr.push("marketing"); 111 | break; 112 | } 113 | } 114 | 115 | if (item[0] === "d") { 116 | ev.Dtag = item[1]; 117 | } 118 | 119 | ev.tags = tags_arr; 120 | }); 121 | 122 | let userMetadataFilter = [{ kinds: [0], authors: [event.pubkey] }]; 123 | relayPool.subscribe( 124 | userMetadataFilter, 125 | defaultRelays, 126 | (event, isAfterEose, relayURL) => { 127 | let metadata = JSON.parse(event.content); 128 | 129 | ev.name = metadata.username; 130 | ev.profilePic = metadata.picture; 131 | }, 132 | undefined, 133 | undefined, 134 | { unsubscribeOnEose: true } 135 | ); 136 | 137 | ev.title = bountyTitle; 138 | ev.reward = bountyReward; 139 | ev.createdAt = bountyDatePosted; 140 | ev.pubkey = event.pubkey; 141 | ev.timestamp = event.created_at; 142 | 143 | setEventData((arr) => [...arr, ev]); 144 | eventLength.push(ev); 145 | checkBountyExist.push(event.id); 146 | }, 147 | undefined, 148 | undefined, 149 | { unsubscribeOnEose: true } 150 | ); 151 | 152 | relayPool.subscribe( 153 | subFilterStatus, 154 | defaultRelays, 155 | (event, isAfterEose, relayUrl) => { 156 | let dTag = `${event.tags[0][1]}`; 157 | let hasdTag = statuses.hasOwnProperty(dTag); 158 | 159 | if (!hasdTag) { 160 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 161 | } else { 162 | if (event.created_at > statuses[`${dTag}`][1]) 163 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 164 | } 165 | setCurrentStatus(statuses); 166 | }, 167 | undefined, 168 | undefined, 169 | { unsubscribeOnEose: true } 170 | ); 171 | 172 | setTimeout(() => { 173 | relayPool.close().then(() => { 174 | console.log("connection closed"); 175 | }); 176 | if (checkBountyExist.length === 0) { 177 | setBountyNotFound(true); 178 | clearInterval(closeMyInterval); 179 | } 180 | }, 40000); 181 | 182 | let closeMyInterval = setInterval(() => { 183 | if ( 184 | eventLength.length === checkBountyExist.length && 185 | eventLength.length >= 1 186 | ) { 187 | setDataLoaded(true); 188 | setLoadingMessage(false); 189 | clearInterval(closeMyInterval); 190 | } 191 | }, 1800); 192 | }, [loadMore]); 193 | 194 | useEffect(() => { 195 | setCurrentBountyCount(eventData.length); 196 | }, [eventData]); 197 | 198 | return ( 199 |
200 |
201 | 202 |
203 |
204 | 205 |
206 |
207 | 208 |
209 | {dataLoaded ? ( 210 |
211 | {eventData.map((item, index) => { 212 | return ( 213 |
214 | 215 |
216 | ); 217 | })} 218 |
219 | ) : ( 220 |
221 | Loading... 222 |
223 | )} 224 |
225 | {currentBountyCount! >= correctBountyCount ? ( 226 | 234 | ) : null} 235 | {currentBountyCount! < correctBountyCount ? ( 236 |

237 | We didn't find more bounties 238 |

239 | ) : null} 240 |
241 |
242 | {bountyNotFound ? : null} 243 |
244 |
245 | ); 246 | } 247 | 248 | export default App; 249 | -------------------------------------------------------------------------------- /src/pages/tags/writingBounties.tsx: -------------------------------------------------------------------------------- 1 | import SideBarMenu from "../../components/menus/sidebarMenu/sidebarMenu"; 2 | import BountiesNotFound from "../../components/errors/bountiesNotFound"; 3 | import MobileMenu from "../../components/menus/mobileMenu/mobileMenu"; 4 | import CategoryList from "../../components/categoriesList/categoryList"; 5 | import BountyCard from "../../components/bounty/bountyCardShortInfo/bountyCardShortInfo"; 6 | 7 | import { useState, useEffect } from "react"; 8 | import { convertTimestamp, formatReward } from "../../utils"; 9 | import { RelayPool } from "nostr-relaypool"; 10 | import { defaultRelaysToPublish, defaultRelays } from "../../const"; 11 | 12 | type event = { 13 | Dtag: string; 14 | createdAt: string; 15 | name: string; 16 | profilePic: string; 17 | pubkey: string; 18 | reward: string; 19 | tags: string[]; 20 | title: string; 21 | timestamp: number; 22 | }; 23 | 24 | function App() { 25 | let currentTimestamp = Math.floor(Date.now() / 1000); 26 | let [eventData, setEventData] = useState([]); 27 | let [bountyNotFound, setBountyNotFound] = useState(false); 28 | let [dataLoaded, setDataLoaded] = useState(false); 29 | let [loadMore, setLoadMore] = useState(false); 30 | let [loadingMessage, setLoadingMessage] = useState(false); 31 | let [queryUntil, setQueryUntil] = useState(currentTimestamp); 32 | let [currentBountyCount, setCurrentBountyCount] = useState(); 33 | let [correctBountyCount, setCorrectBountyCount] = useState(10); 34 | let [currentStatus, setCurrentStatus] = useState({}); 35 | 36 | function loadMoreBounties() { 37 | let lastElement = eventData.length - 1; 38 | setQueryUntil(eventData[lastElement].timestamp); 39 | setLoadMore(!loadMore); 40 | setLoadingMessage(true); 41 | setCorrectBountyCount(correctBountyCount + 20); 42 | } 43 | 44 | useEffect(() => { 45 | let relays = defaultRelaysToPublish; 46 | let statuses: any = {}; 47 | let subFilter = [ 48 | { 49 | kinds: [30023], 50 | "#t": ["writing-bounty"], 51 | until: queryUntil, 52 | limit: 20, 53 | }, 54 | ]; 55 | 56 | let subFilterStatus = [ 57 | { 58 | // @ts-ignore 59 | "#t": ["bounty-status"], 60 | kinds: [1], 61 | until: queryUntil, 62 | }, 63 | ]; 64 | 65 | let checkBountyExist = []; 66 | let eventLength = []; 67 | 68 | let relayPool = new RelayPool(relays, { useEventCache: true }); 69 | 70 | relayPool.onerror((err, relayUrl) => { 71 | console.log("RelayPool error", err, " from relay ", relayUrl); 72 | }); 73 | relayPool.onnotice((relayUrl, notice) => { 74 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 75 | }); 76 | 77 | relayPool.subscribe( 78 | subFilter, 79 | relays, 80 | (event, isAfterEose, relayURL) => { 81 | let parseDate = parseInt(event.tags[3][1]); 82 | let date = convertTimestamp(parseDate); 83 | let tags_arr: string[] = []; 84 | // @ts-ignore 85 | let ev: event = {}; 86 | 87 | let bountyTitle = event.tags[1][1]; 88 | let bountyReward = formatReward(event.tags[2][1]); 89 | let bountyDatePosted = date; 90 | 91 | event.tags.map((item) => { 92 | if (item[0] === "t") { 93 | switch (item[1]) { 94 | case "design-bounty": 95 | tags_arr.push("design"); 96 | break; 97 | case "development-bounty": 98 | tags_arr.push("development"); 99 | break; 100 | case "debugging-bounty": 101 | tags_arr.push("debugging"); 102 | break; 103 | case "writing-bounty": 104 | tags_arr.push("writing"); 105 | break; 106 | case "cybersecurity-bounty": 107 | tags_arr.push("cybersecurity"); 108 | break; 109 | case "marketing-bounty": 110 | tags_arr.push("marketing"); 111 | break; 112 | } 113 | } 114 | 115 | if (item[0] === "d") { 116 | ev.Dtag = item[1]; 117 | } 118 | 119 | ev.tags = tags_arr; 120 | }); 121 | 122 | let userMetadataFilter = [{ kinds: [0], authors: [event.pubkey] }]; 123 | relayPool.subscribe( 124 | userMetadataFilter, 125 | defaultRelays, 126 | (event, isAfterEose, relayURL) => { 127 | let metadata = JSON.parse(event.content); 128 | 129 | ev.name = metadata.username; 130 | ev.profilePic = metadata.picture; 131 | }, 132 | undefined, 133 | undefined, 134 | { unsubscribeOnEose: true } 135 | ); 136 | 137 | ev.title = bountyTitle; 138 | ev.reward = bountyReward; 139 | ev.createdAt = bountyDatePosted; 140 | ev.pubkey = event.pubkey; 141 | ev.timestamp = event.created_at; 142 | 143 | setEventData((arr) => [...arr, ev]); 144 | eventLength.push(ev); 145 | checkBountyExist.push(event.id); 146 | }, 147 | undefined, 148 | undefined, 149 | { unsubscribeOnEose: true } 150 | ); 151 | 152 | relayPool.subscribe( 153 | subFilterStatus, 154 | defaultRelays, 155 | (event, isAfterEose, relayUrl) => { 156 | let dTag = `${event.tags[0][1]}`; 157 | let hasdTag = statuses.hasOwnProperty(dTag); 158 | 159 | if (!hasdTag) { 160 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 161 | } else { 162 | if (event.created_at > statuses[`${dTag}`][1]) 163 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 164 | } 165 | setCurrentStatus(statuses); 166 | }, 167 | undefined, 168 | undefined, 169 | { unsubscribeOnEose: true } 170 | ); 171 | 172 | setTimeout(() => { 173 | relayPool.close().then(() => { 174 | console.log("connection closed"); 175 | }); 176 | if (checkBountyExist.length === 0) { 177 | setBountyNotFound(true); 178 | clearInterval(closeMyInterval); 179 | } 180 | }, 40000); 181 | 182 | let closeMyInterval = setInterval(() => { 183 | if ( 184 | eventLength.length === checkBountyExist.length && 185 | eventLength.length >= 1 186 | ) { 187 | setDataLoaded(true); 188 | setLoadingMessage(false); 189 | clearInterval(closeMyInterval); 190 | } 191 | }, 1800); 192 | }, [loadMore]); 193 | 194 | useEffect(() => { 195 | setCurrentBountyCount(eventData.length); 196 | }, [eventData]); 197 | 198 | return ( 199 |
200 |
201 | 202 |
203 |
204 | 205 |
206 |
207 | 208 |
209 | {dataLoaded ? ( 210 |
211 | {eventData.map((item, index) => { 212 | return ( 213 |
214 | 215 |
216 | ); 217 | })} 218 |
219 | ) : ( 220 |
221 | Loading... 222 |
223 | )} 224 |
225 | {currentBountyCount! >= correctBountyCount ? ( 226 | 234 | ) : null} 235 | {currentBountyCount! < correctBountyCount ? ( 236 |

237 | We didn't find more bounties 238 |

239 | ) : null} 240 |
241 |
242 | {bountyNotFound ? : null} 243 |
244 |
245 | ); 246 | } 247 | 248 | export default App; 249 | -------------------------------------------------------------------------------- /src/pages/tags/debugginBounties.tsx: -------------------------------------------------------------------------------- 1 | import SideBarMenu from "../../components/menus/sidebarMenu/sidebarMenu"; 2 | import BountiesNotFound from "../../components/errors/bountiesNotFound"; 3 | import MobileMenu from "../../components/menus/mobileMenu/mobileMenu"; 4 | import CategoryList from "../../components/categoriesList/categoryList"; 5 | import BountyCard from "../../components/bounty/bountyCardShortInfo/bountyCardShortInfo"; 6 | 7 | import { useState, useEffect } from "react"; 8 | import { convertTimestamp, formatReward } from "../../utils"; 9 | import { RelayPool } from "nostr-relaypool"; 10 | import { defaultRelaysToPublish, defaultRelays } from "../../const"; 11 | 12 | type event = { 13 | Dtag: string; 14 | createdAt: string; 15 | name: string; 16 | profilePic: string; 17 | pubkey: string; 18 | reward: string; 19 | tags: string[]; 20 | title: string; 21 | timestamp: number; 22 | }; 23 | 24 | function App() { 25 | let currentTimestamp = Math.floor(Date.now() / 1000); 26 | let [eventData, setEventData] = useState([]); 27 | let [bountyNotFound, setBountyNotFound] = useState(false); 28 | let [dataLoaded, setDataLoaded] = useState(false); 29 | let [loadMore, setLoadMore] = useState(false); 30 | let [loadingMessage, setLoadingMessage] = useState(false); 31 | let [queryUntil, setQueryUntil] = useState(currentTimestamp); 32 | let [currentBountyCount, setCurrentBountyCount] = useState(); 33 | let [correctBountyCount, setCorrectBountyCount] = useState(10); 34 | let [currentStatus, setCurrentStatus] = useState({}); 35 | 36 | function loadMoreBounties() { 37 | let lastElement = eventData.length - 1; 38 | setQueryUntil(eventData[lastElement].timestamp); 39 | setLoadMore(!loadMore); 40 | setLoadingMessage(true); 41 | setCorrectBountyCount(correctBountyCount + 20); 42 | } 43 | 44 | useEffect(() => { 45 | let relays = defaultRelaysToPublish; 46 | let statuses: any = {}; 47 | let subFilter = [ 48 | { 49 | kinds: [30023], 50 | "#t": ["debugging-bounty"], 51 | until: queryUntil, 52 | limit: 20, 53 | }, 54 | ]; 55 | 56 | let subFilterStatus = [ 57 | { 58 | // @ts-ignore 59 | "#t": ["bounty-status"], 60 | kinds: [1], 61 | until: queryUntil, 62 | }, 63 | ]; 64 | 65 | let checkBountyExist = []; 66 | let eventLength = []; 67 | 68 | let relayPool = new RelayPool(relays, { useEventCache: true }); 69 | 70 | relayPool.onerror((err, relayUrl) => { 71 | console.log("RelayPool error", err, " from relay ", relayUrl); 72 | }); 73 | relayPool.onnotice((relayUrl, notice) => { 74 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 75 | }); 76 | 77 | relayPool.subscribe( 78 | subFilter, 79 | relays, 80 | (event, isAfterEose, relayURL) => { 81 | let parseDate = parseInt(event.tags[3][1]); 82 | let date = convertTimestamp(parseDate); 83 | let tags_arr: string[] = []; 84 | // @ts-ignore 85 | let ev: event = {}; 86 | 87 | let bountyTitle = event.tags[1][1]; 88 | let bountyReward = formatReward(event.tags[2][1]); 89 | let bountyDatePosted = date; 90 | 91 | event.tags.map((item) => { 92 | if (item[0] === "t") { 93 | switch (item[1]) { 94 | case "design-bounty": 95 | tags_arr.push("design"); 96 | break; 97 | case "development-bounty": 98 | tags_arr.push("development"); 99 | break; 100 | case "debugging-bounty": 101 | tags_arr.push("debugging"); 102 | break; 103 | case "writing-bounty": 104 | tags_arr.push("writing"); 105 | break; 106 | case "cybersecurity-bounty": 107 | tags_arr.push("cybersecurity"); 108 | break; 109 | case "marketing-bounty": 110 | tags_arr.push("marketing"); 111 | break; 112 | } 113 | } 114 | 115 | if (item[0] === "d") { 116 | ev.Dtag = item[1]; 117 | } 118 | 119 | ev.tags = tags_arr; 120 | }); 121 | 122 | let userMetadataFilter = [{ kinds: [0], authors: [event.pubkey] }]; 123 | relayPool.subscribe( 124 | userMetadataFilter, 125 | defaultRelays, 126 | (event, isAfterEose, relayURL) => { 127 | let metadata = JSON.parse(event.content); 128 | 129 | ev.name = metadata.username; 130 | ev.profilePic = metadata.picture; 131 | }, 132 | undefined, 133 | undefined, 134 | { unsubscribeOnEose: true } 135 | ); 136 | 137 | ev.title = bountyTitle; 138 | ev.reward = bountyReward; 139 | ev.createdAt = bountyDatePosted; 140 | ev.pubkey = event.pubkey; 141 | ev.timestamp = event.created_at; 142 | 143 | setEventData((arr) => [...arr, ev]); 144 | eventLength.push(ev); 145 | checkBountyExist.push(event.id); 146 | }, 147 | undefined, 148 | undefined, 149 | { unsubscribeOnEose: true } 150 | ); 151 | 152 | relayPool.subscribe( 153 | subFilterStatus, 154 | defaultRelays, 155 | (event, isAfterEose, relayUrl) => { 156 | let dTag = `${event.tags[0][1]}`; 157 | let hasdTag = statuses.hasOwnProperty(dTag); 158 | 159 | if (!hasdTag) { 160 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 161 | } else { 162 | if (event.created_at > statuses[`${dTag}`][1]) 163 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 164 | } 165 | setCurrentStatus(statuses); 166 | }, 167 | undefined, 168 | undefined, 169 | { unsubscribeOnEose: true } 170 | ); 171 | 172 | setTimeout(() => { 173 | relayPool.close().then(() => { 174 | console.log("connection closed"); 175 | }); 176 | if (checkBountyExist.length === 0) { 177 | setBountyNotFound(true); 178 | clearInterval(closeMyInterval); 179 | } 180 | }, 40000); 181 | 182 | let closeMyInterval = setInterval(() => { 183 | if ( 184 | eventLength.length === checkBountyExist.length && 185 | eventLength.length >= 1 186 | ) { 187 | setDataLoaded(true); 188 | setLoadingMessage(false); 189 | clearInterval(closeMyInterval); 190 | } 191 | }, 1800); 192 | }, [loadMore]); 193 | 194 | useEffect(() => { 195 | setCurrentBountyCount(eventData.length); 196 | }, [eventData]); 197 | 198 | return ( 199 |
200 |
201 | 202 |
203 |
204 | 205 |
206 |
207 | 208 |
209 | {dataLoaded ? ( 210 |
211 | {eventData.map((item, index) => { 212 | return ( 213 |
214 | 215 |
216 | ); 217 | })} 218 |
219 | ) : ( 220 |
221 | Loading... 222 |
223 | )} 224 |
225 | {currentBountyCount! >= correctBountyCount ? ( 226 | 234 | ) : null} 235 | {currentBountyCount! < correctBountyCount ? ( 236 |

237 | We didn't find more bounties 238 |

239 | ) : null} 240 |
241 |
242 | {bountyNotFound ? : null} 243 |
244 |
245 | ); 246 | } 247 | 248 | export default App; 249 | -------------------------------------------------------------------------------- /src/pages/tags/marketingBounties.tsx: -------------------------------------------------------------------------------- 1 | import SideBarMenu from "../../components/menus/sidebarMenu/sidebarMenu"; 2 | import BountiesNotFound from "../../components/errors/bountiesNotFound"; 3 | import MobileMenu from "../../components/menus/mobileMenu/mobileMenu"; 4 | import CategoryList from "../../components/categoriesList/categoryList"; 5 | import BountyCard from "../../components/bounty/bountyCardShortInfo/bountyCardShortInfo"; 6 | 7 | import { useState, useEffect } from "react"; 8 | import { convertTimestamp, formatReward } from "../../utils"; 9 | import { RelayPool } from "nostr-relaypool"; 10 | import { defaultRelaysToPublish, defaultRelays } from "../../const"; 11 | 12 | type event = { 13 | Dtag: string; 14 | createdAt: string; 15 | name: string; 16 | profilePic: string; 17 | pubkey: string; 18 | reward: string; 19 | tags: string[]; 20 | title: string; 21 | timestamp: number; 22 | }; 23 | 24 | function App() { 25 | let currentTimestamp = Math.floor(Date.now() / 1000); 26 | let [eventData, setEventData] = useState([]); 27 | let [bountyNotFound, setBountyNotFound] = useState(false); 28 | let [dataLoaded, setDataLoaded] = useState(false); 29 | let [loadMore, setLoadMore] = useState(false); 30 | let [loadingMessage, setLoadingMessage] = useState(false); 31 | let [queryUntil, setQueryUntil] = useState(currentTimestamp); 32 | let [currentBountyCount, setCurrentBountyCount] = useState(); 33 | let [correctBountyCount, setCorrectBountyCount] = useState(10); 34 | let [currentStatus, setCurrentStatus] = useState({}); 35 | 36 | function loadMoreBounties() { 37 | let lastElement = eventData.length - 1; 38 | setQueryUntil(eventData[lastElement].timestamp); 39 | setLoadMore(!loadMore); 40 | setLoadingMessage(true); 41 | setCorrectBountyCount(correctBountyCount + 20); 42 | } 43 | 44 | useEffect(() => { 45 | let relays = defaultRelaysToPublish; 46 | let statuses: any = {}; 47 | let subFilter = [ 48 | { 49 | kinds: [30023], 50 | "#t": ["marketing-bounty"], 51 | until: queryUntil, 52 | limit: 20, 53 | }, 54 | ]; 55 | 56 | let subFilterStatus = [ 57 | { 58 | // @ts-ignore 59 | "#t": ["bounty-status"], 60 | kinds: [1], 61 | until: queryUntil, 62 | }, 63 | ]; 64 | 65 | let checkBountyExist = []; 66 | let eventLength = []; 67 | 68 | let relayPool = new RelayPool(relays, { useEventCache: true }); 69 | 70 | relayPool.onerror((err, relayUrl) => { 71 | console.log("RelayPool error", err, " from relay ", relayUrl); 72 | }); 73 | relayPool.onnotice((relayUrl, notice) => { 74 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 75 | }); 76 | 77 | relayPool.subscribe( 78 | subFilter, 79 | relays, 80 | (event, isAfterEose, relayURL) => { 81 | let parseDate = parseInt(event.tags[3][1]); 82 | let date = convertTimestamp(parseDate); 83 | let tags_arr: string[] = []; 84 | // @ts-ignore 85 | let ev: event = {}; 86 | 87 | let bountyTitle = event.tags[1][1]; 88 | let bountyReward = formatReward(event.tags[2][1]); 89 | let bountyDatePosted = date; 90 | 91 | event.tags.map((item) => { 92 | if (item[0] === "t") { 93 | switch (item[1]) { 94 | case "design-bounty": 95 | tags_arr.push("design"); 96 | break; 97 | case "development-bounty": 98 | tags_arr.push("development"); 99 | break; 100 | case "debugging-bounty": 101 | tags_arr.push("debugging"); 102 | break; 103 | case "writing-bounty": 104 | tags_arr.push("writing"); 105 | break; 106 | case "cybersecurity-bounty": 107 | tags_arr.push("cybersecurity"); 108 | break; 109 | case "marketing-bounty": 110 | tags_arr.push("marketing"); 111 | break; 112 | } 113 | } 114 | 115 | if (item[0] === "d") { 116 | ev.Dtag = item[1]; 117 | } 118 | 119 | ev.tags = tags_arr; 120 | }); 121 | 122 | let userMetadataFilter = [{ kinds: [0], authors: [event.pubkey] }]; 123 | relayPool.subscribe( 124 | userMetadataFilter, 125 | defaultRelays, 126 | (event, isAfterEose, relayURL) => { 127 | let metadata = JSON.parse(event.content); 128 | 129 | ev.name = metadata.username; 130 | ev.profilePic = metadata.picture; 131 | }, 132 | undefined, 133 | undefined, 134 | { unsubscribeOnEose: true } 135 | ); 136 | 137 | ev.title = bountyTitle; 138 | ev.reward = bountyReward; 139 | ev.createdAt = bountyDatePosted; 140 | ev.pubkey = event.pubkey; 141 | ev.timestamp = event.created_at; 142 | 143 | setEventData((arr) => [...arr, ev]); 144 | eventLength.push(ev); 145 | checkBountyExist.push(event.id); 146 | }, 147 | undefined, 148 | undefined, 149 | { unsubscribeOnEose: true } 150 | ); 151 | 152 | relayPool.subscribe( 153 | subFilterStatus, 154 | defaultRelays, 155 | (event, isAfterEose, relayUrl) => { 156 | let dTag = `${event.tags[0][1]}`; 157 | let hasdTag = statuses.hasOwnProperty(dTag); 158 | 159 | if (!hasdTag) { 160 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 161 | } else { 162 | if (event.created_at > statuses[`${dTag}`][1]) 163 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 164 | } 165 | setCurrentStatus(statuses); 166 | }, 167 | undefined, 168 | undefined, 169 | { unsubscribeOnEose: true } 170 | ); 171 | 172 | setTimeout(() => { 173 | relayPool.close().then(() => { 174 | console.log("connection closed"); 175 | }); 176 | if (checkBountyExist.length === 0) { 177 | setBountyNotFound(true); 178 | clearInterval(closeMyInterval); 179 | } 180 | }, 40000); 181 | 182 | let closeMyInterval = setInterval(() => { 183 | if ( 184 | eventLength.length === checkBountyExist.length && 185 | eventLength.length >= 1 186 | ) { 187 | setDataLoaded(true); 188 | setLoadingMessage(false); 189 | clearInterval(closeMyInterval); 190 | } 191 | }, 1800); 192 | }, [loadMore]); 193 | 194 | useEffect(() => { 195 | setCurrentBountyCount(eventData.length); 196 | }, [eventData]); 197 | 198 | return ( 199 |
200 |
201 | 202 |
203 |
204 | 205 |
206 |
207 | 208 |
209 | {dataLoaded ? ( 210 |
211 | {eventData.map((item, index) => { 212 | return ( 213 |
214 | 215 |
216 | ); 217 | })} 218 |
219 | ) : ( 220 |
221 | Loading... 222 |
223 | )} 224 |
225 | {currentBountyCount! >= correctBountyCount ? ( 226 | 234 | ) : null} 235 | {currentBountyCount! < correctBountyCount ? ( 236 |

237 | We didn't find more bounties 238 |

239 | ) : null} 240 |
241 |
242 | {bountyNotFound ? : null} 243 |
244 |
245 | ); 246 | } 247 | 248 | export default App; 249 | -------------------------------------------------------------------------------- /src/pages/tags/developmentBounties.tsx: -------------------------------------------------------------------------------- 1 | import SideBarMenu from "../../components/menus/sidebarMenu/sidebarMenu"; 2 | import BountiesNotFound from "../../components/errors/bountiesNotFound"; 3 | import MobileMenu from "../../components/menus/mobileMenu/mobileMenu"; 4 | import CategoryList from "../../components/categoriesList/categoryList"; 5 | import BountyCard from "../../components/bounty/bountyCardShortInfo/bountyCardShortInfo"; 6 | 7 | import { useState, useEffect } from "react"; 8 | import { convertTimestamp, formatReward } from "../../utils"; 9 | import { RelayPool } from "nostr-relaypool"; 10 | import { defaultRelaysToPublish, defaultRelays } from "../../const"; 11 | 12 | type event = { 13 | Dtag: string; 14 | createdAt: string; 15 | name: string; 16 | profilePic: string; 17 | pubkey: string; 18 | reward: string; 19 | tags: string[]; 20 | title: string; 21 | timestamp: number; 22 | }; 23 | 24 | function App() { 25 | let currentTimestamp = Math.floor(Date.now() / 1000); 26 | let [eventData, setEventData] = useState([]); 27 | let [bountyNotFound, setBountyNotFound] = useState(false); 28 | let [dataLoaded, setDataLoaded] = useState(false); 29 | let [loadMore, setLoadMore] = useState(false); 30 | let [loadingMessage, setLoadingMessage] = useState(false); 31 | let [queryUntil, setQueryUntil] = useState(currentTimestamp); 32 | let [currentBountyCount, setCurrentBountyCount] = useState(); 33 | let [correctBountyCount, setCorrectBountyCount] = useState(10); 34 | let [currentStatus, setCurrentStatus] = useState({}); 35 | 36 | function loadMoreBounties() { 37 | let lastElement = eventData.length - 1; 38 | setQueryUntil(eventData[lastElement].timestamp); 39 | setLoadMore(!loadMore); 40 | setLoadingMessage(true); 41 | setCorrectBountyCount(correctBountyCount + 20); 42 | } 43 | 44 | useEffect(() => { 45 | let relays = defaultRelaysToPublish; 46 | let statuses: any = {}; 47 | let subFilter = [ 48 | { 49 | kinds: [30023], 50 | "#t": ["development-bounty"], 51 | until: queryUntil, 52 | limit: 20, 53 | }, 54 | ]; 55 | 56 | let subFilterStatus = [ 57 | { 58 | // @ts-ignore 59 | "#t": ["bounty-status"], 60 | kinds: [1], 61 | until: queryUntil, 62 | }, 63 | ]; 64 | 65 | let checkBountyExist = []; 66 | let eventLength = []; 67 | 68 | let relayPool = new RelayPool(relays, { useEventCache: true }); 69 | 70 | relayPool.onerror((err, relayUrl) => { 71 | console.log("RelayPool error", err, " from relay ", relayUrl); 72 | }); 73 | relayPool.onnotice((relayUrl, notice) => { 74 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 75 | }); 76 | 77 | relayPool.subscribe( 78 | subFilter, 79 | relays, 80 | (event, isAfterEose, relayURL) => { 81 | let parseDate = parseInt(event.tags[3][1]); 82 | let date = convertTimestamp(parseDate); 83 | let tags_arr: string[] = []; 84 | // @ts-ignore 85 | let ev: event = {}; 86 | 87 | let bountyTitle = event.tags[1][1]; 88 | let bountyReward = formatReward(event.tags[2][1]); 89 | let bountyDatePosted = date; 90 | 91 | event.tags.map((item) => { 92 | if (item[0] === "t") { 93 | switch (item[1]) { 94 | case "design-bounty": 95 | tags_arr.push("design"); 96 | break; 97 | case "development-bounty": 98 | tags_arr.push("development"); 99 | break; 100 | case "debugging-bounty": 101 | tags_arr.push("debugging"); 102 | break; 103 | case "writing-bounty": 104 | tags_arr.push("writing"); 105 | break; 106 | case "cybersecurity-bounty": 107 | tags_arr.push("cybersecurity"); 108 | break; 109 | case "marketing-bounty": 110 | tags_arr.push("marketing"); 111 | break; 112 | } 113 | } 114 | 115 | if (item[0] === "d") { 116 | ev.Dtag = item[1]; 117 | } 118 | 119 | ev.tags = tags_arr; 120 | }); 121 | 122 | let userMetadataFilter = [{ kinds: [0], authors: [event.pubkey] }]; 123 | relayPool.subscribe( 124 | userMetadataFilter, 125 | defaultRelays, 126 | (event, isAfterEose, relayURL) => { 127 | let metadata = JSON.parse(event.content); 128 | 129 | ev.name = metadata.username; 130 | ev.profilePic = metadata.picture; 131 | }, 132 | undefined, 133 | undefined, 134 | { unsubscribeOnEose: true } 135 | ); 136 | 137 | ev.title = bountyTitle; 138 | ev.reward = bountyReward; 139 | ev.createdAt = bountyDatePosted; 140 | ev.pubkey = event.pubkey; 141 | ev.timestamp = event.created_at; 142 | 143 | setEventData((arr) => [...arr, ev]); 144 | eventLength.push(ev); 145 | checkBountyExist.push(event.id); 146 | }, 147 | undefined, 148 | undefined, 149 | { unsubscribeOnEose: true } 150 | ); 151 | 152 | relayPool.subscribe( 153 | subFilterStatus, 154 | defaultRelays, 155 | (event, isAfterEose, relayUrl) => { 156 | let dTag = `${event.tags[0][1]}`; 157 | let hasdTag = statuses.hasOwnProperty(dTag); 158 | 159 | if (!hasdTag) { 160 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 161 | } else { 162 | if (event.created_at > statuses[`${dTag}`][1]) 163 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 164 | } 165 | setCurrentStatus(statuses); 166 | }, 167 | undefined, 168 | undefined, 169 | { unsubscribeOnEose: true } 170 | ); 171 | 172 | setTimeout(() => { 173 | relayPool.close().then(() => { 174 | console.log("connection closed"); 175 | }); 176 | if (checkBountyExist.length === 0) { 177 | setBountyNotFound(true); 178 | clearInterval(closeMyInterval); 179 | } 180 | }, 40000); 181 | 182 | let closeMyInterval = setInterval(() => { 183 | if ( 184 | eventLength.length === checkBountyExist.length && 185 | eventLength.length >= 1 186 | ) { 187 | setDataLoaded(true); 188 | setLoadingMessage(false); 189 | clearInterval(closeMyInterval); 190 | } 191 | }, 1800); 192 | }, [loadMore]); 193 | 194 | useEffect(() => { 195 | setCurrentBountyCount(eventData.length); 196 | }, [eventData]); 197 | 198 | return ( 199 |
200 |
201 | 202 |
203 |
204 | 205 |
206 |
207 | 208 |
209 | {dataLoaded ? ( 210 |
211 | {eventData.map((item, index) => { 212 | return ( 213 |
214 | 215 |
216 | ); 217 | })} 218 |
219 | ) : ( 220 |
221 | Loading... 222 |
223 | )} 224 |
225 | {currentBountyCount! >= correctBountyCount ? ( 226 | 234 | ) : null} 235 | {currentBountyCount! < correctBountyCount ? ( 236 |

237 | We didn't find more bounties 238 |

239 | ) : null} 240 |
241 |
242 | {bountyNotFound ? : null} 243 |
244 |
245 | ); 246 | } 247 | 248 | export default App; 249 | -------------------------------------------------------------------------------- /src/pages/tags/cybersecurityBounties.tsx: -------------------------------------------------------------------------------- 1 | import SideBarMenu from "../../components/menus/sidebarMenu/sidebarMenu"; 2 | import BountiesNotFound from "../../components/errors/bountiesNotFound"; 3 | import MobileMenu from "../../components/menus/mobileMenu/mobileMenu"; 4 | import CategoryList from "../../components/categoriesList/categoryList"; 5 | import BountyCard from "../../components/bounty/bountyCardShortInfo/bountyCardShortInfo"; 6 | 7 | import { useState, useEffect } from "react"; 8 | import { convertTimestamp, formatReward } from "../../utils"; 9 | import { RelayPool } from "nostr-relaypool"; 10 | import { defaultRelaysToPublish, defaultRelays } from "../../const"; 11 | 12 | type event = { 13 | Dtag: string; 14 | createdAt: string; 15 | name: string; 16 | profilePic: string; 17 | pubkey: string; 18 | reward: string; 19 | tags: string[]; 20 | title: string; 21 | timestamp: number; 22 | }; 23 | 24 | function App() { 25 | let currentTimestamp = Math.floor(Date.now() / 1000); 26 | let [eventData, setEventData] = useState([]); 27 | let [bountyNotFound, setBountyNotFound] = useState(false); 28 | let [dataLoaded, setDataLoaded] = useState(false); 29 | let [loadMore, setLoadMore] = useState(false); 30 | let [loadingMessage, setLoadingMessage] = useState(false); 31 | let [queryUntil, setQueryUntil] = useState(currentTimestamp); 32 | let [currentBountyCount, setCurrentBountyCount] = useState(); 33 | let [correctBountyCount, setCorrectBountyCount] = useState(10); 34 | let [currentStatus, setCurrentStatus] = useState({}); 35 | 36 | function loadMoreBounties() { 37 | let lastElement = eventData.length - 1; 38 | setQueryUntil(eventData[lastElement].timestamp); 39 | setLoadMore(!loadMore); 40 | setLoadingMessage(true); 41 | setCorrectBountyCount(correctBountyCount + 20); 42 | } 43 | 44 | useEffect(() => { 45 | let relays = defaultRelaysToPublish; 46 | let statuses: any = {}; 47 | let subFilter = [ 48 | { 49 | kinds: [30023], 50 | "#t": ["cybersecurity-bounty"], 51 | until: queryUntil, 52 | limit: 20, 53 | }, 54 | ]; 55 | 56 | let subFilterStatus = [ 57 | { 58 | // @ts-ignore 59 | "#t": ["bounty-status"], 60 | kinds: [1], 61 | until: queryUntil, 62 | }, 63 | ]; 64 | 65 | let checkBountyExist = []; 66 | let eventLength = []; 67 | 68 | let relayPool = new RelayPool(relays, { useEventCache: true }); 69 | 70 | relayPool.onerror((err, relayUrl) => { 71 | console.log("RelayPool error", err, " from relay ", relayUrl); 72 | }); 73 | relayPool.onnotice((relayUrl, notice) => { 74 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 75 | }); 76 | 77 | relayPool.subscribe( 78 | subFilter, 79 | relays, 80 | (event, isAfterEose, relayURL) => { 81 | let parseDate = parseInt(event.tags[3][1]); 82 | let date = convertTimestamp(parseDate); 83 | let tags_arr: string[] = []; 84 | // @ts-ignore 85 | let ev: event = {}; 86 | 87 | let bountyTitle = event.tags[1][1]; 88 | let bountyReward = formatReward(event.tags[2][1]); 89 | let bountyDatePosted = date; 90 | 91 | event.tags.map((item) => { 92 | if (item[0] === "t") { 93 | switch (item[1]) { 94 | case "design-bounty": 95 | tags_arr.push("design"); 96 | break; 97 | case "development-bounty": 98 | tags_arr.push("development"); 99 | break; 100 | case "debugging-bounty": 101 | tags_arr.push("debugging"); 102 | break; 103 | case "writing-bounty": 104 | tags_arr.push("writing"); 105 | break; 106 | case "cybersecurity-bounty": 107 | tags_arr.push("cybersecurity"); 108 | break; 109 | case "marketing-bounty": 110 | tags_arr.push("marketing"); 111 | break; 112 | } 113 | } 114 | 115 | if (item[0] === "d") { 116 | ev.Dtag = item[1]; 117 | } 118 | 119 | ev.tags = tags_arr; 120 | }); 121 | 122 | let userMetadataFilter = [{ kinds: [0], authors: [event.pubkey] }]; 123 | relayPool.subscribe( 124 | userMetadataFilter, 125 | defaultRelays, 126 | (event, isAfterEose, relayURL) => { 127 | let metadata = JSON.parse(event.content); 128 | 129 | ev.name = metadata.username; 130 | ev.profilePic = metadata.picture; 131 | }, 132 | undefined, 133 | undefined, 134 | { unsubscribeOnEose: true } 135 | ); 136 | 137 | ev.title = bountyTitle; 138 | ev.reward = bountyReward; 139 | ev.createdAt = bountyDatePosted; 140 | ev.pubkey = event.pubkey; 141 | ev.timestamp = event.created_at; 142 | 143 | setEventData((arr) => [...arr, ev]); 144 | eventLength.push(ev); 145 | checkBountyExist.push(event.id); 146 | }, 147 | undefined, 148 | undefined, 149 | { unsubscribeOnEose: true } 150 | ); 151 | 152 | relayPool.subscribe( 153 | subFilterStatus, 154 | defaultRelays, 155 | (event, isAfterEose, relayUrl) => { 156 | let dTag = `${event.tags[0][1]}`; 157 | let hasdTag = statuses.hasOwnProperty(dTag); 158 | 159 | if (!hasdTag) { 160 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 161 | } else { 162 | if (event.created_at > statuses[`${dTag}`][1]) 163 | statuses[`${dTag}`] = [event.tags[1][1], event.created_at]; 164 | } 165 | setCurrentStatus(statuses); 166 | }, 167 | undefined, 168 | undefined, 169 | { unsubscribeOnEose: true } 170 | ); 171 | 172 | setTimeout(() => { 173 | relayPool.close().then(() => { 174 | console.log("connection closed"); 175 | }); 176 | if (checkBountyExist.length === 0) { 177 | setBountyNotFound(true); 178 | clearInterval(closeMyInterval); 179 | } 180 | }, 40000); 181 | 182 | let closeMyInterval = setInterval(() => { 183 | if ( 184 | eventLength.length === checkBountyExist.length && 185 | eventLength.length >= 1 186 | ) { 187 | setDataLoaded(true); 188 | setLoadingMessage(false); 189 | clearInterval(closeMyInterval); 190 | } 191 | }, 1800); 192 | }, [loadMore]); 193 | 194 | useEffect(() => { 195 | setCurrentBountyCount(eventData.length); 196 | }, [eventData]); 197 | 198 | return ( 199 |
200 |
201 | 202 |
203 |
204 | 205 |
206 |
207 | 208 |
209 | {dataLoaded ? ( 210 |
211 | {eventData.map((item, index) => { 212 | return ( 213 |
214 | 215 |
216 | ); 217 | })} 218 |
219 | ) : ( 220 |
221 | Loading... 222 |
223 | )} 224 |
225 | {currentBountyCount! >= correctBountyCount ? ( 226 | 234 | ) : null} 235 | {currentBountyCount! < correctBountyCount ? ( 236 |

237 | We didn't find more bounties 238 |

239 | ) : null} 240 |
241 |
242 | {bountyNotFound ? : null} 243 |
244 |
245 | ); 246 | } 247 | 248 | export default App; 249 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import SideBarMenu from "./components/menus/sidebarMenu/sidebarMenu"; 2 | import BountiesNotFound from "./components/errors/bountiesNotFound"; 3 | import MobileMenu from "./components/menus/mobileMenu/mobileMenu"; 4 | import CategoryList from "./components/categoriesList/categoryList"; 5 | import BountyCard from "./components/bounty/bountyCardShortInfo/bountyCardShortInfo"; 6 | 7 | import { useState, useEffect } from "react"; 8 | import { convertTimestamp, formatReward } from "./utils"; 9 | import { RelayPool } from "nostr-relaypool"; 10 | import { defaultRelaysToPublish, defaultRelays } from "./const"; 11 | 12 | type event = { 13 | Dtag: string; 14 | createdAt: string; 15 | name: string; 16 | profilePic: string; 17 | pubkey: string; 18 | reward: string; 19 | tags: string[]; 20 | title: string; 21 | timestamp: number; 22 | }; 23 | 24 | type statusesObject = { 25 | [key: string]: [string, number]; 26 | }; 27 | 28 | function App() { 29 | let currentTimestamp = Math.floor(Date.now() / 1000); 30 | let [eventData, setEventData] = useState([]); 31 | let [bountyNotFound, setBountyNotFound] = useState(false); 32 | let [dataLoaded, setDataLoaded] = useState(false); 33 | let [loadMore, setLoadMore] = useState(false); 34 | let [loadingMessage, setLoadingMessage] = useState(false); 35 | let [queryUntil, setQueryUntil] = useState(currentTimestamp); 36 | let [currentBountyCount, setCurrentBountyCount] = useState(); 37 | let [correctBountyCount, setCorrectBountyCount] = useState(10); 38 | let [currentStatus, setCurrentStatus] = useState({}); 39 | 40 | function loadMoreBounties() { 41 | let lastElement = eventData.length - 1; 42 | setQueryUntil(eventData[lastElement].timestamp); 43 | setLoadMore(!loadMore); 44 | setLoadingMessage(true); 45 | setCorrectBountyCount(correctBountyCount + 20); 46 | } 47 | 48 | useEffect(() => { 49 | let relays = defaultRelaysToPublish; 50 | let statuses: any = {}; 51 | let subFilter = [ 52 | { 53 | kinds: [30023], 54 | "#t": ["bounty"], 55 | until: queryUntil, 56 | limit: 20, 57 | }, 58 | ]; 59 | 60 | let subFilterStatus = [ 61 | { 62 | // @ts-ignore 63 | "#t": ["bounty-status"], 64 | kinds: [1], 65 | until: queryUntil, 66 | }, 67 | ]; 68 | 69 | let checkBountyExist = []; 70 | let eventLength = []; 71 | 72 | let relayPool = new RelayPool(relays, { useEventCache: true }); 73 | 74 | relayPool.onerror((err, relayUrl) => { 75 | console.log("RelayPool error", err, " from relay ", relayUrl); 76 | }); 77 | relayPool.onnotice((relayUrl, notice) => { 78 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 79 | }); 80 | 81 | relayPool.subscribe( 82 | subFilter, 83 | relays, 84 | (event, isAfterEose, relayURL) => { 85 | let parseDate = parseInt(event.tags[3][1]); 86 | let date = convertTimestamp(parseDate); 87 | let tags_arr: string[] = []; 88 | // @ts-ignore 89 | let ev: event = {}; 90 | 91 | let bountyTitle = event.tags[1][1]; 92 | let bountyReward = formatReward(event.tags[2][1]); 93 | let bountyDatePosted = date; 94 | 95 | event.tags.map((item) => { 96 | if (item[0] === "t") { 97 | switch (item[1]) { 98 | case "design-bounty": 99 | tags_arr.push("design"); 100 | break; 101 | case "development-bounty": 102 | tags_arr.push("development"); 103 | break; 104 | case "debugging-bounty": 105 | tags_arr.push("debugging"); 106 | break; 107 | case "writing-bounty": 108 | tags_arr.push("writing"); 109 | break; 110 | case "cybersecurity-bounty": 111 | tags_arr.push("cybersecurity"); 112 | break; 113 | case "marketing-bounty": 114 | tags_arr.push("marketing"); 115 | break; 116 | } 117 | } 118 | 119 | if (item[0] === "d") { 120 | ev.Dtag = item[1]; 121 | } 122 | 123 | ev.tags = tags_arr; 124 | }); 125 | 126 | let userMetadataFilter = [{ kinds: [0], authors: [event.pubkey] }]; 127 | relayPool.subscribe( 128 | userMetadataFilter, 129 | defaultRelays, 130 | (event, isAfterEose, relayURL) => { 131 | let metadata = JSON.parse(event.content); 132 | 133 | ev.name = metadata.username; 134 | ev.profilePic = metadata.picture; 135 | }, 136 | undefined, 137 | undefined, 138 | { unsubscribeOnEose: true } 139 | ); 140 | 141 | ev.title = bountyTitle; 142 | ev.reward = bountyReward; 143 | ev.createdAt = bountyDatePosted; 144 | ev.pubkey = event.pubkey; 145 | ev.timestamp = event.created_at; 146 | 147 | setEventData((arr) => [...arr, ev]); 148 | eventLength.push(ev); 149 | checkBountyExist.push(event.id); 150 | }, 151 | undefined, 152 | undefined, 153 | { unsubscribeOnEose: true } 154 | ); 155 | 156 | relayPool.subscribe( 157 | subFilterStatus, 158 | defaultRelays, 159 | (event, isAfterEose, relayUrl) => { 160 | let dTag = `${event.tags[0][1]}`; 161 | let hasdTag = statuses.hasOwnProperty(dTag); 162 | const status = event.tags[1][1]; 163 | 164 | if (!hasdTag) { 165 | statuses[`${dTag}`] = [status, event.created_at]; 166 | } 167 | { 168 | if (event.created_at > statuses[`${dTag}`][1]) 169 | statuses[`${dTag}`] = [status, event.created_at]; 170 | } 171 | setCurrentStatus(statuses); 172 | }, 173 | undefined, 174 | undefined, 175 | { unsubscribeOnEose: true } 176 | ); 177 | 178 | setTimeout(() => { 179 | relayPool.close().then(() => { 180 | console.log("connection closed"); 181 | }); 182 | if (checkBountyExist.length === 0) { 183 | setBountyNotFound(true); 184 | clearInterval(closeMyInterval); 185 | } 186 | }, 40000); 187 | 188 | let closeMyInterval = setInterval(() => { 189 | if ( 190 | eventLength.length === checkBountyExist.length && 191 | eventLength.length >= 1 192 | ) { 193 | setDataLoaded(true); 194 | setLoadingMessage(false); 195 | clearInterval(closeMyInterval); 196 | } 197 | }, 1800); 198 | }, [loadMore]); 199 | 200 | useEffect(() => { 201 | setCurrentBountyCount(eventData.length); 202 | }, [eventData]); 203 | 204 | return ( 205 |
206 |
207 | 208 |
209 |
210 | 211 |
212 |
213 | 214 |
215 | {dataLoaded ? ( 216 |
217 | {eventData.map((item, index) => { 218 | return ( 219 |
220 | 221 |
222 | ); 223 | })} 224 |
225 | ) : ( 226 |
227 | Loading... 228 |
229 | )} 230 |
231 | {currentBountyCount! >= correctBountyCount ? ( 232 | 240 | ) : null} 241 | {dataLoaded && 242 | currentBountyCount! > 0 && 243 | currentBountyCount! < correctBountyCount && 244 | correctBountyCount > 10 ? ( 245 |

246 | We didn't find more bounties 247 |

248 | ) : null} 249 |
250 |
251 | {bountyNotFound ? : null} 252 |
253 |
254 | ); 255 | } 256 | 257 | export default App; 258 | -------------------------------------------------------------------------------- /src/pages/bountyFullInfo.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import { useState, useEffect } from "react"; 3 | import { RelayPool } from "nostr-relaypool"; 4 | import { convertTimestamp, decodeNpubMention } from "../utils"; 5 | import { defaultRelaysToPublish, defaultRelays } from "../const"; 6 | 7 | import BountyLargeInfoOpen from "../components/bounty/bountyLargeInfo/bountyLargeInfoOpen"; 8 | import BountyLargeInfor from "../components/bounty/bountyLargeInfo/bountyLargeInfo"; 9 | import SideBarMenu from "../components/menus/sidebarMenu/sidebarMenu"; 10 | import MobileMenu from "../components/menus/mobileMenu/mobileMenu"; 11 | import { nip19 } from "nostr-tools"; 12 | 13 | type event = { 14 | Dtag: string; 15 | content: string; 16 | id: string; 17 | name: string; 18 | pledged: any[]; 19 | profilePic: string; 20 | pubkey: string; 21 | publishedAt: string; 22 | reward: number; 23 | status: string; 24 | title: string; 25 | bountyHunterMetaData: { 26 | name: string; 27 | profilePic: string; 28 | pubkey: string; 29 | lnAddress: string | null; 30 | }; 31 | applications: { 32 | pubkey: string; 33 | name: string; 34 | profilePic: string; 35 | content: string; 36 | id: string; 37 | createdAt: number; 38 | links: { github: string; personalWebsite: string }; 39 | }[]; 40 | }; 41 | 42 | function BountyInfo() { 43 | const params: any = useParams<{ id: string }>(); 44 | let naddrData = nip19.decode(params.id); 45 | let [eventData, setEventData] = useState(); 46 | let [dataLoaded, setDataLoaded] = useState(false); 47 | let [updateValues, setUpdateValues] = useState(false); 48 | 49 | useEffect(() => { 50 | let subFilterContent = [ 51 | { 52 | // @ts-ignore 53 | "#d": [naddrData.data.identifier], 54 | kind: [30023], 55 | }, 56 | ]; 57 | 58 | let relayPool = new RelayPool(defaultRelays); 59 | 60 | relayPool.onerror((err, relayUrl) => { 61 | console.log("RelayPool error", err, " from relay ", relayUrl); 62 | }); 63 | relayPool.onnotice((relayUrl, notice) => { 64 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 65 | }); 66 | 67 | //subscribe for content 68 | relayPool.subscribe( 69 | subFilterContent, 70 | defaultRelaysToPublish, 71 | (event, isAfterEose, relayURL) => { 72 | if (event.kind === 30023) { 73 | //@ts-ignore 74 | let ev: event = {}; 75 | let parseDate = parseInt(event.tags[3][1]); 76 | let date = convertTimestamp(parseDate); 77 | let tags_arr: string[] = []; 78 | 79 | //Subscribe bounty poster metadata 80 | let bountyPosterMetadataFilter = [ 81 | { kinds: [0], authors: [event.pubkey] }, 82 | ]; 83 | relayPool.subscribe( 84 | bountyPosterMetadataFilter, 85 | defaultRelays, 86 | (event, isAfterEose, relayURL) => { 87 | let metadata = JSON.parse(event.content); 88 | 89 | ev.name = metadata.username; 90 | ev.profilePic = metadata.picture; 91 | }, 92 | undefined, 93 | undefined, 94 | { unsubscribeOnEose: true } 95 | ); 96 | 97 | event.tags.map((item) => { 98 | if (item[0] === "rootId") { 99 | tags_arr.push(item[1]); 100 | } 101 | if (item[0] === "d") { 102 | ev.Dtag = item[1]; 103 | } 104 | }); 105 | 106 | //subscribe for bounty-added-reward 107 | let subFilterAddedReward = [ 108 | { 109 | "#a": [ 110 | // @ts-ignore 111 | `30023:${naddrData.data.pubkey}:${naddrData.data.identifier}`, 112 | ], 113 | "#t": ["bounty-added-reward"], 114 | kinds: [1], 115 | }, 116 | ]; 117 | relayPool.subscribe( 118 | subFilterAddedReward, 119 | defaultRelays, 120 | (event, isAfterEose, relayURL) => { 121 | let compatAmount: string; 122 | let compatNote: string; 123 | // Get the reward tag from the list of tags 124 | // @ts-ignore 125 | let rewardTag: Array | undefined = event.tags.find( 126 | (elem) => elem[0] === "reward" 127 | ); 128 | 129 | // Conditionally handle older events with amount in the content field 130 | if (event.content === "" || isNaN(Number(event.content))) { 131 | compatNote = event.content; 132 | compatAmount = rewardTag ? rewardTag[1] : "0"; 133 | } else { 134 | compatNote = ""; 135 | compatAmount = event.content; 136 | } 137 | 138 | let pledgersMetadataFilter = [ 139 | { kinds: [0], authors: [event.pubkey] }, 140 | ]; 141 | relayPool.subscribe( 142 | pledgersMetadataFilter, 143 | defaultRelays, 144 | (event, isAfterEose, relayURL) => { 145 | let metadata = JSON.parse(event.content); 146 | 147 | ev.pledged = [ 148 | { 149 | name: metadata.username, 150 | profilePic: metadata.picture, 151 | amount: compatAmount, 152 | note: compatNote, 153 | pubkey: event.pubkey, 154 | }, 155 | ...ev.pledged, 156 | ]; 157 | }, 158 | undefined, 159 | undefined, 160 | { unsubscribeOnEose: true } 161 | ); 162 | }, 163 | undefined, 164 | undefined, 165 | { unsubscribeOnEose: true } 166 | ); 167 | 168 | // suscrbibe for bounty status 169 | let subFilterStatus = [ 170 | { 171 | // @ts-ignore 172 | "#d": [naddrData.data.identifier], 173 | "#t": ["bounty-status"], 174 | kinds: [1], 175 | limit: 1, 176 | }, 177 | ]; 178 | relayPool.subscribe( 179 | subFilterStatus, 180 | defaultRelays, 181 | (event, isAfterEose, relayURL) => { 182 | let isInprogressOrPaid = 183 | event.tags[1][1] === "in progress" || 184 | event.tags[1][1] === "paid"; 185 | 186 | if (isInprogressOrPaid) { 187 | ev.status = event.tags[1][1]; 188 | let bountyHunterNpub = decodeNpubMention(event.content); 189 | let bountyHunterPubkey = nip19.decode(bountyHunterNpub![0]); 190 | let bountyHunterMetadataFilter = [ 191 | { kinds: [0], authors: [bountyHunterPubkey.data] }, 192 | ]; 193 | relayPool.subscribe( 194 | //@ts-ignore 195 | bountyHunterMetadataFilter, 196 | defaultRelays, 197 | (event, isAfterEose, relayURL) => { 198 | let metadata = JSON.parse(event.content); 199 | let haslud06 = metadata.lud06 !== ""; 200 | let haslud16 = metadata.lud16 !== ""; 201 | 202 | ev.bountyHunterMetaData = { 203 | name: metadata.username, 204 | lnAddress: haslud06 205 | ? metadata.lud06 206 | : haslud16 207 | ? metadata.lud16 208 | : "", 209 | profilePic: metadata.picture, 210 | pubkey: event.pubkey, 211 | }; 212 | }, 213 | undefined, 214 | undefined, 215 | { unsubscribeOnEose: true } 216 | ); 217 | } 218 | }, 219 | undefined, 220 | undefined, 221 | { unsubscribeOnEose: true } 222 | ); 223 | 224 | let subFilterApplications = [ 225 | { 226 | // @ts-ignore 227 | "#d": [naddrData.data.identifier], 228 | kinds: [1], 229 | "#t": ["bounty-application"], 230 | }, 231 | ]; 232 | // subscribe for bounty applications 233 | relayPool.subscribe( 234 | subFilterApplications, 235 | defaultRelays, 236 | (event, isAfterEose, relayURL) => { 237 | const pubkey = event.pubkey; 238 | const content = event.content; 239 | const id = event.id; 240 | const createdAt = event.created_at; 241 | const links = { 242 | github: event.tags[2][1], 243 | personalWebsite: event.tags[3][1], 244 | }; 245 | 246 | let applicantsMetadataFilter = [ 247 | { 248 | authors: [event.pubkey], 249 | kinds: [0], 250 | }, 251 | ]; 252 | 253 | relayPool.subscribe( 254 | applicantsMetadataFilter, 255 | defaultRelays, 256 | (event, isAfterEose, relayURL) => { 257 | const metadata = JSON.parse(event.content); 258 | 259 | ev.applications = [ 260 | { 261 | pubkey: pubkey, 262 | name: metadata.username, 263 | profilePic: metadata.picture, 264 | content: content, 265 | id: id, 266 | createdAt: createdAt, 267 | links: links, 268 | }, 269 | ...ev.applications, 270 | ]; 271 | }, 272 | undefined, 273 | undefined, 274 | { unsubscribeOnEose: true } 275 | ); 276 | }, 277 | undefined, 278 | undefined, 279 | { unsubscribeOnEose: true } 280 | ); 281 | 282 | if (!ev.hasOwnProperty("status")) { 283 | ev.status = ""; 284 | } 285 | 286 | ev.pledged = []; 287 | ev.applications = []; 288 | ev.title = event.tags[1][1]; 289 | ev.content = event.content; 290 | ev.reward = parseInt(event.tags[2][1]); 291 | ev.publishedAt = date; 292 | ev.pubkey = event.pubkey; 293 | ev.id = event.id; 294 | 295 | setEventData(ev); 296 | } 297 | } 298 | ); 299 | 300 | setTimeout(() => { 301 | relayPool.close().then(() => { 302 | console.log("connection closed"); 303 | }); 304 | }, 15000); 305 | 306 | setTimeout(() => { 307 | setDataLoaded(true); 308 | }, 3500); 309 | }, [updateValues]); 310 | 311 | return ( 312 |
313 |
314 | 315 |
316 |
317 | 318 |
319 | 320 |
321 | {dataLoaded ? ( 322 |
323 | {eventData?.status === "" ? ( 324 | 329 | ) : ( 330 | 335 | )} 336 |
337 | ) : ( 338 |
339 | Loading... 340 |
341 | )} 342 |
343 |
344 | ); 345 | } 346 | 347 | export default BountyInfo; 348 | -------------------------------------------------------------------------------- /src/pages/profile.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom"; 2 | import { useState, useEffect } from "react"; 3 | import { RelayPool } from "nostr-relaypool"; 4 | import { convertTimestamp, formatReward } from "../utils"; 5 | import { defaultRelaysToPublish, defaultRelays } from "../const"; 6 | import { nip19 } from "nostr-tools"; 7 | 8 | import SideBarMenu from "../components/menus/sidebarMenu/sidebarMenu"; 9 | import BountiesNotFound from "../components/errors/bountiesNotFound"; 10 | import ProfileCard from "../components/profileCard/profileCard"; 11 | import ProfileActivity from "../components/profileCard/profileStats/profileActivity"; 12 | import BountiesPaid from "../components/profileCard/profileStats/profileBountiesPaid"; 13 | import BountiesProgress from "../components/profileCard/profileStats/profileBountiesProgress"; 14 | import BountyCard from "../components/bounty/bountyCardShortInfo/bountyCardShortInfo"; 15 | import MobileMenu from "../components/menus/mobileMenu/mobileMenu"; 16 | import SatsAdded from "../components/profileCard/profileStats/profileBountiesAddedReward"; 17 | 18 | type event = { 19 | Dtag: string; 20 | createdAt: string; 21 | name: string; 22 | profilePic: string; 23 | pubkey: string; 24 | reward: string; 25 | status: string; 26 | tags: string[]; 27 | title: string; 28 | timestamp: number; 29 | }; 30 | 31 | type statusesObject = { 32 | [key: string]: [string, number]; 33 | }; 34 | 35 | function Profile() { 36 | const params = useParams(); 37 | let relays = defaultRelaysToPublish; 38 | let userMetaDataRelays = defaultRelays; 39 | let currentTimestamp = Math.floor(Date.now() / 1000); 40 | let userPubkey = nip19.decode(params.id!).data; 41 | let last30DaysTimestamp = Math.floor(Date.now() / 1000) - 24 * 60 * 60 * 1000; 42 | 43 | let [metaData, setMetada] = useState({}); 44 | let [eventData, setEventData] = useState([]); 45 | let [bountyNotFound, setBountyNotFound] = useState(false); 46 | let [dataLoaded, setDataLoaded] = useState(false); 47 | let [userNip05, setUserNip05] = useState(false); 48 | let [bountyStatuses, setBountyStatuses] = useState({}); 49 | let [Last30Days, setLast30Days] = useState(0); 50 | let [addedReward, setAddedReward] = useState(0); 51 | let [loadMore, setLoadMore] = useState(false); 52 | let [loadingMessage, setLoadingMessage] = useState(false); 53 | let [queryUntil, setQueryUntil] = useState(currentTimestamp); 54 | let [currentBountyCount, setCurrentBountyCount] = useState(); 55 | let [correctBountyCount, setCorrectBountyCount] = useState(10); 56 | 57 | function loadMoreBounties() { 58 | let lastElement = eventData.length - 1; 59 | // @ts-ignore 60 | setQueryUntil(eventData[lastElement].timestamp); 61 | setLoadMore(!loadMore); 62 | setLoadingMessage(true); 63 | setCorrectBountyCount(correctBountyCount + 10); 64 | } 65 | 66 | let subFilterMetaData = [ 67 | { 68 | authors: [`${userPubkey}`], 69 | kinds: [0], 70 | }, 71 | ]; 72 | let subFilterOlderPost = [ 73 | { 74 | authors: [`${userPubkey}`], 75 | kinds: [1], 76 | since: last30DaysTimestamp, 77 | limit: 30, 78 | }, 79 | ]; 80 | let subFilterAddedReward = [ 81 | { 82 | authors: [`${userPubkey}`], 83 | "#t": ["bounty-added-reward"], 84 | }, 85 | ]; 86 | 87 | let subFilterContent = [ 88 | { 89 | authors: [`${userPubkey}`], 90 | kinds: [30023], 91 | "#t": ["bounty"], 92 | until: queryUntil, 93 | limit: 20, 94 | }, 95 | ]; 96 | 97 | let subFilterStatus = [ 98 | { 99 | // @ts-ignore 100 | "#t": ["bounty-status"], 101 | kinds: [1], 102 | until: queryUntil, 103 | authors: [`${userPubkey}`], 104 | }, 105 | ]; 106 | 107 | let checkBountyExist = []; 108 | let eventLength = []; 109 | let bountyHunterStatuses: statusesObject = {}; 110 | 111 | useEffect(() => { 112 | let relayPool = new RelayPool(relays); 113 | 114 | relayPool.onerror((err, relayUrl) => { 115 | console.log("RelayPool error", err, " from relay ", relayUrl); 116 | }); 117 | relayPool.onnotice((relayUrl, notice) => { 118 | console.log("RelayPool notice", notice, " from relay ", relayUrl); 119 | }); 120 | 121 | //subscribe metadata 122 | relayPool.subscribe( 123 | subFilterMetaData, 124 | userMetaDataRelays, 125 | (event, isAfterEose, relayURL) => { 126 | let parsedContent = JSON.parse(event.content); 127 | 128 | let finalData = { 129 | name: parsedContent.display_name, 130 | display_name: parsedContent.display_name, 131 | profilePic: parsedContent.picture, 132 | LnAddress: parsedContent.lud16, 133 | about: parsedContent.about, 134 | nip05: parsedContent.nip05, 135 | }; 136 | 137 | setMetada(finalData); 138 | 139 | if (parsedContent.nip05 !== "" || undefined) { 140 | let url = parsedContent.nip05.split("@"); 141 | fetch(`https://${url[1]}/.well-known/nostr.json?name=${url[0]}`) 142 | .then((response) => response.json()) 143 | .then((data) => { 144 | let userNamePubKey = data.names[`${url[0]}`]; 145 | let isSamePubkey = event.pubkey === userNamePubKey; 146 | if (isSamePubkey) setUserNip05(true); 147 | }); 148 | } 149 | }, 150 | undefined, 151 | undefined, 152 | { unsubscribeOnEose: true } 153 | ); 154 | 155 | //subscribe older posts 156 | relayPool.subscribe( 157 | subFilterOlderPost, 158 | userMetaDataRelays, 159 | (event, isAfterEose, relayURL) => { 160 | setLast30Days((item) => item + 1); 161 | }, 162 | undefined, 163 | undefined, 164 | { unsubscribeOnEose: true } 165 | ); 166 | 167 | //subscribe to know how many sats this user has pledged 168 | relayPool.subscribe( 169 | subFilterAddedReward, 170 | userMetaDataRelays, 171 | (event, isAfterEose, relayURL) => { 172 | let compatAmount: string; 173 | // Get the reward tag from the list of tags 174 | // @ts-ignore 175 | let rewardTag: Array | undefined = event.tags.find( 176 | (elem) => elem[0] === "reward" 177 | ); 178 | 179 | // Conditionally handle older events with amount in the content field 180 | if (event.content === "" || isNaN(Number(event.content))) { 181 | compatAmount = rewardTag ? rewardTag[1] : "0"; 182 | } else { 183 | compatAmount = event.content; 184 | } 185 | let amount = parseInt(compatAmount); 186 | setAddedReward((item) => item + amount); 187 | }, 188 | undefined, 189 | undefined, 190 | { unsubscribeOnEose: true } 191 | ); 192 | 193 | //subscribe for bounties 194 | relayPool.subscribe( 195 | subFilterContent, 196 | relays, 197 | (event, isAfterEose, relayURL) => { 198 | let parseDate = parseInt(event.tags[3][1]); 199 | let date = convertTimestamp(parseDate); 200 | let tags_arr: string[] = []; 201 | // @ts-ignore 202 | let ev: event = {}; 203 | 204 | let bountyTitle = event.tags[1][1]; 205 | let bountyReward = formatReward(event.tags[2][1]); 206 | let bountyDatePosted = date; 207 | 208 | event.tags.map((item) => { 209 | if (item[0] === "t") { 210 | switch (item[1]) { 211 | case "design-bounty": 212 | tags_arr.push("design"); 213 | break; 214 | case "development-bounty": 215 | tags_arr.push("development"); 216 | break; 217 | case "debugging-bounty": 218 | tags_arr.push("debugging"); 219 | break; 220 | case "writing-bounty": 221 | tags_arr.push("writing"); 222 | break; 223 | case "cybersecurity-bounty": 224 | tags_arr.push("cybersecurity"); 225 | break; 226 | case "marketing-bounty": 227 | tags_arr.push("marketing"); 228 | break; 229 | } 230 | } 231 | 232 | if (item[0] === "d") { 233 | ev.Dtag = item[1]; 234 | } 235 | 236 | ev.tags = tags_arr; 237 | }); 238 | 239 | ev.title = bountyTitle; 240 | ev.reward = bountyReward; 241 | ev.createdAt = bountyDatePosted; 242 | ev.pubkey = event.pubkey; 243 | ev.timestamp = event.created_at; 244 | 245 | setEventData((arr) => [...arr, ev]); 246 | checkBountyExist.push(event.id); 247 | eventLength.push(ev); 248 | }, 249 | undefined, 250 | undefined, 251 | { unsubscribeOnEose: true } 252 | ); 253 | 254 | //subscribe for bounty statuses 255 | relayPool.subscribe( 256 | subFilterStatus, 257 | defaultRelays, 258 | (event, isAfterEose, relayUrl) => { 259 | let dTag = `${event.tags[0][1]}`; 260 | let hasdTag = bountyHunterStatuses.hasOwnProperty(dTag); 261 | 262 | if (!hasdTag) { 263 | bountyHunterStatuses[`${dTag}`] = [ 264 | event.tags[1][1], 265 | event.created_at, 266 | ]; 267 | } else { 268 | if (event.created_at > bountyHunterStatuses[`${dTag}`][1]) 269 | bountyHunterStatuses[`${dTag}`] = [ 270 | event.tags[1][1], 271 | event.created_at, 272 | ]; 273 | } 274 | setBountyStatuses(bountyHunterStatuses); 275 | }, 276 | undefined, 277 | undefined, 278 | { unsubscribeOnEose: true } 279 | ); 280 | 281 | setTimeout(() => { 282 | relayPool.close().then(() => { 283 | console.log("connection closed"); 284 | }); 285 | if (checkBountyExist.length === 0) { 286 | setBountyNotFound(true); 287 | clearInterval(closeMyInterval); 288 | } 289 | }, 20000); 290 | 291 | let closeMyInterval = setInterval(() => { 292 | if ( 293 | eventLength.length === checkBountyExist.length && 294 | eventLength.length >= 1 295 | ) { 296 | setDataLoaded(true); 297 | clearInterval(closeMyInterval); 298 | } 299 | }, 1000); 300 | }, [queryUntil]); 301 | 302 | useEffect(() => { 303 | setCurrentBountyCount(eventData.length); 304 | }, [eventData]); 305 | 306 | return ( 307 |
308 |
309 | 310 |
311 |
312 | 313 |
314 | 315 |
316 | 321 | 322 |
323 | 324 | 325 | 326 | 327 |
328 |
329 | {dataLoaded ? ( 330 |
331 | {eventData.map((item, index) => { 332 | return ( 333 |
334 | 335 |
336 | ); 337 | })} 338 |
339 | ) : ( 340 |
341 | Loading... 342 |
343 | )} 344 |
345 |
346 | {currentBountyCount! >= correctBountyCount ? ( 347 | 355 | ) : null} 356 | {dataLoaded && 357 | currentBountyCount! > 0 && 358 | currentBountyCount! < correctBountyCount && 359 | correctBountyCount > 10 ? ( 360 |

361 | We didn't find more bounties 362 |

363 | ) : null} 364 |
365 | {bountyNotFound ? : null} 366 |
367 |
368 | ); 369 | } 370 | 371 | export default Profile; 372 | -------------------------------------------------------------------------------- /src/components/bounty/bountyLargeInfo/bountyLargeInfoOpen.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getNpub, 3 | addReward, 4 | formatReward, 5 | deleteEvent, 6 | isDarkTheme, 7 | shareBounty, 8 | } from "../../../utils"; 9 | import { Link, useNavigate } from "react-router-dom"; 10 | import { nip19 } from "nostr-tools"; 11 | import { useState } from "react"; 12 | import { ReactMarkdown } from "react-markdown/lib/react-markdown"; 13 | 14 | import avatarImage from "../../../assets/nostr-icon-user.avif"; 15 | import shareIconDm from "../../../assets/share-icon-dm.svg"; 16 | import shareIconLg from "../../../assets/share-icon-lg.svg"; 17 | import CouldNotShare from "../../errors/couldNotShare"; 18 | import editIconDm from "../../../assets/edit-icon-dm.svg"; 19 | import editIconLg from "../../../assets/edit-icon-lg.svg"; 20 | import deleteIcon from "../../../assets/delete-icon.svg"; 21 | 22 | import CommentBox from "../bountyApplicantsBox/bountyApplicantsBox"; 23 | import BountyApplicationCard from "../bountyApplication/bountyApplicationCard"; 24 | import BountyUpdateStatusCard from "../bountyStatus/bountyStatus"; 25 | 26 | type event = { 27 | ev: { 28 | Dtag: string; 29 | content: string; 30 | id: string; 31 | name: string; 32 | pledged: any[]; 33 | profilePic: string; 34 | pubkey: string; 35 | publishedAt: string; 36 | reward: number; 37 | status: string; 38 | title: string; 39 | applications: any[]; 40 | }; 41 | }; 42 | 43 | function BountyLargeInfor({ ev, updateValues, dataLoaded }: event | any) { 44 | let naddr = nip19.naddrEncode({ 45 | identifier: ev.Dtag, 46 | pubkey: ev.pubkey, 47 | kind: 30023, 48 | }); 49 | 50 | let [rewardToAdd, setRewardToAdd] = useState(""); 51 | let [rewardNoteToAdd, setRewardNoteToAdd] = useState(""); 52 | let [notShared, setNotShared] = useState(false); 53 | let [applicationModal, setAppicationModal] = useState(false); 54 | let [statusModal, setStatusModal] = useState(false); 55 | 56 | let totalReward = getFinalReward(); 57 | let isLogged = sessionStorage.getItem("pubkey"); 58 | let posterNpub = nip19.npubEncode(ev.pubkey); 59 | let posterNpubShortened = getNpub(ev.pubkey); 60 | let navigate = useNavigate(); 61 | 62 | function getFinalReward() { 63 | let totalReward = ev.reward; 64 | ev.pledged.map((item: any) => { 65 | let value = parseInt(item.amount); 66 | totalReward += value; 67 | }); 68 | 69 | return totalReward; 70 | } 71 | 72 | function closeModal() { 73 | setAppicationModal(false); 74 | } 75 | 76 | async function shareBountyFn() { 77 | let wasShared = await shareBounty(`https://nostrbounties.com/b/${naddr}`); 78 | if (wasShared !== undefined) { 79 | setNotShared(!notShared); 80 | 81 | setTimeout(() => { 82 | setNotShared(false); 83 | }, 2000); 84 | } 85 | } 86 | 87 | function deleteBountyFn() { 88 | let event = deleteEvent(ev.id); 89 | event.then((data) => { 90 | navigate(`/`); 91 | }); 92 | } 93 | 94 | return ( 95 |
96 | {notShared ? : null} 97 | {applicationModal ? ( 98 | 105 | ) : null} 106 | {statusModal ? ( 107 | 119 | ) : null} 120 | 121 |
122 |
123 |
124 |
125 |
126 |
127 |

128 | {formatReward(totalReward)} sats 129 |

130 |
131 | {ev.status === "" ? ( 132 |

133 | Status: Open 134 |

135 | ) : null} 136 | {isLogged === ev.pubkey ? ( 137 |
138 | {ev.status !== "in progress" ? ( 139 | 145 | ) : null} 146 |
147 | ) : null} 148 |
149 |
150 |
151 |
152 | shareBountyFn()} 157 | alt="share icon" 158 | > 159 | {isLogged === ev.pubkey ? ( 160 |
161 | navigate(`/edit/${naddr}`)} 166 | alt="edit icon" 167 | > 168 | { 173 | deleteBountyFn(); 174 | }} 175 | alt="delete icon" 176 | > 177 |
178 | ) : null} 179 |
180 |
181 | 182 |
183 |

184 | {ev.title} 185 |

186 |
187 |

188 | posted: {ev.publishedAt} by 189 |

190 |
191 | 195 | {ev.name === "" || ev.name === undefined 196 | ? posterNpubShortened 197 | : ev.name} 198 | 199 | avatar image 208 |
209 |
210 |
211 |
212 |
213 | 214 | {ev.content} 215 | 216 |
217 |
218 | {ev.pubkey !== isLogged ? ( 219 | 227 | ) : null} 228 | 229 |
230 | {ev.pledged.map((item: any) => { 231 | let npubAddedReward = nip19.npubEncode(item.pubkey); 232 | let userWithoutName = getNpub(item.pubkey); 233 | return ( 234 |
235 |
239 |
240 | avatar image 247 |
248 |
249 | 253 | {item.name === "" ? userWithoutName : item.name} 254 | {" "} 255 | added{" "} 256 | 257 | {formatReward(item.amount)} sats 258 | {" "} 259 | {item.note.length > 0 && ( 260 | 261 | with the note:{" "} 262 | 263 | {item.note} 264 | 265 | 266 | )} 267 |
268 |
269 |
270 | ); 271 | })} 272 |
273 | 274 | {ev.status === "paid" ? null : ( 275 |
276 | setRewardNoteToAdd(e.target.value)} 279 | className="peer min-h-[auto] basis-6/12 bg-gray-50 border-y border-x border-dark-text text-dark-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-input-bg-dm dark:text-gray-1 border-0" 280 | placeholder="Add a note about why you're adding to this reward (optional)" 281 | value={rewardNoteToAdd} 282 | /> 283 | setRewardToAdd(e.target.value)} 286 | className="peer min-h-[auto] basis-6/12 bg-gray-50 border-y border-x border-dark-text text-dark-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-input-bg-dm dark:text-gray-1 border-0" 287 | placeholder="Add sats to the initial reward" 288 | value={rewardToAdd} 289 | required 290 | /> 291 | 307 |
308 | )} 309 |
310 |
311 | {ev.applications.map((applications: any) => { 312 | return ( 313 |
314 | 322 |
323 | ); 324 | })} 325 |
326 |
327 | ); 328 | } 329 | 330 | export default BountyLargeInfor; 331 | -------------------------------------------------------------------------------- /src/utils.tsx: -------------------------------------------------------------------------------- 1 | import { RelayPool } from "nostr-relaypool"; 2 | import { nip19 } from "nostr-tools"; 3 | import { defaultRelays, defaultRelaysToPublish, allRelays } from "./const"; 4 | import { bech32 } from "bech32"; 5 | 6 | export function convertTimestamp(unixTimestamp: number): string { 7 | const now = Date.now(); 8 | const diff = now - unixTimestamp * 1000; 9 | const seconds = Math.floor(diff / 1000); 10 | const minutes = Math.floor(seconds / 60); 11 | const hours = Math.floor(minutes / 60); 12 | const days = Math.floor(hours / 24); 13 | 14 | if (days === 1) { 15 | return `${days} day ago`; 16 | } else if (days >= 2) { 17 | return `${days} days ago`; 18 | } else if (hours >= 1) { 19 | return `${hours} hours ago`; 20 | } else if (minutes >= 1) { 21 | return `${minutes} minutes ago`; 22 | } else { 23 | return `${seconds} seconds ago`; 24 | } 25 | } 26 | 27 | export async function editBounty(event: any) { 28 | let relays = allRelays; 29 | 30 | // @ts-ignore 31 | if (!window.nostr) { 32 | console.log("you need to install an extension"); 33 | } 34 | // @ts-ignore 35 | let EventMessageSigned = await window.nostr.signEvent(event); 36 | console.log(EventMessageSigned.content); 37 | let relayPool = new RelayPool(relays); 38 | relayPool.publish(EventMessageSigned, relays); 39 | console.log("edited"); 40 | return EventMessageSigned; 41 | } 42 | 43 | export function decodeNpubMention(content: string) { 44 | let npubs: string[] = []; 45 | const regex = /(nostr:)(.{1,63})/g; 46 | const match = content.match(regex); 47 | 48 | match?.map((item) => { 49 | let arrWithNpub = item.split(":"); 50 | npubs.push(arrWithNpub[1]); 51 | }); 52 | 53 | return npubs; 54 | } 55 | 56 | export async function getPersonalRelays() { 57 | // @ts-ignore 58 | let personalRelays = await window.nostr.getRelays(); 59 | return personalRelays; 60 | } 61 | 62 | export async function getPubKey() { 63 | // @ts-ignore 64 | let pubKey = await window.nostr.getPublicKey(); 65 | return pubKey; 66 | } 67 | 68 | export async function sendReply( 69 | currentStatus: string | null, 70 | bountyHunterNpub: string, 71 | dTag: string, 72 | posterPubkey: string, 73 | eventId: string, 74 | naddr: string 75 | ) { 76 | let relays = allRelays; 77 | let rootBountyUrl = `https://nostrbounties.com/b/${naddr}`; 78 | 79 | if (currentStatus === "") { 80 | let eventMessage = { 81 | id: null, 82 | pubkey: null, 83 | created_at: Math.floor(Date.now() / 1000), 84 | kind: 1, 85 | tags: [ 86 | ["d", dTag], 87 | ["status", "in progress"], 88 | ["t", "bounty-status"], 89 | ["a", `30023:${posterPubkey}:${dTag}`], 90 | ], 91 | content: `nostr:${bountyHunterNpub} was assigned to work on this bounty: ${rootBountyUrl} from nostrbounties.com`, 92 | sig: null, 93 | }; 94 | // @ts-ignore 95 | if (!window.nostr) { 96 | console.log("you need to install an extension"); 97 | } 98 | // @ts-ignore 99 | let EventMessageSigned = await window.nostr.signEvent(eventMessage); 100 | if (EventMessageSigned.pubkey === posterPubkey) { 101 | let relayPool = new RelayPool(relays); 102 | 103 | relayPool.publish(EventMessageSigned, relays); 104 | } else { 105 | console.log("you are not allowed to reply status"); 106 | } 107 | } 108 | 109 | if (currentStatus === "in progress") { 110 | let eventMessage = { 111 | id: null, 112 | pubkey: null, 113 | created_at: Math.floor(Date.now() / 1000), 114 | kind: 1, 115 | tags: [ 116 | ["d", dTag], 117 | ["status", "paid"], 118 | ["t", "bounty-status"], 119 | ["a", `30023:${posterPubkey}:${dTag}`], 120 | ], 121 | content: `nostr:${bountyHunterNpub} got paid for completing this bounty: ${rootBountyUrl} from nostrbounties.com`, 122 | sig: null, 123 | }; 124 | // @ts-ignore 125 | if (!window.nostr) { 126 | console.log("you need to install an extension"); 127 | } 128 | // @ts-ignore 129 | let EventMessageSigned = await window.nostr.signEvent(eventMessage); 130 | if (EventMessageSigned.pubkey === posterPubkey) { 131 | let relayPool = new RelayPool(relays); 132 | 133 | relayPool.publish(EventMessageSigned, relays); 134 | } else { 135 | console.log("you are not allowed to reply status"); 136 | } 137 | } 138 | 139 | if (currentStatus === "paid") { 140 | let eventMessage = { 141 | id: null, 142 | pubkey: null, 143 | created_at: Math.floor(Date.now() / 1000), 144 | kind: 1, 145 | tags: [ 146 | ["d", dTag], 147 | ["status", "in progress"], 148 | ["t", "bounty-status"], 149 | ["a", `30023:${posterPubkey}:${dTag}`], 150 | ], 151 | content: `nostr:${bountyHunterNpub} was assigned to work on this bounty: ${rootBountyUrl} from nostrbounties.com`, 152 | sig: null, 153 | }; 154 | // @ts-ignore 155 | if (!window.nostr) { 156 | console.log("you need to install an extension"); 157 | } 158 | // @ts-ignore 159 | let EventMessageSigned = await window.nostr.signEvent(eventMessage); 160 | if (EventMessageSigned.pubkey === posterPubkey) { 161 | let relayPool = new RelayPool(relays); 162 | 163 | relayPool.publish(EventMessageSigned, relays); 164 | } else { 165 | console.log("you are not allowed to reply status"); 166 | } 167 | } 168 | } 169 | 170 | export async function addReward( 171 | amount: string, 172 | note: string, 173 | id: string, 174 | pubkey: string, 175 | dTag: string, 176 | naddr: string 177 | ) { 178 | let relays = allRelays; 179 | 180 | if (amount === "") { 181 | console.log("add a value"); 182 | } else { 183 | let eventNote: string; 184 | let rootBountyUrl = `https://nostrbounties.com/b/${naddr}`; 185 | if (note.length > 0) { 186 | eventNote = `I just added ${amount} sats to ${rootBountyUrl}! ${note}`; 187 | } else { 188 | eventNote = `I just added ${amount} sats to ${rootBountyUrl}!`; 189 | } 190 | 191 | let eventMessage = { 192 | id: null, 193 | pubkey: null, 194 | created_at: Math.floor(Date.now() / 1000), 195 | kind: 1, 196 | tags: [ 197 | ["t", "bounty-added-reward"], 198 | ["reward", `${amount}`], 199 | ["a", `30023:${pubkey}:${dTag}`], 200 | ["e", `${id}`, "", "root"], 201 | ], 202 | content: eventNote, 203 | sig: null, 204 | }; 205 | // @ts-ignore 206 | if (!window.nostr) { 207 | console.log("you need to install an extension"); 208 | } 209 | // @ts-ignore 210 | let EventMessageSigned = await window.nostr.signEvent(eventMessage); 211 | console.log(EventMessageSigned.content); 212 | 213 | let relayPool = new RelayPool(relays); 214 | 215 | relayPool.publish(EventMessageSigned, relays); 216 | console.log("posted"); 217 | } 218 | } 219 | 220 | export async function shareBounty(url: string) { 221 | try { 222 | await navigator.share({ 223 | url: url, 224 | }); 225 | } catch (error) { 226 | return true; 227 | } 228 | } 229 | 230 | export async function sendApplication( 231 | content: string, 232 | dTag: string, 233 | links: string[] 234 | ) { 235 | let relays = allRelays; 236 | 237 | if (content === "") { 238 | console.log("add a comment"); 239 | } else { 240 | let eventMessage = { 241 | id: null, 242 | pubkey: null, 243 | created_at: Math.floor(Date.now() / 1000), 244 | kind: 1, 245 | tags: [ 246 | ["d", dTag], 247 | ["t", "bounty-application"], 248 | ["github", links[0]], 249 | ["personalWeb", links[1]], 250 | ], 251 | content: content, 252 | sig: null, 253 | }; 254 | // @ts-ignore 255 | if (!window.nostr) { 256 | console.log("you need to install an extension"); 257 | } 258 | // @ts-ignore 259 | let EventMessageSigned = await window.nostr.signEvent(eventMessage); 260 | 261 | let relayPool = new RelayPool(relays); 262 | console.log("posted"); 263 | relayPool.publish(EventMessageSigned, defaultRelays); 264 | return EventMessageSigned; 265 | } 266 | } 267 | 268 | export function getNpub(pubkey: string) { 269 | let npub = nip19.npubEncode(pubkey); 270 | let arr_shortnpub = []; 271 | for (let i = 0; i < 12; i++) { 272 | arr_shortnpub.push(npub[i]); 273 | } 274 | let npubShortened = arr_shortnpub.join(""); 275 | return npubShortened + "..."; 276 | } 277 | 278 | export function formatReward(event: string | number) { 279 | if (typeof event === "string") { 280 | const rewardUnformatted = parseInt(event); 281 | const rewardFormatted = Intl.NumberFormat().format(rewardUnformatted); 282 | return rewardFormatted; 283 | } else { 284 | const rewardFormatted = Intl.NumberFormat().format(event); 285 | return rewardFormatted; 286 | } 287 | } 288 | 289 | export function isDarkTheme() { 290 | return ( 291 | window.matchMedia && 292 | window.matchMedia("(prefers-color-scheme: dark)").matches 293 | ); 294 | } 295 | 296 | export async function deleteEvent(id: string) { 297 | let relays = allRelays; 298 | 299 | let eventMessage = { 300 | id: null, 301 | pubkey: null, 302 | content: "", 303 | created_at: Math.floor(Date.now() / 1000), 304 | kind: 5, 305 | tags: [["e", `${id}`]], 306 | sig: null, 307 | }; 308 | // @ts-ignore 309 | let EventMessageSigned = await window.nostr.signEvent(eventMessage); 310 | let relayPool = new RelayPool(relays); 311 | relayPool.publish(EventMessageSigned, relays); 312 | return EventMessageSigned; 313 | } 314 | 315 | export async function getRelayData(relay: string) { 316 | let relayNoProtocol: string; 317 | let url: string; 318 | 319 | if (relay.startsWith("wss://")) { 320 | relayNoProtocol = relay.replace(/^.{6}/, ""); 321 | url = `https://${relayNoProtocol}`; 322 | } else { 323 | relayNoProtocol = relay.replace(/^.{5}/, ""); 324 | url = `http://${relayNoProtocol}`; 325 | } 326 | 327 | let data = await fetch(url, { 328 | method: "get", 329 | mode: "cors", 330 | headers: { 331 | Accept: "application/nostr+json", 332 | }, 333 | }); 334 | 335 | return data.json(); 336 | } 337 | 338 | export function getLNService(address: string) { 339 | let isLNUrl = address.toLowerCase().startsWith("lnurl"); 340 | let isDecodedAddress = address.includes("@"); 341 | 342 | if (isLNUrl) { 343 | let decoded = bech32.decode(address, 2000); 344 | let buf = bech32.fromWords(decoded.words); 345 | let decodedLNurl = new TextDecoder().decode(Uint8Array.from(buf)); 346 | 347 | let service = decodedLNurl.split("@"); 348 | let url = `https://${service[1]}/.well-known/lnurlp/${service[0]}`; 349 | 350 | let data = fetch(url).then((response) => { 351 | return response.json(); 352 | }); 353 | 354 | return data; 355 | } 356 | 357 | if (isDecodedAddress) { 358 | let service = address.split("@"); 359 | let url = `https://${service[1]}/.well-known/lnurlp/${service[0]}`; 360 | 361 | let data = fetch(url).then((response) => { 362 | return response.json(); 363 | }); 364 | 365 | return data; 366 | } 367 | } 368 | 369 | export async function getLNInvoice( 370 | zapEvent: any, 371 | comment: string, 372 | LNService: any, 373 | amount: string 374 | ) { 375 | let hasPubkey = LNService.nostrPubkey; 376 | function getLNURL(url: string) { 377 | let data = fetch(url); 378 | 379 | return data; 380 | } 381 | 382 | if (hasPubkey) { 383 | if (comment !== "") { 384 | let baseUrl = `${LNService.callback}?amount=${amount}&comment=${comment}&nostr=${zapEvent}`; 385 | let data = getLNURL(baseUrl); 386 | return data; 387 | } else { 388 | let baseUrl = `${LNService.callback}?amount=${amount}&nostr=${zapEvent}`; 389 | let data = getLNURL(baseUrl); 390 | return data; 391 | } 392 | } else { 393 | if (comment !== "") { 394 | let baseUrl = `${LNService.callback}?amount=${amount}&comment=${comment}`; 395 | let data = getLNURL(baseUrl); 396 | return data; 397 | } else { 398 | let baseUrl = `${LNService.callback}?amount=${amount}`; 399 | let data = getLNURL(baseUrl); 400 | return data; 401 | } 402 | } 403 | } 404 | 405 | export async function getZapEvent( 406 | content: string, 407 | bountyHunterPubkey: string, 408 | posterPubkey: string, 409 | amount: string, 410 | nadrr: string 411 | ) { 412 | let eventMessage = { 413 | id: null, 414 | pubkey: null, 415 | created_at: Math.floor(Date.now() / 1000), 416 | kind: 9734, 417 | tags: [ 418 | ["p", `${bountyHunterPubkey}`], 419 | ["a", `${nadrr}`], 420 | [ 421 | "relays", 422 | "wss://relay.damus.io", 423 | "wss://nos.lol", 424 | "wss://nostr-pub-wellorder.net/", 425 | "wss://nostr.pleb.network", 426 | ], 427 | ["amount", `${amount}`], 428 | ], 429 | content: content, 430 | sig: null, 431 | }; 432 | 433 | // @ts-ignore 434 | if (!window.nostr) { 435 | console.log("you need to install an extension"); 436 | } 437 | // @ts-ignore 438 | let EventMessageSigned = await window.nostr.signEvent(eventMessage); 439 | if (EventMessageSigned.pubkey === posterPubkey) { 440 | return EventMessageSigned; 441 | } else { 442 | console.log("you are not allowed to pay this bounty"); 443 | } 444 | } 445 | 446 | export function shortenedLNurl(element: string) { 447 | let arr_shortElementVersion = []; 448 | for (let i = 0; i < 26; i++) { 449 | arr_shortElementVersion.push(element[i]); 450 | } 451 | let elementShortened = arr_shortElementVersion.join(""); 452 | return elementShortened + "..."; 453 | } 454 | 455 | export async function getBTCPrice() { 456 | let price = fetch( 457 | "https://api.coinbase.com/v2/exchange-rates?currency=BTC" 458 | ).then((response) => { 459 | return response.json(); 460 | }); 461 | 462 | return price; 463 | } 464 | --------------------------------------------------------------------------------