├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/assets/close-icon-lg.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |

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 |

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 |

33 |
34 | ) : (
35 |
36 |

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 |
![copy user Npub]()
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 |
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 |

65 |
66 | {ev.reward} sats
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |

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 |

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 |

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 |

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 |
![external link]()
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 |

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 |
)
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 |
)
62 |
63 | Create
64 |
65 |
66 |
70 |
)
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 |
)
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 |
)
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 |

77 |
78 | ) : (
79 |
80 |

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 |

118 |
119 |
120 | ) : null}
121 | {links.personalWebsite !== "" ? (
122 |
123 |
127 |

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 |

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 |
179 |
180 |
181 | {shortenedLNurl(LNurl!)}
182 |
183 |
184 | {wasLNurlCopied ? (
185 |
186 | copied!
187 |
188 | ) : (
189 |
![copy user LNurl]()
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 |

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 |

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 |
--------------------------------------------------------------------------------