Merge pull request 'feat: submit mod' (#2) from mod-submit into master
Reviewed-on: #2
This commit is contained in:
commit
ca4164e507
1
.env.example
Normal file
1
.env.example
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_APP_RELAY=wss://relay.degmods.com
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,3 +22,5 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
506
package-lock.json
generated
506
package-lock.json
generated
@ -10,16 +10,26 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nostr-dev-kit/ndk": "2.8.2",
|
"@nostr-dev-kit/ndk": "2.8.2",
|
||||||
"@reduxjs/toolkit": "2.2.6",
|
"@reduxjs/toolkit": "2.2.6",
|
||||||
|
"lodash": "4.17.21",
|
||||||
"nostr-login": "1.5.2",
|
"nostr-login": "1.5.2",
|
||||||
"nostr-tools": "2.7.1",
|
"nostr-tools": "2.7.1",
|
||||||
|
"papaparse": "5.4.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-quill": "2.0.0",
|
||||||
"react-redux": "9.1.2",
|
"react-redux": "9.1.2",
|
||||||
"react-router-dom": "^6.24.1"
|
"react-router-dom": "^6.24.1",
|
||||||
|
"react-toastify": "10.0.5",
|
||||||
|
"react-window": "1.8.10",
|
||||||
|
"uuid": "10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/lodash": "4.17.7",
|
||||||
|
"@types/papaparse": "5.3.14",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/react-window": "1.8.8",
|
||||||
|
"@types/uuid": "10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||||
"@typescript-eslint/parser": "^7.13.1",
|
"@typescript-eslint/parser": "^7.13.1",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
@ -345,6 +355,17 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.24.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz",
|
||||||
|
"integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.24.7",
|
"version": "7.24.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
|
||||||
@ -1523,22 +1544,44 @@
|
|||||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
|
||||||
|
"integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.14.10",
|
"version": "20.14.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz",
|
||||||
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
|
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/papaparse": {
|
||||||
|
"version": "5.3.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.14.tgz",
|
||||||
|
"integrity": "sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.12",
|
"version": "15.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
|
||||||
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/quill": {
|
||||||
|
"version": "1.3.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
|
||||||
|
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
|
||||||
|
"dependencies": {
|
||||||
|
"parchment": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.3",
|
"version": "18.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
|
||||||
@ -1558,11 +1601,26 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-window": {
|
||||||
|
"version": "1.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz",
|
||||||
|
"integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/use-sync-external-store": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
|
||||||
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
|
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
|
||||||
@ -1960,6 +2018,24 @@
|
|||||||
"node": ">=6.14.2"
|
"node": ">=6.14.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"set-function-length": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@ -2048,6 +2124,22 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clone": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
@ -2137,12 +2229,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/deep-equal": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
|
||||||
|
"dependencies": {
|
||||||
|
"is-arguments": "^1.1.1",
|
||||||
|
"is-date-object": "^1.0.5",
|
||||||
|
"is-regex": "^1.1.4",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object-keys": "^1.1.1",
|
||||||
|
"regexp.prototype.flags": "^1.5.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deep-is": {
|
"node_modules/deep-is": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/define-data-property": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/define-properties": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.0",
|
||||||
|
"object-keys": "^1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
@ -2182,6 +2325,25 @@
|
|||||||
"integrity": "sha512-4h+oPeAiGQOHFyUJOqpoEcPj/xxlicxBzOErVeYVMMmAiXUXsGpsFd0QXBMaUUbnD8hhSfLf9uw+MlsoIA7j5w==",
|
"integrity": "sha512-4h+oPeAiGQOHFyUJOqpoEcPj/xxlicxBzOErVeYVMMmAiXUXsGpsFd0QXBMaUUbnD8hhSfLf9uw+MlsoIA7j5w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"get-intrinsic": "^1.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es5-ext": {
|
"node_modules/es5-ext": {
|
||||||
"version": "0.10.64",
|
"version": "0.10.64",
|
||||||
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
|
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
|
||||||
@ -2580,6 +2742,11 @@
|
|||||||
"es5-ext": "~0.10.14"
|
"es5-ext": "~0.10.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg=="
|
||||||
|
},
|
||||||
"node_modules/ext": {
|
"node_modules/ext": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz",
|
||||||
@ -2588,12 +2755,22 @@
|
|||||||
"type": "^2.7.2"
|
"type": "^2.7.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/extend": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-diff": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig=="
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||||
@ -2756,6 +2933,22 @@
|
|||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/functions-have-names": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@ -2765,6 +2958,24 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"has-proto": "^1.0.1",
|
||||||
|
"has-symbols": "^1.0.3",
|
||||||
|
"hasown": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
@ -2849,6 +3060,17 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
|
||||||
|
"dependencies": {
|
||||||
|
"get-intrinsic": "^1.1.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graphemer": {
|
"node_modules/graphemer": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||||
@ -2864,6 +3086,64 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-property-descriptors": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-define-property": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-proto": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
||||||
@ -2930,6 +3210,21 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/is-arguments": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-binary-path": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
@ -2942,6 +3237,20 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-date-object": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-tostringtag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@ -2981,6 +3290,21 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-regex": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"has-tostringtag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-typedarray": {
|
"node_modules/is-typedarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||||
@ -3107,6 +3431,11 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@ -3139,6 +3468,11 @@
|
|||||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/memoize-one": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@ -3416,6 +3750,29 @@
|
|||||||
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"define-properties": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-keys": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@ -3472,6 +3829,16 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/papaparse": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
|
||||||
|
},
|
||||||
|
"node_modules/parchment": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg=="
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@ -3623,6 +3990,32 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/quill": {
|
||||||
|
"version": "1.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
|
||||||
|
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
|
||||||
|
"dependencies": {
|
||||||
|
"clone": "^2.1.1",
|
||||||
|
"deep-equal": "^1.0.1",
|
||||||
|
"eventemitter3": "^2.0.3",
|
||||||
|
"extend": "^3.0.2",
|
||||||
|
"parchment": "^1.1.4",
|
||||||
|
"quill-delta": "^3.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/quill-delta": {
|
||||||
|
"version": "3.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
|
||||||
|
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
|
||||||
|
"dependencies": {
|
||||||
|
"deep-equal": "^1.0.1",
|
||||||
|
"extend": "^3.0.2",
|
||||||
|
"fast-diff": "1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
@ -3646,6 +4039,20 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-quill": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/quill": "^1.3.10",
|
||||||
|
"lodash": "^4.17.4",
|
||||||
|
"quill": "^1.3.7"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16 || ^17 || ^18",
|
||||||
|
"react-dom": "^16 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-redux": {
|
"node_modules/react-redux": {
|
||||||
"version": "9.1.2",
|
"version": "9.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
|
||||||
@ -3707,6 +4114,34 @@
|
|||||||
"react-dom": ">=16.8"
|
"react-dom": ">=16.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-toastify": {
|
||||||
|
"version": "10.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz",
|
||||||
|
"integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-window": {
|
||||||
|
"version": "1.8.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz",
|
||||||
|
"integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.0.0",
|
||||||
|
"memoize-one": ">=3.1.1 <6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@ -3732,6 +4167,28 @@
|
|||||||
"redux": "^5.0.0"
|
"redux": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||||
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||||
|
},
|
||||||
|
"node_modules/regexp.prototype.flags": {
|
||||||
|
"version": "1.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
|
||||||
|
"integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.6",
|
||||||
|
"define-properties": "^1.2.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"set-function-name": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reselect": {
|
"node_modules/reselect": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
@ -3867,6 +4324,36 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-function-length": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.1.4",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-intrinsic": "^1.2.4",
|
||||||
|
"gopd": "^1.0.1",
|
||||||
|
"has-property-descriptors": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/set-function-name": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"define-data-property": "^1.1.4",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"functions-have-names": "^1.2.3",
|
||||||
|
"has-property-descriptors": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/shebang-command": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
@ -4104,8 +4591,7 @@
|
|||||||
"version": "5.26.5",
|
"version": "5.26.5",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@ -4174,6 +4660,18 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
12
package.json
12
package.json
@ -12,16 +12,26 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nostr-dev-kit/ndk": "2.8.2",
|
"@nostr-dev-kit/ndk": "2.8.2",
|
||||||
"@reduxjs/toolkit": "2.2.6",
|
"@reduxjs/toolkit": "2.2.6",
|
||||||
|
"lodash": "4.17.21",
|
||||||
"nostr-login": "1.5.2",
|
"nostr-login": "1.5.2",
|
||||||
"nostr-tools": "2.7.1",
|
"nostr-tools": "2.7.1",
|
||||||
|
"papaparse": "5.4.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-quill": "2.0.0",
|
||||||
"react-redux": "9.1.2",
|
"react-redux": "9.1.2",
|
||||||
"react-router-dom": "^6.24.1"
|
"react-router-dom": "^6.24.1",
|
||||||
|
"react-toastify": "10.0.5",
|
||||||
|
"react-window": "1.8.10",
|
||||||
|
"uuid": "10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/lodash": "4.17.7",
|
||||||
|
"@types/papaparse": "5.3.14",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/react-window": "1.8.8",
|
||||||
|
"@types/uuid": "10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||||
"@typescript-eslint/parser": "^7.13.1",
|
"@typescript-eslint/parser": "^7.13.1",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
203171
public/assets/games.csv
Normal file
203171
public/assets/games.csv
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,22 +1,70 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactQuill from 'react-quill'
|
||||||
|
import 'react-quill/dist/quill.snow.css'
|
||||||
|
import '../styles/customQuillStyles.css'
|
||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
|
|
||||||
|
const editorFormats = [
|
||||||
|
'header',
|
||||||
|
'font',
|
||||||
|
'size',
|
||||||
|
'bold',
|
||||||
|
'italic',
|
||||||
|
'underline',
|
||||||
|
'strike',
|
||||||
|
'blockquote',
|
||||||
|
'list',
|
||||||
|
'bullet',
|
||||||
|
'indent',
|
||||||
|
'link'
|
||||||
|
]
|
||||||
|
|
||||||
|
const editorModules = {
|
||||||
|
toolbar: [
|
||||||
|
[{ header: '1' }, { header: '2' }, { font: [] }],
|
||||||
|
[{ size: [] }],
|
||||||
|
['bold', 'italic', 'underline', 'strike', 'blockquote'],
|
||||||
|
[
|
||||||
|
{ list: 'ordered' },
|
||||||
|
{ list: 'bullet' },
|
||||||
|
{ indent: '-1' },
|
||||||
|
{ indent: '+1' }
|
||||||
|
],
|
||||||
|
['link']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
interface InputFieldProps {
|
interface InputFieldProps {
|
||||||
label: string
|
label: string
|
||||||
description?: string
|
description?: string
|
||||||
type?: 'text' | 'textarea'
|
type?: 'text' | 'textarea' | 'richtext'
|
||||||
placeholder: string
|
placeholder: string
|
||||||
name: string
|
name: string
|
||||||
inputMode?: 'url'
|
inputMode?: 'url'
|
||||||
|
value: string
|
||||||
|
error?: string
|
||||||
|
onChange: (name: string, value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InputField = ({
|
export const InputField = React.memo(
|
||||||
|
({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
placeholder,
|
placeholder,
|
||||||
name,
|
name,
|
||||||
inputMode
|
inputMode,
|
||||||
}: InputFieldProps) => (
|
value,
|
||||||
|
error,
|
||||||
|
onChange
|
||||||
|
}: InputFieldProps) => {
|
||||||
|
const handleChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||||
|
) => {
|
||||||
|
onChange(name, e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<div className='inputLabelWrapperMain'>
|
<div className='inputLabelWrapperMain'>
|
||||||
<label className='form-label labelMain'>{label}</label>
|
<label className='form-label labelMain'>{label}</label>
|
||||||
{description && <p className='labelDescriptionMain'>{description}</p>}
|
{description && <p className='labelDescriptionMain'>{description}</p>}
|
||||||
@ -25,7 +73,18 @@ export const InputField = ({
|
|||||||
className='inputMain'
|
className='inputMain'
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
name={name}
|
name={name}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
></textarea>
|
></textarea>
|
||||||
|
) : type === 'richtext' ? (
|
||||||
|
<ReactQuill
|
||||||
|
className='inputMain'
|
||||||
|
formats={editorFormats}
|
||||||
|
modules={editorModules}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={(content) => onChange(name, content)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
@ -33,53 +92,49 @@ export const InputField = ({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
name={name}
|
name={name}
|
||||||
inputMode={inputMode}
|
inputMode={inputMode}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{error && <InputError message={error} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type InputErrorProps = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputError = ({ message }: InputErrorProps) => {
|
||||||
|
if (!message) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='errorMain'>
|
||||||
|
<div className='errorMainColor'></div>
|
||||||
|
<p className='errorMainText'>{message}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface CheckboxFieldProps {
|
interface CheckboxFieldProps {
|
||||||
label: string
|
label: string
|
||||||
name: string
|
name: string
|
||||||
|
isChecked: boolean
|
||||||
|
handleChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CheckboxField = ({ label, name }: CheckboxFieldProps) => (
|
export const CheckboxField = React.memo(
|
||||||
|
({ label, name, isChecked, handleChange }: CheckboxFieldProps) => (
|
||||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
|
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
|
||||||
<label className='form-label labelMain'>{label}</label>
|
<label className='form-label labelMain'>{label}</label>
|
||||||
<input type='checkbox' className='CheckboxMain' name={name} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
interface ImageUploadFieldProps {
|
|
||||||
label: string
|
|
||||||
description: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ImageUploadField = ({
|
|
||||||
label,
|
|
||||||
description
|
|
||||||
}: ImageUploadFieldProps) => (
|
|
||||||
<div className='inputLabelWrapperMain'>
|
|
||||||
<label className='form-label labelMain'>{label}</label>
|
|
||||||
{description && <p className='labelDescriptionMain'>{description}</p>}
|
|
||||||
<div className='inputWrapperMain'>
|
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='checkbox'
|
||||||
className='inputMain'
|
className='CheckboxMain'
|
||||||
inputMode='url'
|
name={name}
|
||||||
placeholder='We recommend to upload images to https://nostr.build/'
|
checked={isChecked}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<button className='btn btnMain btnMainRemove' type='button'>
|
|
||||||
<svg
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
viewBox='-32 0 512 512'
|
|
||||||
width='1em'
|
|
||||||
height='1em'
|
|
||||||
fill='currentColor'
|
|
||||||
>
|
|
||||||
<path d='M135.2 17.69C140.6 6.848 151.7 0 163.8 0H284.2C296.3 0 307.4 6.848 312.8 17.69L320 32H416C433.7 32 448 46.33 448 64C448 81.67 433.7 96 416 96H32C14.33 96 0 81.67 0 64C0 46.33 14.33 32 32 32H128L135.2 17.69zM394.8 466.1C393.2 492.3 372.3 512 346.9 512H101.1C75.75 512 54.77 492.3 53.19 466.1L31.1 128H416L394.8 466.1z'></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
@ -1,26 +1,394 @@
|
|||||||
|
import _ from 'lodash'
|
||||||
|
import { Event, kinds, UnsignedEvent } from 'nostr-tools'
|
||||||
|
import Papa from 'papaparse'
|
||||||
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { FixedSizeList as List } from 'react-window'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
import { useAppSelector } from '../hooks'
|
||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import { CheckboxField, ImageUploadField, InputField } from './Inputs'
|
import {
|
||||||
|
isReachable,
|
||||||
|
isValidImageUrl,
|
||||||
|
isValidUrl,
|
||||||
|
log,
|
||||||
|
LogType,
|
||||||
|
now
|
||||||
|
} from '../utils'
|
||||||
|
import { CheckboxField, InputError, InputField } from './Inputs'
|
||||||
|
import { RelayController } from '../controllers'
|
||||||
|
|
||||||
|
interface DownloadUrl {
|
||||||
|
url: string
|
||||||
|
hash: string
|
||||||
|
signatureKey: string
|
||||||
|
malwareScanLink: string
|
||||||
|
modVersion: string
|
||||||
|
customNote: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormState {
|
||||||
|
game: string
|
||||||
|
title: string
|
||||||
|
body: string
|
||||||
|
featuredImageUrl: string
|
||||||
|
summary: string
|
||||||
|
nsfw: boolean
|
||||||
|
screenshotsUrls: string[]
|
||||||
|
tags: string
|
||||||
|
downloadUrls: DownloadUrl[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormErrors {
|
||||||
|
game?: string
|
||||||
|
title?: string
|
||||||
|
body?: string
|
||||||
|
featuredImageUrl?: string
|
||||||
|
summary?: string
|
||||||
|
nsfw?: string
|
||||||
|
screenshotsUrls?: string[]
|
||||||
|
tags?: string
|
||||||
|
downloadUrls?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameOption {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedCSV = false
|
||||||
|
|
||||||
export const ModForm = () => {
|
export const ModForm = () => {
|
||||||
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
|
||||||
|
const [isPublishing, setIsPublishing] = useState(false)
|
||||||
|
const [gameOptions, setGameOptions] = useState<GameOption[]>([])
|
||||||
|
const [formState, setFormState] = useState<FormState>({
|
||||||
|
game: '',
|
||||||
|
title: '',
|
||||||
|
body: '',
|
||||||
|
featuredImageUrl: '',
|
||||||
|
summary: '',
|
||||||
|
nsfw: false,
|
||||||
|
screenshotsUrls: [''],
|
||||||
|
tags: '',
|
||||||
|
downloadUrls: [
|
||||||
|
{
|
||||||
|
url: '',
|
||||||
|
hash: '',
|
||||||
|
signatureKey: '',
|
||||||
|
malwareScanLink: '',
|
||||||
|
modVersion: '',
|
||||||
|
customNote: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const [formErrors, setFormErrors] = useState<FormErrors>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (processedCSV) return
|
||||||
|
processedCSV = true
|
||||||
|
|
||||||
|
// Fetch the CSV file from the public folder
|
||||||
|
fetch('/assets/games.csv')
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((csvText) => {
|
||||||
|
// Parse the CSV text using PapaParse
|
||||||
|
Papa.parse<{
|
||||||
|
'Game Name': string
|
||||||
|
'16 by 9 image': string
|
||||||
|
'Boxart image': string
|
||||||
|
}>(csvText, {
|
||||||
|
worker: true,
|
||||||
|
header: true,
|
||||||
|
complete: (results) => {
|
||||||
|
const options = results.data.map((row) => ({
|
||||||
|
label: row['Game Name'],
|
||||||
|
value: row['Game Name']
|
||||||
|
}))
|
||||||
|
setGameOptions(options)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => console.error('Error fetching CSV file:', error))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleInputChange = useCallback((name: string, value: string) => {
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: value
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleCheckboxChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, checked } = e.target
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: checked
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const addScreenshotUrl = useCallback(() => {
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
screenshotsUrls: [...prevState.screenshotsUrls, '']
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const removeScreenshotUrl = useCallback((index: number) => {
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
screenshotsUrls: prevState.screenshotsUrls.filter((_, i) => i !== index)
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleScreenshotUrlChange = useCallback(
|
||||||
|
(index: number, value: string) => {
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
screenshotsUrls: [
|
||||||
|
...prevState.screenshotsUrls.map((url, i) => {
|
||||||
|
if (index === i) return value
|
||||||
|
return url
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const addDownloadUrl = useCallback(() => {
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
downloadUrls: [
|
||||||
|
...prevState.downloadUrls,
|
||||||
|
{
|
||||||
|
url: '',
|
||||||
|
hash: '',
|
||||||
|
signatureKey: '',
|
||||||
|
malwareScanLink: '',
|
||||||
|
modVersion: '',
|
||||||
|
customNote: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const removeDownloadUrl = useCallback((index: number) => {
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
downloadUrls: prevState.downloadUrls.filter((_, i) => i !== index)
|
||||||
|
}))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDownloadUrlChange = useCallback(
|
||||||
|
(index: number, field: keyof DownloadUrl, value: string) => {
|
||||||
|
setFormState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
downloadUrls: [
|
||||||
|
...prevState.downloadUrls.map((url, i) => {
|
||||||
|
if (index === i) url[field] = value
|
||||||
|
return url
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePublish = async () => {
|
||||||
|
setIsPublishing(true)
|
||||||
|
|
||||||
|
let hexPubkey: string
|
||||||
|
|
||||||
|
if (userState.isAuth && userState.user?.pubkey) {
|
||||||
|
hexPubkey = userState.user.pubkey as string
|
||||||
|
} else {
|
||||||
|
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hexPubkey) {
|
||||||
|
toast.error('Could not get pubkey')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await validateState())) {
|
||||||
|
setIsPublishing(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuid = uuidv4()
|
||||||
|
const currentTimeStamp = now()
|
||||||
|
|
||||||
|
const unsignedEvent: UnsignedEvent = {
|
||||||
|
kind: kinds.ClassifiedListing,
|
||||||
|
created_at: currentTimeStamp,
|
||||||
|
pubkey: hexPubkey,
|
||||||
|
content: formState.body,
|
||||||
|
tags: [
|
||||||
|
['d', uuid],
|
||||||
|
['t', window.location.host],
|
||||||
|
['published_at', currentTimeStamp.toString()],
|
||||||
|
['game', formState.game],
|
||||||
|
['title', formState.title],
|
||||||
|
['featuredImageUrl', formState.featuredImageUrl],
|
||||||
|
['summary', formState.summary],
|
||||||
|
['nsfw', formState.nsfw.toString()],
|
||||||
|
['screenshotsUrls', ...formState.screenshotsUrls],
|
||||||
|
['tags', ...formState.tags.split(',')],
|
||||||
|
[
|
||||||
|
'downloadUrls',
|
||||||
|
...formState.downloadUrls.map((downloadUrl) =>
|
||||||
|
JSON.stringify(downloadUrl)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedEvent = await window.nostr
|
||||||
|
?.signEvent(unsignedEvent)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed to sign the event!')
|
||||||
|
log(true, LogType.Error, 'Failed to sign the event!', err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!signedEvent) {
|
||||||
|
setIsPublishing(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishedOnRelays = await RelayController.getInstance().publish(
|
||||||
|
signedEvent as Event
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('publishedOnRelays :>> ', publishedOnRelays)
|
||||||
|
|
||||||
|
if (!publishedOnRelays) {
|
||||||
|
toast.error('Failed to publish event!')
|
||||||
|
setIsPublishing(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle cases where publishing failed or succeeded
|
||||||
|
if (publishedOnRelays.length === 0) {
|
||||||
|
toast.error('Failed to publish event on any relay')
|
||||||
|
} else {
|
||||||
|
toast.success(
|
||||||
|
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
|
||||||
|
'\n'
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPublishing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateState = async (): Promise<boolean> => {
|
||||||
|
const errors: FormErrors = {}
|
||||||
|
|
||||||
|
if (formState.game === '') {
|
||||||
|
errors.game = 'Game field can not be empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formState.title === '') {
|
||||||
|
errors.title = 'Title field can not be empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formState.body === '') {
|
||||||
|
errors.body = 'Body field can not be empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formState.featuredImageUrl === '') {
|
||||||
|
errors.featuredImageUrl = 'FeaturedImageUrl field can not be empty'
|
||||||
|
} else if (
|
||||||
|
!isValidImageUrl(formState.featuredImageUrl) ||
|
||||||
|
!(await isReachable(formState.featuredImageUrl))
|
||||||
|
) {
|
||||||
|
errors.featuredImageUrl =
|
||||||
|
'FeaturedImageUrl must be a valid and reachable image URL'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formState.summary === '') {
|
||||||
|
errors.summary = 'Summary field can not be empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formState.screenshotsUrls.length === 0) {
|
||||||
|
errors.screenshotsUrls = ['Required at least one screenshot url']
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < formState.screenshotsUrls.length; i++) {
|
||||||
|
const url = formState.screenshotsUrls[i]
|
||||||
|
if (
|
||||||
|
!isValidUrl(url) ||
|
||||||
|
!isValidImageUrl(url) ||
|
||||||
|
!(await isReachable(url))
|
||||||
|
) {
|
||||||
|
if (!errors.screenshotsUrls)
|
||||||
|
errors.screenshotsUrls = Array(formState.screenshotsUrls.length)
|
||||||
|
|
||||||
|
errors.screenshotsUrls![i] =
|
||||||
|
'All screenshot URLs must be valid and reachable image URLs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formState.tags === '') {
|
||||||
|
errors.tags = 'Tags field can not be empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formState.downloadUrls.length === 0) {
|
||||||
|
errors.downloadUrls = ['Required at least one download url']
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < formState.downloadUrls.length; i++) {
|
||||||
|
const downloadUrl = formState.downloadUrls[i]
|
||||||
|
if (
|
||||||
|
!isValidUrl(downloadUrl.url) ||
|
||||||
|
!(await isReachable(downloadUrl.url))
|
||||||
|
) {
|
||||||
|
if (!errors.downloadUrls)
|
||||||
|
errors.downloadUrls = Array(formState.downloadUrls.length)
|
||||||
|
|
||||||
|
errors.downloadUrls![i] = 'Download url must be valid and reachable'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormErrors(errors)
|
||||||
|
|
||||||
|
return Object.keys(errors).length === 0
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InputField
|
<GameDropdown
|
||||||
label='Game'
|
options={gameOptions}
|
||||||
description="Can't find the game you're looking for? Send us a DM mentioning it so we can add it."
|
selected={formState.game}
|
||||||
placeholder='The mod is for a game called...'
|
error={formErrors.game}
|
||||||
name='game'
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputField
|
<InputField
|
||||||
label='Title'
|
label='Title'
|
||||||
placeholder='Return the banana mod'
|
placeholder='Return the banana mod'
|
||||||
name='title'
|
name='title'
|
||||||
|
value={formState.title}
|
||||||
|
error={formErrors.title}
|
||||||
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputField
|
<InputField
|
||||||
label='Body'
|
label='Body'
|
||||||
type='textarea'
|
type='richtext'
|
||||||
placeholder="Here's what this mod is all about"
|
placeholder="Here's what this mod is all about"
|
||||||
name='body'
|
name='body'
|
||||||
|
value={formState.body}
|
||||||
|
error={formErrors.body}
|
||||||
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputField
|
<InputField
|
||||||
label='Featured Image URL'
|
label='Featured Image URL'
|
||||||
description='We recommend to upload images to https://nostr.build/'
|
description='We recommend to upload images to https://nostr.build/'
|
||||||
@ -28,29 +396,85 @@ export const ModForm = () => {
|
|||||||
inputMode='url'
|
inputMode='url'
|
||||||
placeholder='Image URL'
|
placeholder='Image URL'
|
||||||
name='featuredImageUrl'
|
name='featuredImageUrl'
|
||||||
|
value={formState.featuredImageUrl}
|
||||||
|
error={formErrors.featuredImageUrl}
|
||||||
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
label='Summary'
|
label='Summary'
|
||||||
type='textarea'
|
type='textarea'
|
||||||
placeholder='This is a quick description of my mod'
|
placeholder='This is a quick description of my mod'
|
||||||
name='summary'
|
name='summary'
|
||||||
|
value={formState.summary}
|
||||||
|
error={formErrors.summary}
|
||||||
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
<CheckboxField label='This mod not safe for work (NSFW)' name='nsfw' />
|
<CheckboxField
|
||||||
<ImageUploadField
|
label='This mod not safe for work (NSFW)'
|
||||||
label='Screenshots URLs'
|
name='nsfw'
|
||||||
description='We recommend to upload images to https://nostr.build/'
|
isChecked={formState.nsfw}
|
||||||
|
handleChange={handleCheckboxChange}
|
||||||
/>
|
/>
|
||||||
|
<div className='inputLabelWrapperMain'>
|
||||||
|
<div className='labelWrapperMain'>
|
||||||
|
<label className='form-label labelMain'>Screenshots URLs</label>
|
||||||
|
<button
|
||||||
|
className='btn btnMain btnMainAdd'
|
||||||
|
type='button'
|
||||||
|
onClick={addScreenshotUrl}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-32 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M432 256c0 17.69-14.33 32.01-32 32.01H256v144c0 17.69-14.33 31.99-32 31.99s-32-14.3-32-31.99v-144H48c-17.67 0-32-14.32-32-32.01s14.33-31.99 32-31.99H192v-144c0-17.69 14.33-32.01 32-32.01s32 14.32 32 32.01v144h144C417.7 224 432 238.3 432 256z'></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className='labelDescriptionMain'>
|
||||||
|
We recommend to upload images to https://nostr.build/
|
||||||
|
</p>
|
||||||
|
{formState.screenshotsUrls.map((url, index) => (
|
||||||
|
<>
|
||||||
|
<ScreenshotUrlFields
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
url={url}
|
||||||
|
onUrlChange={handleScreenshotUrlChange}
|
||||||
|
onRemove={removeScreenshotUrl}
|
||||||
|
/>
|
||||||
|
{formErrors.screenshotsUrls &&
|
||||||
|
formErrors.screenshotsUrls[index] && (
|
||||||
|
<InputError message={formErrors.screenshotsUrls[index]} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
{formState.screenshotsUrls.length === 0 &&
|
||||||
|
formErrors.screenshotsUrls &&
|
||||||
|
formErrors.screenshotsUrls[0] && (
|
||||||
|
<InputError message={formErrors.screenshotsUrls[0]} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<InputField
|
<InputField
|
||||||
label='Tags'
|
label='Tags'
|
||||||
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
|
description='Separate each tag with a comma. (Example: tag1, tag2, tag3)'
|
||||||
placeholder='Tags'
|
placeholder='Tags'
|
||||||
name='tags'
|
name='tags'
|
||||||
|
value={formState.tags}
|
||||||
|
error={formErrors.tags}
|
||||||
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='inputLabelWrapperMain'>
|
<div className='inputLabelWrapperMain'>
|
||||||
<div className='labelWrapperMain'>
|
<div className='labelWrapperMain'>
|
||||||
<label className='form-label labelMain'>Download URLs</label>
|
<label className='form-label labelMain'>Download URLs</label>
|
||||||
<button className='btn btnMain btnMainAdd' type='button'>
|
<button
|
||||||
|
className='btn btnMain btnMainAdd'
|
||||||
|
type='button'
|
||||||
|
onClick={addDownloadUrl}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
viewBox='-32 0 512 512'
|
viewBox='-32 0 512 512'
|
||||||
@ -64,29 +488,95 @@ export const ModForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
<p className='labelDescriptionMain'>
|
<p className='labelDescriptionMain'>
|
||||||
You can upload your game mod to Github, as an example, and keep
|
You can upload your game mod to Github, as an example, and keep
|
||||||
updating it there. Also, its advisable that you hash your package as
|
updating it there. Also, it's advisable that you hash your package as
|
||||||
well with your nostr public key.
|
well with your nostr public key.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<DownloadUrlFields />
|
{formState.downloadUrls.map((download, index) => (
|
||||||
<DownloadUrlFields />
|
<>
|
||||||
|
<DownloadUrlFields
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
url={download.url}
|
||||||
|
hash={download.hash}
|
||||||
|
signatureKey={download.signatureKey}
|
||||||
|
malwareScanLink={download.malwareScanLink}
|
||||||
|
modVersion={download.modVersion}
|
||||||
|
customNote={download.customNote}
|
||||||
|
onUrlChange={handleDownloadUrlChange}
|
||||||
|
onRemove={removeDownloadUrl}
|
||||||
|
/>
|
||||||
|
{formErrors.downloadUrls && formErrors.downloadUrls[index] && (
|
||||||
|
<InputError message={formErrors.downloadUrls[index]} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{formState.downloadUrls.length === 0 &&
|
||||||
|
formErrors.downloadUrls &&
|
||||||
|
formErrors.downloadUrls[0] && (
|
||||||
|
<InputError message={formErrors.downloadUrls[0]} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='IBMSMSMBS_WriteAction'>
|
||||||
|
<button
|
||||||
|
className='btn btnMain'
|
||||||
|
type='button'
|
||||||
|
onClick={handlePublish}
|
||||||
|
disabled={isPublishing}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
type DownloadUrlFieldsProps = {
|
||||||
|
index: number
|
||||||
|
url: string
|
||||||
|
hash: string
|
||||||
|
signatureKey: string
|
||||||
|
malwareScanLink: string
|
||||||
|
modVersion: string
|
||||||
|
customNote: string
|
||||||
|
onUrlChange: (index: number, field: keyof DownloadUrl, value: string) => void
|
||||||
|
onRemove: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DownloadUrlFields = React.memo(
|
||||||
|
({
|
||||||
|
index,
|
||||||
|
url,
|
||||||
|
hash,
|
||||||
|
signatureKey,
|
||||||
|
malwareScanLink,
|
||||||
|
modVersion,
|
||||||
|
customNote,
|
||||||
|
onUrlChange,
|
||||||
|
onRemove
|
||||||
|
}: DownloadUrlFieldsProps) => {
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
onUrlChange(index, name as keyof DownloadUrl, value)
|
||||||
|
}
|
||||||
|
|
||||||
const DownloadUrlFields = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className='inputWrapperMainWrapper'>
|
<div className='inputWrapperMainWrapper'>
|
||||||
<div className='inputWrapperMain'>
|
<div className='inputWrapperMain'>
|
||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
className='inputMain'
|
className='inputMain'
|
||||||
inputMode='url'
|
name='url'
|
||||||
placeholder='https://...'
|
placeholder='Download URL'
|
||||||
value='https://github.com/'
|
value={url}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<button className='btn btnMain btnMainRemove' type='button'>
|
|
||||||
|
<button
|
||||||
|
className='btn btnMain btnMainRemove'
|
||||||
|
type='button'
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
viewBox='-32 0 512 512'
|
viewBox='-32 0 512 512'
|
||||||
@ -113,8 +603,10 @@ const DownloadUrlFields = () => {
|
|||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
className='inputMain'
|
className='inputMain'
|
||||||
inputMode='url'
|
name='hash'
|
||||||
placeholder='SHA-256 Hash'
|
placeholder='SHA-256 Hash'
|
||||||
|
value={hash}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<div className='inputWrapperMainBox'></div>
|
<div className='inputWrapperMainBox'></div>
|
||||||
</div>
|
</div>
|
||||||
@ -133,9 +625,10 @@ const DownloadUrlFields = () => {
|
|||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
className='inputMain'
|
className='inputMain'
|
||||||
inputMode='url'
|
|
||||||
placeholder='Signature public key'
|
placeholder='Signature public key'
|
||||||
value='npub18n4ysp43ux5c98fs6h9c57qpr4p8r3j8f6e32v0vj8egzy878aqqyzzk9r'
|
name='signatureKey'
|
||||||
|
value={signatureKey}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<div className='inputWrapperMainBox'></div>
|
<div className='inputWrapperMainBox'></div>
|
||||||
</div>
|
</div>
|
||||||
@ -154,8 +647,10 @@ const DownloadUrlFields = () => {
|
|||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
className='inputMain'
|
className='inputMain'
|
||||||
inputMode='url'
|
name='malwareScanLink'
|
||||||
placeholder='Malware scan link'
|
placeholder='Malware Scan Link'
|
||||||
|
value={malwareScanLink}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<div className='inputWrapperMainBox'></div>
|
<div className='inputWrapperMainBox'></div>
|
||||||
</div>
|
</div>
|
||||||
@ -174,8 +669,10 @@ const DownloadUrlFields = () => {
|
|||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
className='inputMain'
|
className='inputMain'
|
||||||
inputMode='url'
|
|
||||||
placeholder='Mod version (1.0)'
|
placeholder='Mod version (1.0)'
|
||||||
|
name='modVersion'
|
||||||
|
value={modVersion}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<div className='inputWrapperMainBox'></div>
|
<div className='inputWrapperMainBox'></div>
|
||||||
</div>
|
</div>
|
||||||
@ -194,11 +691,163 @@ const DownloadUrlFields = () => {
|
|||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
className='inputMain'
|
className='inputMain'
|
||||||
inputMode='url'
|
placeholder='Custom note/message'
|
||||||
placeholder='Custome note/message'
|
name='customNote'
|
||||||
|
value={customNote}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
<div className='inputWrapperMainBox'></div>
|
<div className='inputWrapperMainBox'></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ScreenshotUrlFieldsProps = {
|
||||||
|
index: number
|
||||||
|
url: string
|
||||||
|
onUrlChange: (index: number, value: string) => void
|
||||||
|
onRemove: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScreenshotUrlFields = React.memo(
|
||||||
|
({ index, url, onUrlChange, onRemove }: ScreenshotUrlFieldsProps) => {
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onUrlChange(index, e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='inputWrapperMain'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='inputMain'
|
||||||
|
inputMode='url'
|
||||||
|
placeholder='We recommend to upload images to https://nostr.build/'
|
||||||
|
value={url}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='btn btnMain btnMainRemove'
|
||||||
|
type='button'
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-32 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M323.3 32.01H188.7C172.3 32.01 160 44.31 160 60.73V96.01H32C14.33 96.01 0 110.3 0 128S14.33 160 32 160H480c17.67 0 32-14.33 32-32.01S497.7 96.01 480 96.01H352v-35.28C352 44.31 339.7 32.01 323.3 32.01zM64.9 477.5C66.5 492.3 79.31 504 94.72 504H417.3c15.41 0 28.22-11.72 29.81-26.5L480 192.2H32L64.9 477.5z'></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type GameDropdownProps = {
|
||||||
|
options: GameOption[]
|
||||||
|
selected: string
|
||||||
|
error?: string
|
||||||
|
onChange: (name: string, value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameDropdown = ({
|
||||||
|
options,
|
||||||
|
selected,
|
||||||
|
error,
|
||||||
|
onChange
|
||||||
|
}: GameDropdownProps) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('')
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const isOptionClicked = useRef(false)
|
||||||
|
|
||||||
|
const handleSearchChange = useCallback((value: string) => {
|
||||||
|
setSearchTerm(value)
|
||||||
|
_.debounce(() => {
|
||||||
|
setDebouncedSearchTerm(value)
|
||||||
|
}, 300)()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const filteredOptions = useMemo(() => {
|
||||||
|
if (debouncedSearchTerm === '') return []
|
||||||
|
else {
|
||||||
|
return options.filter((option) =>
|
||||||
|
option.label.toLowerCase().includes(debouncedSearchTerm.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [debouncedSearchTerm, options])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='inputLabelWrapperMain'>
|
||||||
|
<label className='form-label labelMain'>Game</label>
|
||||||
|
<p className='labelDescriptionMain'>
|
||||||
|
Can't find the game you're looking for? Send us a DM mentioning it so we
|
||||||
|
can add it.
|
||||||
|
</p>
|
||||||
|
<div className='dropdown dropdownMain'>
|
||||||
|
<div className='inputWrapperMain'>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type='text'
|
||||||
|
className='inputMain inputMainWithBtn dropdown-toggle'
|
||||||
|
placeholder='This mod is for a game called...'
|
||||||
|
aria-expanded='false'
|
||||||
|
data-bs-toggle='dropdown'
|
||||||
|
value={searchTerm || selected}
|
||||||
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
|
onBlur={() => {
|
||||||
|
if (!isOptionClicked.current) {
|
||||||
|
setSearchTerm('')
|
||||||
|
}
|
||||||
|
isOptionClicked.current = false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className='btn btnMain btnMainInsideField btnMainRemove'
|
||||||
|
type='button'
|
||||||
|
onClick={() => onChange('game', '')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z'></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className='dropdown-menu dropdownMainMenu'>
|
||||||
|
<List
|
||||||
|
height={500}
|
||||||
|
width={'100%'}
|
||||||
|
itemCount={filteredOptions.length}
|
||||||
|
itemSize={35}
|
||||||
|
>
|
||||||
|
{({ index, style }) => (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className='dropdown-item dropdownMainMenuItem'
|
||||||
|
onMouseDown={() => (isOptionClicked.current = true)}
|
||||||
|
onClick={() => {
|
||||||
|
onChange('game', filteredOptions[index].value)
|
||||||
|
setSearchTerm('')
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.blur()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredOptions[index].label}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && <InputError message={error} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from './metadata'
|
export * from './metadata'
|
||||||
|
export * from './relay'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import NDK, { NDKUser } from '@nostr-dev-kit/ndk'
|
import NDK, { NDKRelayList, NDKUser } from '@nostr-dev-kit/ndk'
|
||||||
import { UserProfile } from '../types/user'
|
import { UserProfile } from '../types/user'
|
||||||
import { hexToNpub } from '../utils'
|
import { hexToNpub } from '../utils'
|
||||||
|
|
||||||
@ -41,4 +41,14 @@ export class MetadataController {
|
|||||||
|
|
||||||
return await user.fetchProfile()
|
return await user.fetchProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findWriteRelays = async (hexKey: string) => {
|
||||||
|
const ndkRelayList = await NDKRelayList.forUser(hexKey, this.profileNdk)
|
||||||
|
|
||||||
|
if (!ndkRelayList) {
|
||||||
|
throw new Error(`Couldn't found user's relay list`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ndkRelayList.writeRelayUrls
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
121
src/controllers/relay.ts
Normal file
121
src/controllers/relay.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { Relay, Event } from 'nostr-tools'
|
||||||
|
import { log, LogType, timeout } from '../utils'
|
||||||
|
import { MetadataController } from './metadata'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton class to manage relay operations.
|
||||||
|
*/
|
||||||
|
export class RelayController {
|
||||||
|
private static instance: RelayController
|
||||||
|
private debug = true
|
||||||
|
public connectedRelays: Relay[] = []
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
private connectRelay = async (relayUrl: string) => {
|
||||||
|
const relay = this.connectedRelays.find(
|
||||||
|
(relay) => _.trimEnd(relay.url, '/') === _.trimEnd(relayUrl, '/')
|
||||||
|
)
|
||||||
|
if (relay) {
|
||||||
|
// already connected, skip
|
||||||
|
return relay
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Relay.connect(relayUrl)
|
||||||
|
.then((relay) => {
|
||||||
|
log(this.debug, LogType.Info, `✅ nostr (${relayUrl}): Connected!`)
|
||||||
|
this.connectedRelays.push(relay)
|
||||||
|
return relay
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log(
|
||||||
|
this.debug,
|
||||||
|
LogType.Error,
|
||||||
|
`❌ nostr (${relayUrl}): Connection error!`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the singleton instance of RelayController.
|
||||||
|
*
|
||||||
|
* @returns The singleton instance of RelayController.
|
||||||
|
*/
|
||||||
|
public static getInstance(): RelayController {
|
||||||
|
if (!RelayController.instance) {
|
||||||
|
RelayController.instance = new RelayController()
|
||||||
|
}
|
||||||
|
return RelayController.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
publish = async (event: Event) => {
|
||||||
|
const appRelayPromise = this.connectRelay(import.meta.env.VITE_APP_RELAY)
|
||||||
|
|
||||||
|
// todo: window.nostr.getRelays() is not implemented yet in nostr-login, implement the logic once its done
|
||||||
|
|
||||||
|
const writeRelaysPromise = MetadataController.getInstance().findWriteRelays(
|
||||||
|
event.pubkey
|
||||||
|
)
|
||||||
|
|
||||||
|
log(this.debug, LogType.Info, `Finding user's write relays`)
|
||||||
|
const writeRelayUrls = await Promise.race([
|
||||||
|
writeRelaysPromise,
|
||||||
|
timeout()
|
||||||
|
]).catch((err) => {
|
||||||
|
log(this.debug, LogType.Error, err)
|
||||||
|
return [] as string[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const relayPromises = writeRelayUrls.map((relayUrl) =>
|
||||||
|
this.connectRelay(relayUrl)
|
||||||
|
)
|
||||||
|
|
||||||
|
await Promise.allSettled([appRelayPromise, ...relayPromises])
|
||||||
|
|
||||||
|
if (this.connectedRelays.length === 0) {
|
||||||
|
log(this.debug, LogType.Error, 'No relay is connected!')
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const publishedOnRelays: string[] = []
|
||||||
|
|
||||||
|
const publishPromises = this.connectedRelays.map((relay) => {
|
||||||
|
log(
|
||||||
|
this.debug,
|
||||||
|
LogType.Info,
|
||||||
|
`⬆️ nostr (${relay.url}): Sending event:`,
|
||||||
|
event
|
||||||
|
)
|
||||||
|
|
||||||
|
return Promise.race([relay.publish(event), timeout(30000)])
|
||||||
|
.then((res) => {
|
||||||
|
log(
|
||||||
|
this.debug,
|
||||||
|
LogType.Info,
|
||||||
|
`⬆️ nostr (${relay.url}): Publish result:`,
|
||||||
|
res
|
||||||
|
)
|
||||||
|
|
||||||
|
publishedOnRelays.push(relay.url)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log(
|
||||||
|
this.debug,
|
||||||
|
LogType.Error,
|
||||||
|
`❌ nostr (${relay.url}): Publish error!`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.allSettled(publishPromises)
|
||||||
|
|
||||||
|
console.log('publishedOnRelays :>> ', publishedOnRelays)
|
||||||
|
|
||||||
|
return publishedOnRelays
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@ import React from 'react'
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { ToastContainer } from 'react-toastify'
|
||||||
|
import 'react-toastify/dist/ReactToastify.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import { store } from './store/index.ts'
|
import { store } from './store/index.ts'
|
||||||
@ -11,6 +13,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App />
|
<App />
|
||||||
|
<ToastContainer />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
@ -16,11 +16,6 @@ export const SubmitModPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className='IBMSMSMBS_Write'>
|
<div className='IBMSMSMBS_Write'>
|
||||||
<ModForm />
|
<ModForm />
|
||||||
<div className='IBMSMSMBS_WriteAction'>
|
|
||||||
<button className='btn btnMain' type='button'>
|
|
||||||
Publish
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ProfileSection />
|
<ProfileSection />
|
||||||
|
@ -7,7 +7,7 @@ export interface IUserState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialState: IUserState = {
|
const initialState: IUserState = {
|
||||||
isAuth: localStorage.getItem('login') ? true : false,
|
isAuth: false,
|
||||||
user: {}
|
user: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
13
src/styles/customQuillStyles.css
Normal file
13
src/styles/customQuillStyles.css
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
.quill .ql-toolbar.ql-snow {
|
||||||
|
border: none;
|
||||||
|
border-bottom: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quill .ql-container.ql-snow {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quill .ql-editor.ql-blank::before {
|
||||||
|
color: #757575;
|
||||||
|
font-size: medium;
|
||||||
|
}
|
@ -291,6 +291,7 @@ h6 {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputWrapperMainWrapper {
|
.inputWrapperMainWrapper {
|
||||||
@ -513,3 +514,39 @@ a:hover {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btnMain.btnMainInsideField:hover {
|
||||||
|
background: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnMain.btnMainInsideField {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputMain.inputMainWithBtn {
|
||||||
|
padding-right: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMain {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
grid-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMainColor {
|
||||||
|
width: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: tomato;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMainText {
|
||||||
|
}
|
||||||
|
@ -1 +1,3 @@
|
|||||||
export * from './nostr'
|
export * from './nostr'
|
||||||
|
export * from './url'
|
||||||
|
export * from './utils'
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current time in seconds since the Unix epoch (January 1, 1970).
|
||||||
|
*
|
||||||
|
* This function retrieves the current time in milliseconds since the Unix epoch
|
||||||
|
* using `Date.now()` and then converts it to seconds by dividing by 1000.
|
||||||
|
* Finally, it rounds the result to the nearest whole number using `Math.round()`.
|
||||||
|
*
|
||||||
|
* @returns {number} The current time in seconds since the Unix epoch.
|
||||||
|
*/
|
||||||
|
export const now = () => Math.round(Date.now() / 1000)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a hexadecimal public key to an npub format.
|
* Converts a hexadecimal public key to an npub format.
|
||||||
* If the input is already in npub format, it returns the input unchanged.
|
* If the input is already in npub format, it returns the input unchanged.
|
||||||
|
22
src/utils/url.ts
Normal file
22
src/utils/url.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export const isValidUrl = (url: string) => {
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
return true
|
||||||
|
} catch (_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isValidImageUrl = (url: string) => {
|
||||||
|
const regex = /\.(jpeg|jpg|gif|png)$/
|
||||||
|
return regex.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isReachable = async (url: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { method: 'HEAD' })
|
||||||
|
return response.ok
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
36
src/utils/utils.ts
Normal file
36
src/utils/utils.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export enum LogType {
|
||||||
|
Info = 'info',
|
||||||
|
Error = 'error',
|
||||||
|
Warn = 'warn'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log function to conditionally log messages to the console
|
||||||
|
*
|
||||||
|
* @param isOn boolean or undefined indicating if logging is enabled
|
||||||
|
* @param type LogType indicating the type of log (info, error, warn)
|
||||||
|
* @param args unknown[] represents the rest parameters for log messages
|
||||||
|
*/
|
||||||
|
export const log = (
|
||||||
|
isOn: boolean | undefined, // Flag to determine if logging is enabled
|
||||||
|
type: LogType, // Type of log (info, error, warn)
|
||||||
|
...args: unknown[] // Log messages to be printed
|
||||||
|
) => {
|
||||||
|
if (!isOn) return // If logging is not enabled, return early
|
||||||
|
console[type](...args) // Log the messages to the console with the specified log type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a promise that rejects with a timeout error after a specified duration.
|
||||||
|
* @param ms The duration in milliseconds after which the promise should reject. Defaults to 60000 milliseconds (1 minute).
|
||||||
|
* @returns A promise that rejects with an Error('Timeout') after the specified duration.
|
||||||
|
*/
|
||||||
|
export const timeout = (ms: number = 60000) => {
|
||||||
|
return new Promise<never>((_, reject) => {
|
||||||
|
// Set a timeout using setTimeout
|
||||||
|
setTimeout(() => {
|
||||||
|
// Reject the promise with an Error indicating a timeout
|
||||||
|
reject(new Error('Timeout'))
|
||||||
|
}, ms) // Timeout duration in milliseconds
|
||||||
|
})
|
||||||
|
}
|
9
src/vite-env.d.ts
vendored
9
src/vite-env.d.ts
vendored
@ -1 +1,10 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APP_RELAY: string
|
||||||
|
// more env variables...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user