diff --git a/.gitignore b/.gitignore index 1f28034..e935ebe 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ .env build storybook-static -environments/.dev.preview-server.env +envs/client/.dev.preview-server.env +envs/server # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies diff --git a/api/auth/services/lnurlAuth.service.js b/api/auth/services/lnurlAuth.service.js index b6bda5a..1dcb91c 100644 --- a/api/auth/services/lnurlAuth.service.js +++ b/api/auth/services/lnurlAuth.service.js @@ -56,12 +56,17 @@ function removeExpiredHashes() { }) } -async function generateAuthUrl() { - const hostname = 'https://auth.bolt.fun/.netlify/functions/login'; + + +async function generateAuthUrl(options) { + const hostname = CONSTS.LNURL_AUTH_HOST ?? 'https://auth.bolt.fun/.netlify/functions/login'; const secret = await generateK1(); const hash = createHash(secret); await addHash(hash) - const url = `${hostname}?tag=login&k1=${secret}` + let url = `${hostname}?tag=login&k1=${secret}` + if (options.user_token) { + url = url + `&action=link&user_token=${options.user_token}` + } return { url, encoded: lnurl.encode(url).toUpperCase(), diff --git a/api/auth/utils/helperFuncs.js b/api/auth/utils/helperFuncs.js index 975bb2a..54fbf09 100644 --- a/api/auth/utils/helperFuncs.js +++ b/api/auth/utils/helperFuncs.js @@ -3,9 +3,11 @@ const { prisma } = require('../../prisma') const getUserByPubKey = (pubKey) => { if (!pubKey) return null; - return prisma.user.findFirst({ - where: { pubKey } - }) + return prisma.userKey.findUnique({ + where: { + key: pubKey + }, + }).user() } diff --git a/api/functions/get-login-url/get-login-url.js b/api/functions/get-login-url/get-login-url.js index a22c467..1df402c 100644 --- a/api/functions/get-login-url/get-login-url.js +++ b/api/functions/get-login-url/get-login-url.js @@ -5,14 +5,35 @@ const { createExpressApp } = require('../../modules'); const express = require('express'); const jose = require('jose'); const { JWT_SECRET } = require('../../utils/consts'); +const extractKeyFromCookie = require('../../utils/extractKeyFromCookie'); +const { getUserByPubKey } = require('../../auth/utils/helperFuncs'); const getLoginUrl = async (req, res) => { + + const { action } = req.query; + try { - const data = await LnurlAuthService.generateAuthUrl(); + + let user_token = null; + if (action === 'link') { + const userPubKey = await extractKeyFromCookie(req.headers.cookie ?? req.headers.Cookie) + const user = await getUserByPubKey(userPubKey); + + if (!user) + return res.status(400).json({ status: 'ERROR', reason: 'Only authenticated user can request a linking URL' }); + + user_token = await new jose.SignJWT({ user_id: user.id }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('5min') + .sign(Buffer.from(JWT_SECRET, 'utf-8')) + } + + const data = await LnurlAuthService.generateAuthUrl({ user_token }); const session_token = await new jose.SignJWT({ hash: data.secretHash }) .setProtectedHeader({ alg: 'HS256' }) @@ -24,7 +45,6 @@ const getLoginUrl = async (req, res) => { .status(200) .json({ ...data, session_token }); } catch (error) { - console.log(error); res.status(500).send("Unexpected error happened, please try again") } diff --git a/api/functions/login/login.js b/api/functions/login/login.js index fb240e4..83a7b5c 100644 --- a/api/functions/login/login.js +++ b/api/functions/login/login.js @@ -8,11 +8,13 @@ const express = require('express'); const jose = require('jose'); const { JWT_SECRET } = require('../../utils/consts'); const { generatePrivateKey, getPublicKey } = require('../../utils/nostr-tools'); +const { getUserByPubKey } = require('../../auth/utils/helperFuncs'); const loginHandler = async (req, res) => { - const { tag, k1, sig, key } = req.query; + + const { tag, k1, sig, key, action, user_token } = req.query; if (tag !== 'login') return res.status(400).json({ status: 'ERROR', reason: 'Invalid Tag Provided' }) @@ -24,24 +26,86 @@ const loginHandler = async (req, res) => { } + if (action === 'link' && user_token) { + try { + const { payload } = await jose.jwtVerify(user_token, Buffer.from(JWT_SECRET), { + algorithms: ['HS256'], + }) + const user_id = payload.user_id; + const existingKeys = await prisma.userKey.findMany({ where: { user_id }, select: { key: true } }); + + if (existingKeys.length >= 3) + return res.status(400).json({ status: 'ERROR', reason: "Can only link up to 3 wallets" }) + + if (existingKeys.includes(key)) + return res.status(400).json({ status: 'ERROR', reason: "Wallet already linked" }); + + // Remove old linking for this key if existing + await prisma.userKey.deleteMany({ + where: { key } + }) + + + await prisma.userKey.create({ + data: { + key, + user_id, + } + }); + + return res + .status(200) + .json({ status: "OK" }) + + } catch (error) { + return res.status(400).json({ status: 'ERROR', reason: 'Invalid User Token' }) + } + } + try { //Create user if not already existing - const user = await prisma.user.findFirst({ where: { pubKey: key } }) + const user = await getUserByPubKey(key) if (user === null) { - const nostr_prv_key = generatePrivateKey(); - const nostr_pub_key = getPublicKey(nostr_prv_key); + // Check if user had a previous account using this wallet - await prisma.user.create({ - data: { - pubKey: key, - name: key, - avatar: `https://avatars.dicebear.com/api/bottts/${key}.svg`, - nostr_prv_key, - nostr_pub_key, + const oldAccount = await prisma.user.findFirst({ + where: { + pubKey: key } - }) + }); + + if (oldAccount) { + await prisma.userKey.create({ + data: { + key, + name: "My original wallet key", + user_id: oldAccount.id, + } + }); + } else { + const nostr_prv_key = generatePrivateKey(); + const nostr_pub_key = getPublicKey(nostr_prv_key); + + const createdUser = await prisma.user.create({ + data: { + pubKey: key, + name: key, + avatar: `https://avatars.dicebear.com/api/bottts/${key}.svg`, + nostr_prv_key, + nostr_pub_key, + }, + }) + await prisma.userKey.create({ + data: { + key, + name: "My original wallet key", + user_id: createdUser.id, + } + }); + } + } // calc the hash of k1 diff --git a/environments/.preview.env b/environments/.preview.env deleted file mode 100644 index e69de29..0000000 diff --git a/environments/.prod.github.env b/environments/.prod.github.env deleted file mode 100644 index 94cbdea..0000000 --- a/environments/.prod.github.env +++ /dev/null @@ -1,2 +0,0 @@ -REACT_APP_FOR_GITHUB=true -REACT_APP_API_END_POINT = https://makers.bolt.fun/.netlify/functions \ No newline at end of file diff --git a/environments/.dev.mock-server.env b/envs/client/.dev.mock-server.env similarity index 100% rename from environments/.dev.mock-server.env rename to envs/client/.dev.mock-server.env diff --git a/environments/.dev.prod-server.env b/envs/client/.dev.prod-server.env similarity index 100% rename from environments/.dev.prod-server.env rename to envs/client/.dev.prod-server.env diff --git a/environments/.dev.server.env b/envs/client/.dev.server.env similarity index 100% rename from environments/.dev.server.env rename to envs/client/.dev.server.env diff --git a/environments/.prod.mock-server.env b/envs/client/.prod.mock-server.env similarity index 100% rename from environments/.prod.mock-server.env rename to envs/client/.prod.mock-server.env diff --git a/package-lock.json b/package-lock.json index 333e0f7..c57e2a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "my-app", "version": "0.1.0", "dependencies": { - "@apollo/client": "^3.5.10", + "@apollo/client": "^3.6.9", "@hookform/resolvers": "^2.8.8", "@noble/secp256k1": "^1.6.3", "@prisma/client": "^3.12.0", @@ -42,6 +42,7 @@ "crypto": "^1.0.1", "dayjs": "^1.11.1", "dompurify": "^2.3.10", + "embla-carousel-react": "^7.0.0", "env-cmd": "^10.1.0", "express": "^4.18.1", "express-session": "^1.17.3", @@ -76,13 +77,12 @@ "react-loader-spinner": "^6.0.0-0", "react-loading-skeleton": "^3.1.0", "react-modal": "^3.15.1", - "react-multi-carousel": "^2.8.0", "react-query": "^3.35.0", "react-redux": "^8.0.0", - "react-responsive-carousel": "^3.2.23", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-select": "^5.3.2", + "react-toastify": "^9.0.8", "react-tooltip": "^4.2.21", "react-topbar-progress-indicator": "^4.1.1", "remirror": "^1.0.77", @@ -144,27 +144,27 @@ } }, "node_modules/@apollo/client": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.5.10.tgz", - "integrity": "sha512-tL3iSpFe9Oldq7gYikZK1dcYxp1c01nlSwtsMz75382HcI6fvQXyFXUCJTTK3wgO2/ckaBvRGw7VqjFREdVoRw==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.6.9.tgz", + "integrity": "sha512-Y1yu8qa2YeaCUBVuw08x8NHenFi0sw2I3KCu7Kw9mDSu86HmmtHJkCAifKVrN2iPgDTW/BbP3EpSV8/EQCcxZA==", "dependencies": { - "@graphql-typed-document-node/core": "^3.0.0", + "@graphql-typed-document-node/core": "^3.1.1", "@wry/context": "^0.6.0", "@wry/equality": "^0.5.0", "@wry/trie": "^0.3.0", - "graphql-tag": "^2.12.3", + "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", "optimism": "^0.16.1", "prop-types": "^15.7.2", "symbol-observable": "^4.0.0", - "ts-invariant": "^0.9.4", + "ts-invariant": "^0.10.3", "tslib": "^2.3.0", - "zen-observable-ts": "^1.2.0" + "zen-observable-ts": "^1.2.5" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5", - "react": "^16.8.0 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" }, "peerDependenciesMeta": { @@ -19588,7 +19588,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", - "dev": true, "engines": { "node": ">=6" } @@ -21866,6 +21865,22 @@ "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==", "dev": true }, + "node_modules/embla-carousel": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-7.0.0.tgz", + "integrity": "sha512-vgyElJaBRTtzWROQuO9Qx/VlibzKdhwnuQ2Ldh5/7/jddrB4XTI6IQlgR5ZglRaXjH4nXjVtUSwWWZMEIZQLFQ==" + }, + "node_modules/embla-carousel-react": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-7.0.0.tgz", + "integrity": "sha512-y17TYtqvvziWbDwwfX5dh8n4qU1luytz4+6WWMBnR4pJfLfkKBGsqYNZ4WhmAgUcYL1Uliti8Cjg+NJd46MPxw==", + "dependencies": { + "embla-carousel": "7.0.0" + }, + "peerDependencies": { + "react": "^18.1.0" + } + }, "node_modules/emittery": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", @@ -60197,17 +60212,6 @@ "react-dom": ">= 16.3.0" } }, - "node_modules/react-easy-swipe": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/react-easy-swipe/-/react-easy-swipe-0.0.21.tgz", - "integrity": "sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg==", - "dependencies": { - "prop-types": "^15.5.8" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/react-element-to-jsx-string": { "version": "14.3.4", "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz", @@ -60382,14 +60386,6 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" } }, - "node_modules/react-multi-carousel": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/react-multi-carousel/-/react-multi-carousel-2.8.0.tgz", - "integrity": "sha512-xuxQVGGiH8yWDDWgt9Z9+C+zzoACuBT740cV+6To52DYCwLlQGJIntDFLOCqMsO85Ist1x+630HA8lazb/a/tA==", - "engines": { - "node": ">=8" - } - }, "node_modules/react-onclickoutside": { "version": "6.12.1", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz", @@ -60503,16 +60499,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-responsive-carousel": { - "version": "3.2.23", - "resolved": "https://registry.npmjs.org/react-responsive-carousel/-/react-responsive-carousel-3.2.23.tgz", - "integrity": "sha512-pqJLsBaKHWJhw/ItODgbVoziR2z4lpcJg+YwmRlSk4rKH32VE633mAtZZ9kDXjy4wFO+pgUZmDKPsPe1fPmHCg==", - "dependencies": { - "classnames": "^2.2.5", - "prop-types": "^15.5.8", - "react-easy-swipe": "^0.0.21" - } - }, "node_modules/react-router": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", @@ -60710,6 +60696,18 @@ "react": "^16.8.0 || ^17.0.0" } }, + "node_modules/react-toastify": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.8.tgz", + "integrity": "sha512-EwM+teWt49HSHx+67qI08yLAW1zAsBxCXLCsUfxHYv1W7/R3ZLhrqKalh7j+kjgPna1h5LQMSMwns4tB4ww2yQ==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-tooltip": { "version": "4.2.21", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.21.tgz", @@ -65311,9 +65309,9 @@ "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" }, "node_modules/ts-invariant": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.9.4.tgz", - "integrity": "sha512-63jtX/ZSwnUNi/WhXjnK8kz4cHHpYS60AnmA6ixz17l7E12a5puCWFlNpkne5Rl0J8TBPVHpGjsj4fxs8ObVLQ==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", "dependencies": { "tslib": "^2.1.0" }, @@ -68013,9 +68011,9 @@ "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" }, "node_modules/zen-observable-ts": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.3.tgz", - "integrity": "sha512-hc/TGiPkAWpByykMwDcem3SdUgA4We+0Qb36bItSuJC9xD0XVBZoFHYoadAomDSNf64CG8Ydj0Qb8Od8BUWz5g==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", "dependencies": { "zen-observable": "0.8.15" } @@ -68041,22 +68039,22 @@ } }, "@apollo/client": { - "version": "3.5.10", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.5.10.tgz", - "integrity": "sha512-tL3iSpFe9Oldq7gYikZK1dcYxp1c01nlSwtsMz75382HcI6fvQXyFXUCJTTK3wgO2/ckaBvRGw7VqjFREdVoRw==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.6.9.tgz", + "integrity": "sha512-Y1yu8qa2YeaCUBVuw08x8NHenFi0sw2I3KCu7Kw9mDSu86HmmtHJkCAifKVrN2iPgDTW/BbP3EpSV8/EQCcxZA==", "requires": { - "@graphql-typed-document-node/core": "^3.0.0", + "@graphql-typed-document-node/core": "^3.1.1", "@wry/context": "^0.6.0", "@wry/equality": "^0.5.0", "@wry/trie": "^0.3.0", - "graphql-tag": "^2.12.3", + "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", "optimism": "^0.16.1", "prop-types": "^15.7.2", "symbol-observable": "^4.0.0", - "ts-invariant": "^0.9.4", + "ts-invariant": "^0.10.3", "tslib": "^2.3.0", - "zen-observable-ts": "^1.2.0" + "zen-observable-ts": "^1.2.5" } }, "@apollo/protobufjs": { @@ -83053,8 +83051,7 @@ "clsx": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", - "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", - "dev": true + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==" }, "co": { "version": "4.6.0", @@ -84862,6 +84859,19 @@ "integrity": "sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==", "dev": true }, + "embla-carousel": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-7.0.0.tgz", + "integrity": "sha512-vgyElJaBRTtzWROQuO9Qx/VlibzKdhwnuQ2Ldh5/7/jddrB4XTI6IQlgR5ZglRaXjH4nXjVtUSwWWZMEIZQLFQ==" + }, + "embla-carousel-react": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-7.0.0.tgz", + "integrity": "sha512-y17TYtqvvziWbDwwfX5dh8n4qU1luytz4+6WWMBnR4pJfLfkKBGsqYNZ4WhmAgUcYL1Uliti8Cjg+NJd46MPxw==", + "requires": { + "embla-carousel": "7.0.0" + } + }, "emittery": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", @@ -113858,14 +113868,6 @@ "prop-types": "^15.6.0" } }, - "react-easy-swipe": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/react-easy-swipe/-/react-easy-swipe-0.0.21.tgz", - "integrity": "sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg==", - "requires": { - "prop-types": "^15.5.8" - } - }, "react-element-to-jsx-string": { "version": "14.3.4", "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.4.tgz", @@ -113997,11 +113999,6 @@ "warning": "^4.0.3" } }, - "react-multi-carousel": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/react-multi-carousel/-/react-multi-carousel-2.8.0.tgz", - "integrity": "sha512-xuxQVGGiH8yWDDWgt9Z9+C+zzoACuBT740cV+6To52DYCwLlQGJIntDFLOCqMsO85Ist1x+630HA8lazb/a/tA==" - }, "react-onclickoutside": { "version": "6.12.1", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz", @@ -114063,16 +114060,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" }, - "react-responsive-carousel": { - "version": "3.2.23", - "resolved": "https://registry.npmjs.org/react-responsive-carousel/-/react-responsive-carousel-3.2.23.tgz", - "integrity": "sha512-pqJLsBaKHWJhw/ItODgbVoziR2z4lpcJg+YwmRlSk4rKH32VE633mAtZZ9kDXjy4wFO+pgUZmDKPsPe1fPmHCg==", - "requires": { - "classnames": "^2.2.5", - "prop-types": "^15.5.8", - "react-easy-swipe": "^0.0.21" - } - }, "react-router": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz", @@ -114239,6 +114226,14 @@ "use-latest": "^1.0.0" } }, + "react-toastify": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.8.tgz", + "integrity": "sha512-EwM+teWt49HSHx+67qI08yLAW1zAsBxCXLCsUfxHYv1W7/R3ZLhrqKalh7j+kjgPna1h5LQMSMwns4tB4ww2yQ==", + "requires": { + "clsx": "^1.1.1" + } + }, "react-tooltip": { "version": "4.2.21", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.21.tgz", @@ -117867,9 +117862,9 @@ "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==" }, "ts-invariant": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.9.4.tgz", - "integrity": "sha512-63jtX/ZSwnUNi/WhXjnK8kz4cHHpYS60AnmA6ixz17l7E12a5puCWFlNpkne5Rl0J8TBPVHpGjsj4fxs8ObVLQ==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", "requires": { "tslib": "^2.1.0" } @@ -119989,9 +119984,9 @@ "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" }, "zen-observable-ts": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.3.tgz", - "integrity": "sha512-hc/TGiPkAWpByykMwDcem3SdUgA4We+0Qb36bItSuJC9xD0XVBZoFHYoadAomDSNf64CG8Ydj0Qb8Od8BUWz5g==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", + "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", "requires": { "zen-observable": "0.8.15" } diff --git a/package.json b/package.json index 7653fcc..dd0041d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@apollo/client": "^3.5.10", + "@apollo/client": "^3.6.9", "@hookform/resolvers": "^2.8.8", "@noble/secp256k1": "^1.6.3", "@prisma/client": "^3.12.0", @@ -37,6 +37,7 @@ "crypto": "^1.0.1", "dayjs": "^1.11.1", "dompurify": "^2.3.10", + "embla-carousel-react": "^7.0.0", "env-cmd": "^10.1.0", "express": "^4.18.1", "express-session": "^1.17.3", @@ -71,13 +72,12 @@ "react-loader-spinner": "^6.0.0-0", "react-loading-skeleton": "^3.1.0", "react-modal": "^3.15.1", - "react-multi-carousel": "^2.8.0", "react-query": "^3.35.0", "react-redux": "^8.0.0", - "react-responsive-carousel": "^3.2.23", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-select": "^5.3.2", + "react-toastify": "^9.0.8", "react-tooltip": "^4.2.21", "react-topbar-progress-indicator": "^4.1.1", "remirror": "^1.0.77", @@ -90,23 +90,22 @@ "yup": "^0.32.11" }, "scripts": { - "client:prod-server": "env-cmd -f ./environments/.dev.prod-server.env react-scripts start", - "client:preview-server": "env-cmd -f ./environments/.dev.preview-server.env react-scripts start", - "client:mocks": "env-cmd -f ./environments/.dev.mock-server.env react-scripts start", - "client:dev-server": "env-cmd -f ./environments/.dev.server.env react-scripts start", - "server:dev": "serverless offline", + "client:prod-server": "env-cmd -f ./envs/client/.dev.prod-server.env react-scripts start", + "client:preview-server": "env-cmd -f ./envs/client/.dev.preview-server.env react-scripts start", + "client:mocks": "env-cmd -f ./envs/client/.dev.mock-server.env react-scripts start", + "client:dev-server": "env-cmd -f ./envs/client/.dev.server.env react-scripts start", + "server:dev": "env-cmd -f ./envs/server/local.env serverless offline", + "server:preview": "env-cmd -f ./envs/server/preview.env serverless offline", + "server:prod": "env-cmd -f ./envs/server/prod.env serverless offline", "generate-graphql": "graphql-codegen", + "storybook": "env-cmd -f ./envs/client/.dev.preview-server.env start-storybook -p 6006 -s public", + "storybook:mocks": "env-cmd -f ./envs/client/.dev.mock-server.env start-storybook -p 6006 -s public", "build": "react-scripts build", - "build:mocks": "env-cmd -f ./environments/.prod.mock-server.env react-scripts build", + "build:mocks": "env-cmd -f ./envs/client/.prod.mock-server.env react-scripts build", + "build-storybook": "env-cmd -f ./envs/client/.dev.preview-server.env build-storybook -s public", + "build-storybook:mocks": "env-cmd -f ./envs/client/.prod.mock-server.env build-storybook -s public", "test": "react-scripts test", "eject": "react-scripts eject", - "predeploy": "env-cmd -f ./environments/.prod.github.env npm run build", - "deploy": "gh-pages -d build", - "only-deploy": "gh-pages -d build", - "storybook": "env-cmd -f ./environments/.dev.preview-server.env start-storybook -p 6006 -s public", - "storybook:mocks": "env-cmd -f ./environments/.dev.mock-server.env start-storybook -p 6006 -s public", - "build-storybook": "env-cmd -f ./environments/.prod.preview-server.env build-storybook -s public", - "build-storybook:mocks": "env-cmd -f ./environments/.prod.mock-server.env build-storybook -s public", "db:migrate-dev": "prisma migrate dev", "db:migrate-deploy": "prisma migrate deploy", "db:reset": "prisma migrate reset", diff --git a/prisma/migrations/20220808073740_add_userkeys_table/migration.sql b/prisma/migrations/20220808073740_add_userkeys_table/migration.sql new file mode 100644 index 0000000..1d6b0e2 --- /dev/null +++ b/prisma/migrations/20220808073740_add_userkeys_table/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "UserKey" ( + "key" TEXT NOT NULL, + "user_id" INTEGER, + + CONSTRAINT "UserKey_pkey" PRIMARY KEY ("key") +); + +-- AddForeignKey +ALTER TABLE "UserKey" ADD CONSTRAINT "UserKey_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5e9048e..09cdc25 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -64,6 +64,15 @@ model User { questions Question[] posts_comments PostComment[] donations Donation[] + userKeys UserKey[] +} + +model UserKey { + key String @id + name String @default("My new wallet key") + + user User? @relation(fields: [user_id], references: [id]) + user_id Int? } // ----------------- diff --git a/prisma/seed/data.js b/prisma/seed/data.js index 40d1fe0..4f22d06 100644 --- a/prisma/seed/data.js +++ b/prisma/seed/data.js @@ -1,3 +1,4 @@ +const { randomItem, randomItems, random, getCoverImage } = require("./helpers") const categories = [ { @@ -46,6 +47,68 @@ const categories = [ } ] +const tags = [ + { + id: 1, + title: 'Bitcoin', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', + icon: "🅱", + isOfficial: true, + }, + { + id: 2, + title: 'Lightning', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', + icon: "⚡", + isOfficial: true, + }, + { + id: 3, + title: 'Webln', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', + icon: "🔗", + isOfficial: true, + }, + { + id: 4, + title: 'Gaming', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', + icon: "🎮", + isOfficial: true, + }, + { + + id: 5, + title: 'Design', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', + icon: '🎨', + isOfficial: true, + }, + { + + id: 6, + title: 'Launch', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', + icon: '🚀', + isOfficial: true, + }, + { + + id: 7, + title: 'Brainstory', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', + icon: '🧠', + isOfficial: true, + }, + { + + id: 8, + title: 'Development', + description: 'Lorem ipsum dolor sit amort consectetur, adipisicing elit. Possimus officia sit numquam nobis iure atque ab sunt nihil voluptatibus', + icon: '💻', + isOfficial: true, + } +] const projects = [ { @@ -56,9 +119,8 @@ const projects = [ "website": "https://geyser.fund/", "lightning_address": "divineorgan67@walletofsatoshi.com", "votes_count": 232, - "category": { - "id": 1 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 16, @@ -68,9 +130,8 @@ const projects = [ "website": "https://getalby.com/", "lightning_address": "hello@getalby.com", "votes_count": 215, - "category": { - "id": 9 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 20, @@ -80,9 +141,8 @@ const projects = [ "website": "https://lightning.video/", "lightning_address": "moritz@getalby.com", "votes_count": 175, - "category": { - "id": 7 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 1, @@ -92,9 +152,8 @@ const projects = [ "website": "https://kollider.xyz/", "lightning_address": "johns@getalby.com", "votes_count": 120, - "category": { - "id": 1 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 12, @@ -104,9 +163,8 @@ const projects = [ "website": "https://www.bitrefill.com/buy", "lightning_address": "moritz@getalby.com", "votes_count": 25, - "category": { - "id": 8 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 7, @@ -116,9 +174,8 @@ const projects = [ "website": "https://www.wavlake.com/", "lightning_address": "moritz@getalby.com", "votes_count": 25, - "category": { - "id": 7 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 3, @@ -128,9 +185,8 @@ const projects = [ "website": "https://sparkshot.io/", "lightning_address": "johns@getalby.com", "votes_count": 11, - "category": { - "id": 3 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 17, @@ -140,9 +196,8 @@ const projects = [ "website": "https://lightning.gifts/", "lightning_address": "moritz@getalby.com", "votes_count": 10, - "category": { - "id": 8 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 4, @@ -152,9 +207,8 @@ const projects = [ "website": "https://amboss.space/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 6 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 5, @@ -164,9 +218,8 @@ const projects = [ "website": "https://www.lnblackjack.com/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 4 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 19, @@ -176,9 +229,8 @@ const projects = [ "website": "https://yalls.org/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 2 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 13, @@ -188,9 +240,8 @@ const projects = [ "website": "https://lightningnetworkstores.com/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 8 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 9, @@ -200,9 +251,8 @@ const projects = [ "website": "https://lightning-poker.com/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 4 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 6, @@ -212,9 +262,8 @@ const projects = [ "website": "https://lngames.net/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 4 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 21, @@ -224,9 +273,8 @@ const projects = [ "website": "https://www.starbackr.com/", "lightning_address": "moritz@geralby.com", "votes_count": 0, - "category": { - "id": 7 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 8, @@ -236,9 +284,8 @@ const projects = [ "website": "https://loft.trade/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 1 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 10, @@ -248,9 +295,8 @@ const projects = [ "website": "https://lightning-roulette.com/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 4 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 14, @@ -260,9 +306,8 @@ const projects = [ "website": "https://kriptode.com/satsforlikes/index.html", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 9 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 18, @@ -272,9 +317,8 @@ const projects = [ "website": "https://scarce.city/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 3 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 15, @@ -284,9 +328,8 @@ const projects = [ "website": "https://lnshort.it/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 9 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 11, @@ -296,9 +339,8 @@ const projects = [ "website": "https://stacker.news/", "lightning_address": "moritz@getalby.com", "votes_count": 0, - "category": { - "id": 7 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) }, { "id": 2, @@ -308,14 +350,78 @@ const projects = [ "website": "https://lnmarkets.com/", "lightning_address": "johns@getalby.com", "votes_count": 0, - "category": { - "id": 1 - } + "category_id": randomItem(categories).id, + tags: randomItems(random(2, 3), tags) } ] +const hackathons = [ + { + title: 'Fulmo Hackday', + start_date: new Date(2022, 2, 22), + end_date: new Date(2022, 2, 28), + location: "Instanbul, Turkey", + cover_image: getCoverImage(), + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.", + tags: randomItems(3, tags), + website: "https://bolt.fun/hackathons/shock-the-web" + }, + { + title: 'Lightning Leagues', + start_date: new Date(2022, 2, 22), + end_date: new Date(2022, 2, 28), + location: "Instanbul, Turkey", + cover_image: getCoverImage(), + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.", + tags: randomItems(3, tags), + website: "https://bolt.fun/hackathons/shock-the-web" + }, + { + title: 'Surfing on Lightning', + start_date: new Date(2022, 2, 22), + end_date: new Date(2022, 2, 28), + location: "Instanbul, Turkey", + cover_image: getCoverImage(), + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.", + tags: randomItems(3, tags), + website: "https://bolt.fun/hackathons/shock-the-web" + }, + { + title: 'Lightning Startups', + start_date: new Date(2022, 2, 22), + end_date: new Date(2022, 2, 28), + location: "Instanbul, Turkey", + cover_image: getCoverImage(), + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.", + tags: randomItems(3, tags), + website: "https://bolt.fun/hackathons/shock-the-web" + }, + { + title: 'Design-a-thon', + start_date: new Date(2022, 2, 22), + end_date: new Date(2022, 2, 28), + location: "Instanbul, Turkey", + cover_image: getCoverImage(), + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.", + tags: randomItems(3, tags), + website: "https://bolt.fun/hackathons/shock-the-web" + }, + { + title: 'Lightning Olympics', + start_date: new Date(2022, 2, 22), + end_date: new Date(2022, 2, 28), + location: "Instanbul, Turkey", + cover_image: getCoverImage(), + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.", + tags: randomItems(3, tags), + website: "https://bolt.fun/hackathons/shock-the-web" + }, +] + module.exports = { categories, - projects + projects, + tags, + hackathons, } \ No newline at end of file diff --git a/prisma/seed/helpers.js b/prisma/seed/helpers.js new file mode 100644 index 0000000..8c83c3b --- /dev/null +++ b/prisma/seed/helpers.js @@ -0,0 +1,51 @@ +function random(min, max) { + return Math.random() * (max - min) + min; +} + +function randomItem(args) { + return args[Math.floor(Math.random() * args.length)]; +} + +function randomItems(cnt, args) { + return shuffle(args).slice(0, Math.floor(cnt)); +} + +function shuffle(_array) { + let array = [..._array] + let currentIndex = array.length, randomIndex; + + // While there remain elements to shuffle. + while (currentIndex !== 0) { + + // Pick a remaining element. + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], array[currentIndex]]; + } + + return array; +} + + +let coverImgsCntr = -1; + +function getCoverImage() { + const coverImgs = [ + 'https://picsum.photos/id/10/1600/900', + 'https://picsum.photos/id/1000/1600/900', + 'https://picsum.photos/id/1002/1600/900', + 'https://picsum.photos/id/1018/1600/900', + ] + + return coverImgs[(++coverImgsCntr) % coverImgs.length] +} + +module.exports = { + random, + randomItem, + randomItems, + getCoverImage, +} \ No newline at end of file diff --git a/prisma/seed/index.js b/prisma/seed/index.js index 894d82e..1484395 100644 --- a/prisma/seed/index.js +++ b/prisma/seed/index.js @@ -1,6 +1,10 @@ const { PrismaClient } = require("@prisma/client"); const { generatePrivateKey, getPublicKey } = require("../../api/utils/nostr-tools"); -const { categories, projects } = require("./data"); +const { categories, projects, tags, hackathons } = require("./data"); +const Chance = require('chance'); +const { getCoverImage, randomItems, random } = require("./helpers"); + +const chance = new Chance(); const prisma = new PrismaClient() @@ -41,9 +45,24 @@ async function generateNostrKeys() { async function main() { - console.log("Purging old data"); - await purge() + // console.log("Purging old data"); + // await purge() + // await createCategories(); + + // await createTags(); + + // await createProjects(); + + // await createStories(); + + // await createHackathons(); + + await fillUserKeysTable() + +} + +async function createCategories() { console.log("Creating Categories"); await prisma.category.createMany({ data: categories.map(item => ({ @@ -53,50 +72,105 @@ async function main() { icon: '🎭' })) }) +} +async function createTags() { + console.log("Creating Tags"); + await prisma.tag.createMany({ + data: tags.map(item => ({ + id: item.id, + title: item.title, + description: item.description, + isOfficial: item.isOfficial, + icon: item.icon, + })) + }) +} +async function createProjects() { console.log("Creating Projects"); - projects.forEach(async item => { - const { category, ...feilds } = item + + for (let i = 0; i < projects.length; i++) { + const item = projects[i]; + const { tags, ...feilds } = item await prisma.project.create({ data: { ...feilds, tags: { - connectOrCreate: [ - { - where: { - title: 'Tag 1' - }, - create: { - title: "Tag 1" - } - }, - { - where: { - title: 'Tag 2' - }, - create: { - title: "Tag 2" - } - }, - { - where: { - title: 'Tag 3' - }, - create: { - title: "Tag 3" - } - },] + connect: tags.map(t => ({ + id: t.id + })) }, - category_id: category.id, description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.", screenshots: Array(4).fill('https://via.placeholder.com/1280x729.png?text=Project+Screenshot') } }) + } +} + +async function createStories() { + console.log("Creating Stories"); + + const user = await prisma.user.findFirst(); + + for (let i = 0; i < 15; i++) { + const randomTags = randomItems(random(2, 5), tags) + await prisma.story.create({ + data: { + body: chance.paragraph(), + excerpt: chance.paragraph({ sentences: 1 }), + title: chance.sentence({ words: chance.integer({ min: 3, max: 7 }) }), + cover_image: getCoverImage(), + is_published: true, + tags: { + connect: randomTags.map(t => ({ id: t.id })) + }, + user_id: user.id, + votes_count: Math.floor(random(10, 6600)), + } + }) + } +} + +async function createHackathons() { + console.log("Creating Hackathons"); + + for (let i = 0; i < hackathons.length; i++) { + const item = hackathons[i]; + const { tags, ...feilds } = item + await prisma.hackathon.create({ + data: { + ...feilds, + tags: { + connect: tags.map(t => ({ + id: t.id + })) + }, + description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.", + } + }) + } +} + +async function fillUserKeysTable() { + console.log('Filling Users Keys Table'); + const allUsers = await prisma.user.findMany({ + select: { + id: true, + pubKey: true, + } }) + await prisma.userKey.createMany({ + data: allUsers.filter(u => !!u.pubKey).map(u => ({ + key: u.pubKey, + user_id: u.id + })) + }) } + + main() .catch((e) => { console.error(e) diff --git a/src/App.tsx b/src/App.tsx index 5d0c5b1..74b7d24 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,25 +10,26 @@ import { setUser } from "./redux/features/user.slice"; import ProtectedRoute from "./Components/ProtectedRoute/ProtectedRoute"; import { Helmet } from "react-helmet"; import { NavbarLayout } from "./utils/routing/layouts"; -import { Loadable } from "./utils/routing"; +import { Loadable, PAGES_ROUTES } from "./utils/routing"; // Pages -const FeedPage = Loadable(React.lazy(() => import("./features/Posts/pages/FeedPage/FeedPage"))) -const PostDetailsPage = Loadable(React.lazy(() => import("./features/Posts/pages/PostDetailsPage/PostDetailsPage"))) -const CreatePostPage = Loadable(React.lazy(() => import("./features/Posts/pages/CreatePostPage/CreatePostPage"))) +const FeedPage = Loadable(React.lazy(() => import( /* webpackChunkName: "feed_page" */ "./features/Posts/pages/FeedPage/FeedPage"))) +const PostDetailsPage = Loadable(React.lazy(() => import( /* webpackChunkName: "post_details_page" */ "./features/Posts/pages/PostDetailsPage/PostDetailsPage"))) +const CreatePostPage = Loadable(React.lazy(() => import( /* webpackChunkName: "create_post_page" */ "./features/Posts/pages/CreatePostPage/CreatePostPage"))) -const HottestPage = Loadable(React.lazy(() => import("src/features/Projects/pages/HottestPage/HottestPage"))) -const CategoryPage = Loadable(React.lazy(() => import("src/features/Projects/pages/CategoryPage/CategoryPage"))) -const ExplorePage = Loadable(React.lazy(() => import("src/features/Projects/pages/ExplorePage"))) +const HottestPage = Loadable(React.lazy(() => import( /* webpackChunkName: "hottest_page" */ "src/features/Projects/pages/HottestPage/HottestPage"))) +const CategoryPage = Loadable(React.lazy(() => import( /* webpackChunkName: "category_page" */ "src/features/Projects/pages/CategoryPage/CategoryPage"))) +const ExplorePage = Loadable(React.lazy(() => import( /* webpackChunkName: "explore_page" */ "src/features/Projects/pages/ExplorePage"))) -const HackathonsPage = Loadable(React.lazy(() => import("./features/Hackathons/pages/HackathonsPage/HackathonsPage"))) +const HackathonsPage = Loadable(React.lazy(() => import( /* webpackChunkName: "hackathons_page" */ "./features/Hackathons/pages/HackathonsPage/HackathonsPage"))) -const DonatePage = Loadable(React.lazy(() => import("./features/Donations/pages/DonatePage/DonatePage"))) -const LoginPage = Loadable(React.lazy(() => import("./features/Auth/pages/LoginPage/LoginPage"))) -const LogoutPage = Loadable(React.lazy(() => import("./features/Auth/pages/LogoutPage/LogoutPage"))) -const ProfilePage = Loadable(React.lazy(() => import("./features/Profiles/pages/ProfilePage/ProfilePage"))) +const DonatePage = Loadable(React.lazy(() => import( /* webpackChunkName: "donate_page" */ "./features/Donations/pages/DonatePage/DonatePage"))) +const LoginPage = Loadable(React.lazy(() => import( /* webpackChunkName: "login_page" */ "./features/Auth/pages/LoginPage/LoginPage"))) +const LogoutPage = Loadable(React.lazy(() => import( /* webpackChunkName: "logout_page" */ "./features/Auth/pages/LogoutPage/LogoutPage"))) +const ProfilePage = Loadable(React.lazy(() => import( /* webpackChunkName: "profile_page" */ "./features/Profiles/pages/ProfilePage/ProfilePage"))) +const EditProfilePage = Loadable(React.lazy(() => import( /* webpackChunkName: "edit_profile_page" */ "./features/Profiles/pages/EditProfilePage/EditProfilePage"))) @@ -90,25 +91,27 @@ function App() { }> - } /> + } /> }> - } /> - } /> - } /> + } /> + } /> + } /> - } /> - } /> + } /> + } /> - } /> + } /> - } /> + } /> - } /> - } /> - } /> + } /> + } /> - } /> + } /> + } /> + + } /> diff --git a/src/Components/Card/Card.stories.tsx b/src/Components/Card/Card.stories.tsx new file mode 100644 index 0000000..2cc58a4 --- /dev/null +++ b/src/Components/Card/Card.stories.tsx @@ -0,0 +1,23 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import Card from './Card'; + +export default { + title: 'Shared/Card', + component: Card, + argTypes: { + backgroundColor: { control: 'color' }, + }, +} as ComponentMeta; + + +const Template: ComponentStory = (args) =>

Lorem ipsum dolor sit amet consectetur, adipisicing elit. Voluptas corrupti molestias, accusantium porro vitae mollitia voluptatibus omnis. Itaque assumenda minus cum reprehenderit sit, cupiditate, impedit doloribus ad modi corporis maiores. Corrupti praesentium, dolor vero veniam suscipit architecto accusamus beatae minus iste sed ea harum aperiam quibusdam fugiat molestias dolores magni!

+
+ +export const Default = Template.bind({}); + + +export const Primary = Template.bind({}); +Primary.args = { + onlyMd: true +} diff --git a/src/Components/Card/Card.tsx b/src/Components/Card/Card.tsx new file mode 100644 index 0000000..edc6123 --- /dev/null +++ b/src/Components/Card/Card.tsx @@ -0,0 +1,33 @@ + +import React, { PropsWithChildren } from 'react' + +interface Props extends React.ComponentPropsWithoutRef<'div'> { + onlyMd?: boolean; + defaultPadding?: boolean + +} + +const Card = React.forwardRef>(({ + onlyMd = false, + defaultPadding = true, + className, + ...props +}, ref) => { + + + return ( +
+ ) +}) + +export default Card; \ No newline at end of file diff --git a/src/Components/ErrorBoundary/ErrorBoundary.tsx b/src/Components/ErrorBoundary/ErrorBoundary.tsx index 8c41157..7fcfef3 100644 --- a/src/Components/ErrorBoundary/ErrorBoundary.tsx +++ b/src/Components/ErrorBoundary/ErrorBoundary.tsx @@ -1,4 +1,5 @@ import React, { Component, ErrorInfo, ReactNode } from "react"; +import ErrorMessage from "../ErrorMessage/ErrorMessage"; interface Props { place?: string @@ -25,7 +26,16 @@ class ErrorBoundary extends Component { public render() { if (this.state.hasError) { - return

Sorry.. there was an error

; + return
+ + Sorry, something went wrong...😵 +
+ Try refreshing the page. +

+ + } type="unknown">
+
; } return this.props.children; diff --git a/src/Components/ErrorMessage/ErrorMessage.tsx b/src/Components/ErrorMessage/ErrorMessage.tsx index 69ce4dd..b6cd8fc 100644 --- a/src/Components/ErrorMessage/ErrorMessage.tsx +++ b/src/Components/ErrorMessage/ErrorMessage.tsx @@ -1,7 +1,7 @@ import { Link } from "react-router-dom" interface Props { - message?: string, + message?: string | JSX.Element, type?: 'unknown' | 'fetching' } @@ -22,9 +22,9 @@ export default function ErrorMessage({ return (
-

+

{messageToShow} -

+
Back to home page
) diff --git a/src/Components/Inputs/TextEditor/InsertLinkModal/InsertLinkModal.stories.tsx b/src/Components/Inputs/TextEditor/InsertLinkModal/InsertLinkModal.stories.tsx new file mode 100644 index 0000000..96c2fd1 --- /dev/null +++ b/src/Components/Inputs/TextEditor/InsertLinkModal/InsertLinkModal.stories.tsx @@ -0,0 +1,17 @@ +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import InsertLinkModal from './InsertLinkModal'; + +import { ModalsDecorator } from 'src/utils/storybook/decorators'; + +export default { + title: 'Shared/Inputs/Text Editor/Insert Link Modal', + component: InsertLinkModal, + + decorators: [ModalsDecorator] +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Default = Template.bind({}); + diff --git a/src/Components/Inputs/TextEditor/InsertLinkModal/InsertLinkModal.tsx b/src/Components/Inputs/TextEditor/InsertLinkModal/InsertLinkModal.tsx new file mode 100644 index 0000000..290094e --- /dev/null +++ b/src/Components/Inputs/TextEditor/InsertLinkModal/InsertLinkModal.tsx @@ -0,0 +1,107 @@ +import React, { FormEvent, useState } from 'react' +import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer' +import { motion } from 'framer-motion' +import { IoClose } from 'react-icons/io5' +import Button from 'src/Components/Button/Button' +import { useAppDispatch } from 'src/utils/hooks' +import { PayloadAction } from '@reduxjs/toolkit' +import * as yup from "yup"; +import { SubmitHandler, useForm } from "react-hook-form" +import { yupResolver } from "@hookform/resolvers/yup"; + +interface Props extends ModalCard { + callbackAction: PayloadAction<{ href: string, text: string }> +} + +const schema = yup.object({ + text: yup.string().trim().required().min(2, 'Link text should be at least 2 characters'), + href: yup.string().trim().required().url(), + +}).required(); + + +export interface IFormInputs { + text: string, + href: string, +} + + +export default function InsertLinkModal({ onClose, direction, callbackAction, ...props }: Props) { + + const { register, formState: { errors }, handleSubmit } = useForm({ + defaultValues: { + href: '', + text: '', + }, + resolver: yupResolver(schema), + mode: 'onBlur', + }); + + const dispatch = useAppDispatch(); + + const onSubmit: SubmitHandler = data => { + const action = Object.assign({}, callbackAction); + action.payload = { text: data.text, href: data.href } + dispatch(action) + onClose?.(); + }; + + return ( + + +

Insert Link

+
+
+
+

+ Link Text +

+
+ +
+ {errors.text &&

+ {errors.text.message} +

} +
+
+

+ Link URL +

+
+ +
+ {errors.href &&

+ {errors.href.message} +

} +
+
+
+ + +
+
+ +
+ ) +} diff --git a/src/Components/Inputs/TextEditor/InsertLinkModal/index.tsx b/src/Components/Inputs/TextEditor/InsertLinkModal/index.tsx new file mode 100644 index 0000000..73eee05 --- /dev/null +++ b/src/Components/Inputs/TextEditor/InsertLinkModal/index.tsx @@ -0,0 +1,4 @@ + +import { lazyModal } from 'src/utils/helperFunctions'; + +export const { LazyComponent: InsertLinkModal } = lazyModal(() => import('./InsertLinkModal')) \ No newline at end of file diff --git a/src/Components/Inputs/TextEditor/ToolButton/LinkToolBtn.tsx b/src/Components/Inputs/TextEditor/ToolButton/LinkToolBtn.tsx new file mode 100644 index 0000000..48b3e54 --- /dev/null +++ b/src/Components/Inputs/TextEditor/ToolButton/LinkToolBtn.tsx @@ -0,0 +1,68 @@ +import { useActive, useCommands } from '@remirror/react'; +import { useAppDispatch } from 'src/utils/hooks'; +import { openModal } from 'src/redux/features/modals.slice'; +import { useReduxEffect } from 'src/utils/hooks/useReduxEffect'; +import { useCallback } from 'react'; +import { createAction } from '@reduxjs/toolkit'; +import { cmdToBtn } from './helpers'; + +interface Props { + classes: { + button: string, + icon: string, + active: string, + enabled: string + disabled: string + } +} + +const INSERT_LINK_ACTION = createAction<{ href: string, text: string }>('LINK_INSERTED_IN_EDITOR')({ href: '', text: '' }) + +export default function LinkToolButton({ classes }: Props) { + + const commands = useCommands(); + + const dispatch = useAppDispatch() + + const onInsertLink = useCallback(({ payload: { href, text } }: typeof INSERT_LINK_ACTION) => { + commands.insertMarkdown(`[${text}](${href})`) + }, [commands]) + + useReduxEffect(onInsertLink, INSERT_LINK_ACTION.type) + + + + const { tip, Icon } = cmdToBtn['link']; + const onClick = () => { + dispatch(openModal({ + Modal: "InsertLinkModal", + props: { + callbackAction: { + type: INSERT_LINK_ACTION.type, + payload: { + href: "", + text: "", + } + } + } + })) + } + + return ( + + ) + + + +} + diff --git a/src/Components/Inputs/TextEditor/ToolButton/ToolBtn.tsx b/src/Components/Inputs/TextEditor/ToolButton/ToolBtn.tsx index d494ca0..0846b08 100644 --- a/src/Components/Inputs/TextEditor/ToolButton/ToolBtn.tsx +++ b/src/Components/Inputs/TextEditor/ToolButton/ToolBtn.tsx @@ -3,6 +3,7 @@ import HeadingsToolButton from './HeadingsToolBtn'; import DefaultToolButton from './DefaultToolBtn'; import { Command, isCommand } from './helpers'; import VideoToolButton from './VideoToolBtn'; +import LinkToolButton from './LinkToolBtn'; interface Props { cmd: Command @@ -43,6 +44,10 @@ export default function ToolButton({ cmd, if (cmd === 'img') return + + if (cmd === 'link') + return + return diff --git a/src/Components/Inputs/TextEditor/ToolButton/helpers.ts b/src/Components/Inputs/TextEditor/ToolButton/helpers.ts index c7dad6d..30e3142 100644 --- a/src/Components/Inputs/TextEditor/ToolButton/helpers.ts +++ b/src/Components/Inputs/TextEditor/ToolButton/helpers.ts @@ -1,5 +1,5 @@ import { FiBold, FiItalic, FiType, FiUnderline, FiAlignCenter, FiAlignLeft, FiAlignRight, FiCode } from 'react-icons/fi' -import { FaListOl, FaListUl, FaUndo, FaRedo, FaImage, FaYoutube, FaQuoteLeft } from 'react-icons/fa' +import { FaListOl, FaListUl, FaUndo, FaRedo, FaImage, FaYoutube, FaQuoteLeft, FaLink } from 'react-icons/fa' import { BiCodeCurly } from 'react-icons/bi'; @@ -97,6 +97,12 @@ export const cmdToBtn = { tip: "Insert Image", Icon: FaImage, }, + link: { + cmd: 'insertLink', + activeCmd: 'link', + tip: "Insert Link", + Icon: FaLink, + }, youtube: { cmd: 'addYouTubeVideo', diff --git a/src/Components/Modals/Modal/Modal.tsx b/src/Components/Modals/Modal/Modal.tsx index bb8b994..66fe3d3 100644 --- a/src/Components/Modals/Modal/Modal.tsx +++ b/src/Components/Modals/Modal/Modal.tsx @@ -22,7 +22,7 @@ export default function Modal({ onClose, children, ...props }: Props) { return dispatch(removeClosedModal(props.id))} diff --git a/src/Components/Modals/NoWeblnModal/NoWeblnModal.tsx b/src/Components/Modals/NoWeblnModal/NoWeblnModal.tsx new file mode 100644 index 0000000..1037ffc --- /dev/null +++ b/src/Components/Modals/NoWeblnModal/NoWeblnModal.tsx @@ -0,0 +1,69 @@ +import { motion } from 'framer-motion' +import { useAppSelector } from 'src/utils/hooks'; +import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer' +import Button from 'src/Components/Button/Button' +import { IoClose } from 'react-icons/io5'; +import NutImg from './nut.png' +import AlbyImg from './alby.png' + +export default function NoWeblnModal({ onClose, direction, ...props }: ModalCard) { + + const isMobile = useAppSelector(s => s.ui.isMobileScreen); + + + let content: JSX.Element; + + if (isMobile) + content = <> +
+ Nut images +
+

+ Oops! Looks like you’re browsing on mobile. +

+

+ In order to use BOLT🔩FUN’s voting button, you need to use a lightning browser wallet like Alby. You can download the extension on your desktop and try again. +

+ + else + content = <> +
+ Nut images +
+

+ Oops! Looks like you don’t have Alby installed +

+

+ In order to use BOLT🔩FUN’s voting button, you’ll need to use a lightning browser wallet like Alby. Download it to continue. +

+ + + + + return ( + + +

No WebLN Detected

+ {content} +
+ ) +} diff --git a/src/Components/Modals/NoWeblnModal/alby.png b/src/Components/Modals/NoWeblnModal/alby.png new file mode 100644 index 0000000..eca9dfc Binary files /dev/null and b/src/Components/Modals/NoWeblnModal/alby.png differ diff --git a/src/Components/Modals/NoWeblnModal/index.ts b/src/Components/Modals/NoWeblnModal/index.ts new file mode 100644 index 0000000..4dd1808 --- /dev/null +++ b/src/Components/Modals/NoWeblnModal/index.ts @@ -0,0 +1,4 @@ + +import { lazyModal } from 'src/utils/helperFunctions'; + +export const { LazyComponent: NoWeblnModal } = lazyModal(() => import('./NoWeblnModal')) \ No newline at end of file diff --git a/src/Components/Modals/NoWeblnModal/nut.png b/src/Components/Modals/NoWeblnModal/nut.png new file mode 100644 index 0000000..0313442 Binary files /dev/null and b/src/Components/Modals/NoWeblnModal/nut.png differ diff --git a/src/Components/Navbar/NavDesktop.tsx b/src/Components/Navbar/NavDesktop.tsx index ae3e460..5bbc318 100644 --- a/src/Components/Navbar/NavDesktop.tsx +++ b/src/Components/Navbar/NavDesktop.tsx @@ -5,16 +5,14 @@ import ASSETS from "src/assets"; import Search from "./Search/Search"; import IconButton from "../IconButton/IconButton"; import { Link, useNavigate } from "react-router-dom"; -import { useRef, useState } from "react"; +import { useState } from "react"; import { Menu, MenuItem, MenuButton, - useMenuState, - ControlledMenu, } from '@szhsin/react-menu'; import '@szhsin/react-menu/dist/index.css'; -import { FiChevronDown, FiLogIn } from "react-icons/fi"; +import { FiChevronDown } from "react-icons/fi"; import Avatar from "src/features/Profiles/Components/Avatar/Avatar"; import { createRoute } from "src/utils/routing"; import Button from "../Button/Button"; @@ -41,7 +39,7 @@ export default function NavDesktop() { return (