merge: pull request #118 from peakshift/dev

Navigation component, redesigned profile edit, better carousel
This commit is contained in:
Johns Beharry
2022-08-23 09:06:33 +02:00
committed by GitHub
97 changed files with 2395 additions and 1061 deletions

3
.gitignore vendored
View File

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

View File

@@ -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(),

View File

@@ -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()
}

View File

@@ -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")
}

View File

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

View File

@@ -1,2 +0,0 @@
REACT_APP_FOR_GITHUB=true
REACT_APP_API_END_POINT = https://makers.bolt.fun/.netlify/functions

165
package-lock.json generated
View File

@@ -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"
}

View File

@@ -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",

View File

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

View File

@@ -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?
}
// -----------------

View File

@@ -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,
}

51
prisma/seed/helpers.js Normal file
View File

@@ -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,
}

View File

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

View File

@@ -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() {
</Helmet>
<Suspense fallback={<LoadingPage />}>
<Routes>
<Route path="/blog/create-post" element={<ProtectedRoute><CreatePostPage /></ProtectedRoute>} />
<Route path={PAGES_ROUTES.blog.createPost} element={<ProtectedRoute><CreatePostPage /></ProtectedRoute>} />
<Route element={<NavbarLayout />}>
<Route path="/products/hottest" element={<HottestPage />} />
<Route path="/products/category/:id" element={<CategoryPage />} />
<Route path="/products" element={<ExplorePage />} />
<Route path={PAGES_ROUTES.projects.hottest} element={<HottestPage />} />
<Route path={PAGES_ROUTES.projects.byCategoryId} element={<CategoryPage />} />
<Route path={PAGES_ROUTES.projects.default} element={<ExplorePage />} />
<Route path="/blog/post/:type/:id/*" element={<PostDetailsPage />} />
<Route path="/blog" element={<FeedPage />} />
<Route path={PAGES_ROUTES.blog.postById} element={<PostDetailsPage />} />
<Route path={PAGES_ROUTES.blog.feed} element={<FeedPage />} />
<Route path="/hackathons" element={<HackathonsPage />} />
<Route path={PAGES_ROUTES.hackathons.default} element={<HackathonsPage />} />
<Route path="/donate" element={<DonatePage />} />
<Route path={PAGES_ROUTES.donate.default} element={<DonatePage />} />
<Route path="/profile/:id/*" element={<ProfilePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} />
<Route path={PAGES_ROUTES.profile.editProfile} element={<EditProfilePage />} />
<Route path={PAGES_ROUTES.profile.byId} element={<ProfilePage />} />
<Route path="/" element={<Navigate to="/products" />} />
<Route path={PAGES_ROUTES.auth.login} element={<LoginPage />} />
<Route path={PAGES_ROUTES.auth.logout} element={<LogoutPage />} />
<Route path="/" element={<Navigate to={PAGES_ROUTES.projects.default} />} />
</Route>
</Routes>

View File

@@ -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<typeof Card>;
const Template: ComponentStory<typeof Card> = (args) => <Card {...args} > <p className="text-body4 text-gray-700">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!</p>
</Card>
export const Default = Template.bind({});
export const Primary = Template.bind({});
Primary.args = {
onlyMd: true
}

View File

@@ -0,0 +1,33 @@
import React, { PropsWithChildren } from 'react'
interface Props extends React.ComponentPropsWithoutRef<'div'> {
onlyMd?: boolean;
defaultPadding?: boolean
}
const Card = React.forwardRef<HTMLDivElement, PropsWithChildren<Props>>(({
onlyMd = false,
defaultPadding = true,
className,
...props
}, ref) => {
return (
<div
{...props}
ref={ref}
className={`
${onlyMd ?
`md:bg-white md:rounded-16 md:border-2 border-gray-200 ${defaultPadding && "md:p-24"}` :
`bg-white rounded-12 md:rounded-16 border-2 border-gray-200 ${defaultPadding && "p-16 md:p-24"}`
}
${className}
`}
></div>
)
})
export default Card;

View File

@@ -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<Props, State> {
public render() {
if (this.state.hasError) {
return <h1>Sorry.. there was an error</h1>;
return <div className="page-container">
<ErrorMessage message={
<p className="text-body3">
Sorry, something went wrong...😵
<br />
Try refreshing the page.
</p>
} type="unknown"></ErrorMessage>
</div>;
}
return this.props.children;

View File

@@ -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 (
<div className="bg-red-50 border border-red-500 rounded-12 text-gray-900 px-20 py-36 flex flex-col items-center ">
<p >
<div >
{messageToShow}
</p>
</div>
<a href='/' className='text-primary-500 mt-36 underline' >Back to home page</a>
</div>
)

View File

@@ -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<typeof InsertLinkModal>;
const Template: ComponentStory<typeof InsertLinkModal> = (args) => <InsertLinkModal {...args} />;
export const Default = Template.bind({});

View File

@@ -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<IFormInputs>({
defaultValues: {
href: '',
text: '',
},
resolver: yupResolver(schema),
mode: 'onBlur',
});
const dispatch = useAppDispatch();
const onSubmit: SubmitHandler<IFormInputs> = data => {
const action = Object.assign({}, callbackAction);
action.payload = { text: data.text, href: data.href }
dispatch(action)
onClose?.();
};
return (
<motion.div
custom={direction}
variants={modalCardVariants}
initial='initial'
animate="animate"
exit='exit'
className="modal-card max-w-[660px] p-24 rounded-xl relative"
>
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
<h2 className='text-h5 font-bold'>Insert Link</h2>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="grid md:grid-cols-2 gap-16 mt-32">
<div>
<p className="text-body5">
Link Text
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='My Website'
{...register('text')}
/>
</div>
{errors.text && <p className="input-error">
{errors.text.message}
</p>}
</div>
<div>
<p className="text-body5">
Link URL
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='https://www.website.com'
{...register('href')}
/>
</div>
{errors.href && <p className="input-error">
{errors.href.message}
</p>}
</div>
</div>
<div className="flex gap-16 justify-end mt-32">
<Button onClick={onClose}>
Cancel
</Button>
<Button type='submit' color='primary' >
Insert
</Button>
</div>
</form>
</motion.div>
)
}

View File

@@ -0,0 +1,4 @@
import { lazyModal } from 'src/utils/helperFunctions';
export const { LazyComponent: InsertLinkModal } = lazyModal(() => import('./InsertLinkModal'))

View File

@@ -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 (
<button
type='button'
data-tip={tip}
className={`
${classes.button}
${classes.enabled}
`}
onClick={onClick}
>
<Icon className={classes.icon} />
</button>
)
}

View File

@@ -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 <ImageToolButton classes={classes} />
if (cmd === 'link')
return <LinkToolButton classes={classes} />
return <DefaultToolButton classes={classes} cmd={cmd} />

View File

@@ -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',

View File

@@ -22,7 +22,7 @@ export default function Modal({ onClose, children, ...props }: Props) {
return <ReactModal
isOpen={props.isOpen}
onRequestClose={onClose}
overlayClassName='fixed w-full inset-0 overflow-x-hidden z-[2020]'
overlayClassName='fixed w-full inset-0 overflow-x-hidden z-[2020] no-scrollbar'
className=' '
closeTimeoutMS={1000}
onAfterClose={() => dispatch(removeClosedModal(props.id))}

View File

@@ -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 = <>
<div className="flex justify-center my-24">
<img
src={NutImg}
className='w-full max-w-[164px] aspect-square object-cover'
alt="Nut images" />
</div>
<h3 className="text-h4 font-bolder">
Oops! Looks like youre browsing on mobile.
</h3>
<p className="text-body4 text-gray-600 mt-8">
In order to use BOLT🔩FUNs voting button, you need to use a lightning browser wallet like Alby. You can download the extension on your desktop and try again.
</p>
</>
else
content = <>
<div className="flex justify-center my-24">
<img
src={AlbyImg}
className='w-full max-w-[164px] aspect-square object-cover'
alt="Nut images" />
</div>
<h3 className="text-h4 font-bolder">
Oops! Looks like you dont have Alby installed
</h3>
<p className="text-body4 text-gray-600 mt-8">
In order to use BOLT🔩FUNs voting button, youll need to use a lightning browser wallet like Alby. Download it to continue.
</p>
<Button
color='black'
fullWidth
newTab
className='mt-32'
href='https://getalby.com'
>Download Alby</Button>
</>
return (
<motion.div
custom={direction}
variants={modalCardVariants}
initial='initial'
animate="animate"
exit='exit'
className="modal-card max-w-[343px] p-24 rounded-xl relative"
>
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
<h2 className='text-h5 font-bold'>No WebLN Detected</h2>
{content}
</motion.div>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,4 @@
import { lazyModal } from 'src/utils/helperFunctions';
export const { LazyComponent: NoWeblnModal } = lazyModal(() => import('./NoWeblnModal'))

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -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 (
<nav className="bg-white py-16 flex items-center w-full min-w-full">
<div className="page-container flex items-center !p-0">
<div className="content-container flex items-center">
<Link to="/">
<h2 className="text-h5 font-bold mr-40 lg:mr-64">
<img className='h-40' src={ASSETS.Logo} alt="Bolt fun logo" />
@@ -49,8 +47,8 @@ export default function NavDesktop() {
</Link>
<ul className="flex gap-32 xl:gap-64">
<li className="relative">
<Link to={'/products'} className='text-body4 font-bold hover:text-primary-600'>
Products
<Link to={'/projects'} className='text-body4 font-bold hover:text-primary-600'>
Projects
</Link>
</li>
<li>
@@ -161,13 +159,15 @@ export default function NavDesktop() {
: <Button className="ml-16 py-12 px-16 lg:px-20" onClick={onConnectWallet}><AiFillThunderbolt className='inline-block text-thunder transform scale-125' /> Connect Wallet </Button>
} */}
{currentSection === 'products' && <IconButton className='mr-16 self-center' onClick={openSearch}>
{currentSection === 'apps' && <IconButton className='mr-16 self-center' onClick={openSearch}>
<BsSearch className='scale-125 text-gray-400' />
</IconButton>}
</motion.div>
{curUser !== undefined &&
(curUser ?
<Menu
align="end"
offsetY={4}
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={40} /> </MenuButton>}>
<MenuItem
@@ -178,7 +178,17 @@ export default function NavDesktop() {
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Profile
👾 Profile
</MenuItem>
<MenuItem
href="/edit-profile"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/edit-profile");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Settings
</MenuItem>
<MenuItem
href="/logout"
@@ -188,12 +198,12 @@ export default function NavDesktop() {
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Logout
👋 Logout
</MenuItem>
</Menu>
:
<Button color="primary" href="/login">
<Button size="sm" color="white" href="/login">
Connect
</Button>
)

View File

@@ -16,8 +16,6 @@ import { Menu, MenuButton, MenuItem } from "@szhsin/react-menu";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { createRoute } from "src/utils/routing";
const NAV_HEIGHT = "67px"
const navBtnVariant = {
menuHide: { rotate: 90, opacity: 0 },
menuShow: { rotate: 0, opacity: 1 },
@@ -75,15 +73,28 @@ export default function NavMobile() {
return (
<div className={`${styles.navMobile}`}>
<nav className={`bg-white h-[67px] w-full py-16`}>
<div className="page-container flex justify-between items-center !p-0">
<Link to="/">
<img className='h-32' src={ASSETS.Logo} alt="Bolt fun logo" />
</Link>
<div className="content-container flex justify-between items-center">
<div className="flex-1 flex content-start">
<IconButton className='auto text-2xl w-[50px] h-[50px] hover:bg-gray-200 self-center' onClick={() => toggleDrawerOpen()}>
{!drawerOpen ? (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='menuHide' animate='menuShow'><FiMenu /></motion.div>)
: (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='closeHide' animate='closeShow'><GrClose /></motion.div>)}
</IconButton>
</div>
<div className="flex-[2] flex justify-center">
<Link to="/">
<img className='max-h-32' src={ASSETS.Logo} alt="Bolt fun logo" />
</Link>
</div>
<div className="flex-1 flex justify-end">
{curUser ?
<div className="ml-auto"></div>
{curUser !== undefined &&
(curUser &&
<Menu
align="end"
offsetY={4}
menuClassName='!p-8 !rounded-12'
menuButton={<MenuButton ><Avatar src={curUser.avatar} width={32} /> </MenuButton>}>
<MenuItem
@@ -94,7 +105,17 @@ export default function NavMobile() {
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Profile
👾 Profile
</MenuItem>
<MenuItem
href="/edit-profile"
onClick={(e) => {
e.syntheticEvent.preventDefault();
navigate("/edit-profile");
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Settings
</MenuItem>
<MenuItem
href="/logout"
@@ -104,16 +125,16 @@ export default function NavMobile() {
}}
className='!p-16 font-medium flex gap-16 hover:bg-gray-100 !rounded-12'
>
Logout
👋 Logout
</MenuItem>
</Menu>
:
<Button size="sm" color="none" className="!text-body5 whitespace-nowrap" href="/login">
Connect
</Button>
}</div>
)
}
<IconButton className='auto text-2xl w-[50px] h-[50px] hover:bg-gray-200 self-center' onClick={() => toggleDrawerOpen()}>
{!drawerOpen ? (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='menuHide' animate='menuShow'><FiMenu /></motion.div>)
: (<motion.div key={drawerOpen ? 1 : 0} variants={navBtnVariant} initial='closeHide' animate='closeShow'><GrClose /></motion.div>)}
</IconButton>
</div>
</nav>
@@ -132,36 +153,15 @@ export default function NavMobile() {
>
<div className="flex flex-col gap-16 py-16">
<Search onResultClick={() => toggleDrawerOpen(false)} />
{
curUser ?
<Button
color="gray"
fullWidth
className="!py-16 px-40 rounded-12 "
href='/logout'
onClick={() => toggleDrawerOpen()}
>
Logout
</Button> :
<Button
color="primary"
fullWidth
className="!py-16 px-40 rounded-12 "
href='/login'
onClick={() => toggleDrawerOpen()}
>
Connect your lightning wallet
</Button>
}
</div>
<ul className="px-32 flex flex-col py-16 gap-32 border-t">
<ul className="flex flex-col py-16 gap-32 border-t">
<li className="relative">
<Link
to={'/products'}
to={'/projects'}
onClick={() => toggleDrawerOpen(false)}
className='text-body4 font-bold hover:text-primary-600'>
Products
Projects
</Link>
</li>
<li>
@@ -188,8 +188,8 @@ export default function NavMobile() {
onClick={() => toggleDrawerOpen(false)}
className='font-medium flex gap-16 !rounded-12 '
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">🏼</span>
<div className="shrink-0 bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2 shrink-0">🏼</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
@@ -204,8 +204,8 @@ export default function NavMobile() {
className='font-medium flex gap-16 !rounded-12 opacity-60'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">💬</span>
<div className="shrink-0 bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2 shrink-0">💬</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
@@ -221,8 +221,8 @@ export default function NavMobile() {
onClick={() => toggleDrawerOpen(false)}
className='font-medium flex gap-16 !rounded-12'
>
<div className="bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2">🏆</span>
<div className="shrink-0 bg-white border border-gray-100 w-48 h-48 rounded-full flex justify-center items-center">
<span className="text-body2 shrink-0">🏆</span>
</div>
<div>
<p className="text-body4 text-black font-medium">
@@ -254,31 +254,40 @@ export default function NavMobile() {
Donate
</Link>
</li>
{curUser &&
<li className="relative">
<Link
to={'/logout'}
onClick={() => toggleDrawerOpen(false)}
className='text-body4 font-bold hover:text-primary-600'>
Logout 👋
</Link>
</li>}
</ul>
<ul className="px-16 py-16 pb-32 flex flex-wrap gap-y-12 border-t pt-32 mt-auto">
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2">
<a href="/">About Us</a>
<a href="/#">About Us</a>
</li>
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2">
<a href="/">Support</a>
<a href="/#">Support</a>
</li>
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2">
<a href="/">Press</a>
<a href="/#">Press</a>
</li>
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2">
<a href="/">Contacts</a>
<a href="/#">Contacts</a>
</li>
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2">
<a href="/">Careers</a>
<a href="/#">Careers</a>
</li>
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2">
<a href="/">Sitemap</a>
<a href="/#">Sitemap</a>
</li>
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2">
<a href="/">Legal</a>
<a href="/#">Legal</a>
</li>
<li className="text-body4 text-gray-500 hover:text-gray-700 w-1/2">
<a href="/">Cookies Settings</a>
<a href="/#">Cookies Settings</a>
</li>
</ul>

View File

@@ -1,7 +1,8 @@
import NavMobile from "./NavMobile";
import { MdComment, MdHomeFilled, MdLocalFireDepartment } from "react-icons/md";
import { useEffect, } from "react";
import { useAppDispatch, useMediaQuery } from "src/utils/hooks";
import { useCallback, useEffect, } from "react";
import { useAppDispatch, useMediaQuery, useResizeListener } from "src/utils/hooks";
import { setNavHeight } from "src/redux/features/ui.slice";
import NavDesktop from "./NavDesktop";
import { MEDIA_QUERIES } from "src/utils/theme/media_queries";
@@ -43,18 +44,24 @@ export default function Navbar() {
const isLargeScreen = useMediaQuery(MEDIA_QUERIES.isLarge)
useEffect(() => {
const updateNavHeight = useCallback(() => {
const nav = document.querySelector("nav");
if (nav) {
const navStyles = getComputedStyle(nav);
if (navStyles.display !== "none") {
dispatch(setNavHeight(nav.clientHeight));
document.documentElement.style.setProperty('--navHeight', nav.clientHeight + 'px')
}
}
}, [dispatch])
useEffect(() => {
updateNavHeight();
}, [updateNavHeight]);
useResizeListener(updateNavHeight)
return (
<div className="sticky top-0 left-0 w-full z-[2010]">

View File

@@ -55,7 +55,7 @@ type BtnState = 'ready' | 'voting' | 'loading' | "success" | "fail";
export default function VoteButton({
votes,
onVote = () => { },
fillType = 'leftRight',
fillType = 'background',
direction = 'horizontal',
disableCounter = false,
disableShake = true,

View File

@@ -17,56 +17,65 @@ export const useDonate = () => {
onError: (error: any) => void,
onSetteled: () => void
}>) => {
setPaymentStatus(PaymentStatus.FETCHING_PAYMENT_DETAILS)
donateMutation({
variables: {
amountInSat: amount
},
onCompleted: async (donationData) => {
try {
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
const webln = await Wallet_Service.getWebln()
const paymentResponse = await webln.sendPayment(donationData.donate.payment_request);
setPaymentStatus(PaymentStatus.PAID);
//Confirm Voting payment
confirmDonation({
variables: {
paymentRequest: donationData.donate.payment_request,
preimage: paymentResponse.preimage
},
onCompleted: () => {
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
config?.onSuccess?.();
config?.onSetteled?.()
},
onError: (error) => {
console.log(error)
setPaymentStatus(PaymentStatus.NETWORK_ERROR);
config?.onError?.(error);
config?.onSetteled?.();
alert("A network error happened while confirming the payment...")
},
refetchQueries: [
'DonationsStats'
]
})
} catch (error) {
setPaymentStatus(PaymentStatus.CANCELED);
config?.onError?.(error);
config?.onSetteled?.();
alert("Payment rejected by user")
Wallet_Service.getWebln()
.then(webln => {
if (!webln) {
config?.onError?.(new Error('No WebLN Detetcted'))
config?.onSetteled?.()
return
}
},
onError: (error) => {
console.log(error);
setPaymentStatus(PaymentStatus.NETWORK_ERROR);
config?.onError?.(error);
config?.onSetteled?.();
alert("A network error happened...")
}
})
setPaymentStatus(PaymentStatus.FETCHING_PAYMENT_DETAILS)
donateMutation({
variables: {
amountInSat: amount
},
onCompleted: async (donationData) => {
try {
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
const paymentResponse = await webln.sendPayment(donationData.donate.payment_request);
setPaymentStatus(PaymentStatus.PAID);
//Confirm Voting payment
confirmDonation({
variables: {
paymentRequest: donationData.donate.payment_request,
preimage: paymentResponse.preimage
},
onCompleted: () => {
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
config?.onSuccess?.();
config?.onSetteled?.()
},
onError: (error) => {
console.log(error)
setPaymentStatus(PaymentStatus.NETWORK_ERROR);
config?.onError?.(error);
config?.onSetteled?.();
alert("A network error happened while confirming the payment...")
},
refetchQueries: [
'DonationsStats'
]
})
} catch (error) {
setPaymentStatus(PaymentStatus.CANCELED);
config?.onError?.(error);
config?.onSetteled?.();
alert("Payment rejected by user")
}
},
onError: (error) => {
console.log(error);
setPaymentStatus(PaymentStatus.NETWORK_ERROR);
config?.onError?.(error);
config?.onSetteled?.();
alert("A network error happened...")
}
})
})
}, [confirmDonation, donateMutation]);
const isLoading = paymentStatus !== PaymentStatus.DEFAULT && paymentStatus !== PaymentStatus.PAYMENT_CONFIRMED && paymentStatus !== PaymentStatus.NOT_PAID && paymentStatus !== PaymentStatus.NETWORK_ERROR && paymentStatus !== PaymentStatus.CANCELED

View File

@@ -2,7 +2,6 @@
import { useState } from 'react'
import Button from 'src/Components/Button/Button'
import { useGetHackathonsQuery } from 'src/graphql'
import { useAppSelector } from 'src/utils/hooks'
import HackathonsList from '../../Components/HackathonsList/HackathonsList'
import SortByFilter from '../../Components/SortByFilter/SortByFilter'
import styles from './styles.module.scss'
@@ -21,9 +20,6 @@ export default function HackathonsPage() {
tag: Number(tagFilter)
},
})
const { navHeight } = useAppSelector((state) => ({
navHeight: state.ui.navHeight
}));
return (
<>
@@ -35,11 +31,7 @@ export default function HackathonsPage() {
className={`page-container pt-16 w-full ${styles.grid}`}
>
<aside className='no-scrollbar'>
<div className="sticky flex flex-col gap-24 md:overflow-y-scroll"
style={{
top: `${navHeight + 16}px`,
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
}}>
<div className="flex flex-col gap-24 md:overflow-y-scroll sticky-side-element">
<h1 id='title' className="text-body1 lg:text-h2 font-bolder">Hackathons 🏆</h1>
<SortByFilter
filterChanged={setSortByFilter}

View File

@@ -7,6 +7,7 @@ import DOMPurify from 'dompurify';
import { Vote_Item_Type } from "src/graphql";
import { useVote } from "src/utils/hooks";
import { useState } from "react";
import Card from "src/Components/Card/Card";
interface Props {
@@ -25,13 +26,12 @@ export default function CommentCard({ comment, canReply, onReply }: Props) {
});
return (
<div className="border-2 border-gray-200 rounded-12 md:rounded-16 p-24">
<Card>
<Header author={comment.author} date={new Date(comment.created_at).toISOString()} />
<div
className="text-body4 mt-16 whitespace-pre-line"
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked.parse(comment.body)) }}
>
</div>
<div className="flex gap-24 mt-16 items-center">
<VoteButton
@@ -46,6 +46,6 @@ export default function CommentCard({ comment, canReply, onReply }: Props) {
<BiComment /> <span className="align-middle text-body5">Reply</span>
</button>}
</div>
</div>
</Card>
)
}

View File

@@ -1,18 +1,16 @@
import React, { useEffect, useMemo, useState } from 'react'
import { useState } from 'react'
import CommentRoot from '../Comment/Comment'
import AddComment from '../AddComment/AddComment'
import { Comment, } from '../types'
import { createWorkerFactory, useWorker } from '@shopify/react-web-worker'
import { useAppSelector } from "src/utils/hooks";
import * as CommentsWorker from './comments.worker'
import { Post_Type } from 'src/graphql'
import useComments from './useComments'
import IconButton from 'src/Components/IconButton/IconButton'
import { AiOutlineClose } from 'react-icons/ai'
import { Link } from 'react-router-dom'
import { createRoute } from 'src/utils/routing'
import { Link, useLocation } from 'react-router-dom'
import { createRoute, PAGES_ROUTES } from 'src/utils/routing'
import Preferences from 'src/services/preferences.service'
import Card from 'src/Components/Card/Card';
// const createWorker = createWorkerFactory(() => import('./comments.worker'));
@@ -24,24 +22,11 @@ interface Props {
export default function CommentsSection({ type, id }: Props) {
// const worker = useWorker(createWorker);
// const commentsTree = useMemo(() => convertCommentsToTree(comments), [comments])
// const [commentsTree, setCommentsTree] = useState<Comment[]>([])
const user = useAppSelector(state => state.user.me);
const [showTooltip, setShowTooltip] = useState(Preferences.get('showNostrCommentsTooltip'))
// const filter = useMemo(() => `boltfun ${type}_comment ${id}` + (process.env.NODE_ENV === 'development' ? ' dev' : ""), [id, type])
const [showTooltip, setShowTooltip] = useState(Preferences.get('showNostrCommentsTooltip'));
const location = useLocation()
// useEffect(() => {
// CommentsWorker.connect();
// const unsub = CommentsWorker.sub(filter, (newComments) => {
// setCommentsTree(newComments)
// })
// return () => {
// unsub();
// }
// }, [filter]);
const { commentsTree, postComment, connectionStatus } = useComments({ type, id })
const handleNewComment = async (content: string, parentId?: string) => {
@@ -60,8 +45,7 @@ export default function CommentsSection({ type, id }: Props) {
return (
<div className="border-2 border-gray-200 rounded-12 md:rounded-16 p-32 bg-white">
<Card onlyMd>
<div className="flex flex-wrap justify-between">
<h6 className="text-body2 font-bolder">Discussion</h6>
{connectionStatus.status === 'Connected' && <div className="bg-green-50 text-green-500 text-body5 font-medium py-4 px-12 rounded-48"> &#8226; <span className="hidden md:inline">Connected to {connectionStatus.connectedRelaysCount} relays</span> 📡</div>}
@@ -75,12 +59,23 @@ export default function CommentsSection({ type, id }: Props) {
<IconButton className='shrink-0 self-start' onClick={closeTooltip}><AiOutlineClose className='text-gray-600' /></IconButton>
</div>}
{!!user && <div className="mt-24">
<AddComment
placeholder='Leave a comment...'
onSubmit={content => handleNewComment(content)}
avatar={user.avatar}
/>
{<div className="mt-24 relative">
<div className={!user ? "blur-[2px]" : ""}>
<AddComment
placeholder='Leave a comment...'
onSubmit={content => handleNewComment(content)}
avatar={user?.avatar ?? 'https://avatars.dicebear.com/api/bottts/Default.svg'}
/>
</div>
{!user && <div className="absolute inset-0 bg-gray-400 bg-opacity-50 rounded-12 flex flex-col justify-center items-center">
<Link
className='bg-white rounded-12 px-24 py-12'
to={PAGES_ROUTES.auth.login}
state={{
from: location.pathname
}}
>Connect with to comment</Link>
</div>}
</div>}
<div className='flex flex-col gap-16 mt-32'>
@@ -93,6 +88,6 @@ export default function CommentsSection({ type, id }: Props) {
onReply={content => handleNewComment(content, comment.nostr_id.toString())}
/>)}
</div>
</div>
</Card>
)
}

View File

@@ -166,7 +166,7 @@ async function signEvent(event: any) {
}
async function confirmPublishingEvent(event: any) {
const res = await fetch(CONSTS.apiEndpoint + '/nostr-confirm-event', {
await fetch(CONSTS.apiEndpoint + '/nostr-confirm-event', {
method: "post",
body: JSON.stringify({ event }),
credentials: 'include',
@@ -174,8 +174,8 @@ async function confirmPublishingEvent(event: any) {
'Content-Type': 'application/json'
},
});
const data = await res.json()
return data.event;
}

View File

@@ -1 +1 @@
export { default as CommentsSection } from './CommentsSection/CommentsSection'
export { default } from './CommentsSection/CommentsSection'

View File

@@ -7,6 +7,7 @@ import { Author, Tag, Vote_Item_Type } from 'src/graphql';
import Badge from "src/Components/Badge/Badge"
import { createRoute } from "src/utils/routing"
import { BiComment } from "react-icons/bi"
import Card from "src/Components/Card/Card"
export type StoryCardType = Pick<Story,
@@ -34,7 +35,7 @@ export default function StoryCard({ story }: Props) {
});
return (
<div className="bg-white rounded-12 overflow-hidden border-2 border-gray-200">
<Card className="overflow-hidden" defaultPadding={false}>
{story.cover_image && <img src={story.cover_image} className='h-[200px] w-full object-cover' alt="" />}
<div className="p-24">
<Header author={story.author} date={story.createdAt} />
@@ -56,6 +57,7 @@ export default function StoryCard({ story }: Props) {
</div>
</div>
</div>
</div>
</Card>
)
}

View File

@@ -1,5 +1,6 @@
import Skeleton from 'react-loading-skeleton'
import { Link } from 'react-router-dom'
import Card from 'src/Components/Card/Card'
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
import { useTrendingPostsQuery } from 'src/graphql'
import { random } from 'src/utils/helperFunctions'
@@ -12,7 +13,7 @@ export default function TrendingCard() {
return (
<div className="bg-white rounded-12 border-2 border-gray-200 p-16">
<Card onlyMd>
<h3 className="text-body2 font-bolder mb-16">Trending on BOLT.FUN</h3>
<ul className='flex flex-col'>
{
@@ -34,6 +35,6 @@ export default function TrendingCard() {
}
)}
</ul>
</div>
</Card>
)
}

View File

@@ -17,9 +17,10 @@ export default function Toolbar() {
{/* <TextEditorComponents.ToolButton cmd='leftAlign' />
<TextEditorComponents.ToolButton cmd='centerAlign' />
<TextEditorComponents.ToolButton cmd='rightAlign' /> */}
<TextEditorComponents.ToolButton cmd='blockquote' />
<TextEditorComponents.ToolButton cmd='link' />
<TextEditorComponents.ToolButton cmd='code' />
<TextEditorComponents.ToolButton cmd='codeBlock' />
<TextEditorComponents.ToolButton cmd='blockquote' />
<TextEditorComponents.ToolButton cmd='bulletList' />
<TextEditorComponents.ToolButton cmd='orderedList' />
<TextEditorComponents.ToolButton cmd='img' />

View File

@@ -2,7 +2,7 @@
import { useUpdateEffect } from '@react-hookz/web'
import { useState } from 'react'
import { useFeedQuery } from 'src/graphql'
import { useAppSelector, useInfiniteQuery, usePreload } from 'src/utils/hooks'
import { useInfiniteQuery, usePreload } from 'src/utils/hooks'
import PostsList from '../../Components/PostsList/PostsList'
import TrendingCard from '../../Components/TrendingCard/TrendingCard'
import PopularTagsFilter, { FilterTag } from './PopularTagsFilter/PopularTagsFilter'
@@ -34,10 +34,6 @@ export default function FeedPage() {
usePreload('PostPage');
const { navHeight, isLoggedIn } = useAppSelector((state) => ({
navHeight: state.ui.navHeight,
isLoggedIn: Boolean(state.user.me),
}));
return (
@@ -76,11 +72,7 @@ export default function FeedPage() {
/>
</div>
<aside id='categories' className='no-scrollbar'>
<div className="sticky md:overflow-y-scroll"
style={{
top: `${navHeight + 16}px`,
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
}}>
<div className="pb-16 md:overflow-y-scroll sticky-side-element">
<Button
href='/blog/create-post'
color='primary'
@@ -98,12 +90,7 @@ export default function FeedPage() {
</div>
</aside>
<aside id='side' className='no-scrollbar'>
<div className="sticky flex flex-col gap-24"
style={{
top: `${navHeight + 16}px`,
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
overflowY: "scroll",
}}>
<div className="pb-16 flex flex-col gap-24 overflow-y-auto sticky-side-element" >
<TrendingCard />
<div className='min-h-[300px] text-white flex flex-col justify-end p-24 rounded-12 relative overflow-hidden'
style={{

View File

@@ -4,6 +4,7 @@ import Slider from 'src/Components/Slider/Slider';
import { Tag, usePopularTagsQuery } from 'src/graphql';
import { MEDIA_QUERIES } from 'src/utils/theme';
import { capitalize } from 'src/utils/helperFunctions';
import Card from 'src/Components/Card/Card';
export type FilterTag = Pick<Tag, 'id' | 'title' | "icon">
@@ -30,7 +31,8 @@ export default function PopularTagsFilter({ value, onChange }: Props) {
return (
<div className='overflow-hidden'>
{isMdScreen ?
<div className='bg-white border-2 border-gray-200 rounded-12 p-16'>
<Card>
<p className="text-body2 font-bolder text-black mb-16">Popular Tags</p>
<ul className=' flex flex-col gap-16'>
{tagsQuery.loading ?
@@ -60,7 +62,8 @@ export default function PopularTagsFilter({ value, onChange }: Props) {
</span>
</li>)}
</ul>
</div>
</Card>
:
<>
{

View File

@@ -1,6 +1,7 @@
import dayjs from "dayjs";
import { Link } from "react-router-dom";
import Button from "src/Components/Button/Button";
import Card from "src/Components/Card/Card";
import { Author } from "src/features/Posts/types";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { trimText } from "src/utils/helperFunctions";
@@ -16,7 +17,7 @@ interface Props {
export default function AuthorCard({ author }: Props) {
return (
<div className="bg-white p-16 border-2 border-gray-200 rounded-12">
<Card>
<div className='flex gap-8'>
<Avatar width={48} src={author.avatar} />
<div className="overflow-hidden">
@@ -31,6 +32,6 @@ export default function AuthorCard({ author }: Props) {
className="mt-16">
Maker's Profile
</Button>
</div>
</Card>
)
}

View File

@@ -5,9 +5,10 @@ import styles from '../PageContent/styles.module.scss'
import Badge from "src/Components/Badge/Badge";
import { BiComment } from "react-icons/bi";
import { RiFlashlightLine } from "react-icons/ri";
import { CommentsSection } from "src/features/Posts/Components/Comments";
import { numberFormatter } from "src/utils/helperFunctions";
import { lazy } from "react";
const CommentsSection = lazy(() => import("src/features/Posts/Components/Comments"))
interface Props {
question: Question

View File

@@ -9,6 +9,7 @@ import { useAppSelector } from "src/utils/hooks";
import { useUpdateStory } from './useUpdateStory'
import { FaPen } from "react-icons/fa";
import DOMPurify from 'dompurify';
import Card from "src/Components/Card/Card";
interface Props {
@@ -26,9 +27,8 @@ export default function StoryPageContent({ story }: Props) {
return (
<>
<div id="content" className="bg-white md:p-32 md:border-2 border-gray-200 rounded-16 relative">
<div id="content" className="bg-white md:p-32 md:border-2 border-gray-200 rounded-16 relative"> </div>
<Card id="content" onlyMd className="relative">
{story.cover_image &&
<img src={story.cover_image}
className='w-full object-cover rounded-12 md:rounded-16 mb-16'
@@ -72,7 +72,8 @@ export default function StoryPageContent({ story }: Props) {
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(marked.parse(story.body)) }}
>
</div>
</div>
</Card>
{/* <div id="comments" className="mt-10 comments_col">
<CommentsSection comments={story.comments} />
</div> */}

View File

@@ -4,16 +4,17 @@ import { useParams } from 'react-router-dom'
import NotFoundPage from 'src/features/Shared/pages/NotFoundPage/NotFoundPage'
import { Post_Type, usePostDetailsQuery } from 'src/graphql'
import { capitalize } from 'src/utils/helperFunctions'
import { useAppSelector, } from 'src/utils/hooks'
import { CommentsSection } from '../../Components/Comments'
import ScrollToTop from 'src/utils/routing/scrollToTop'
import TrendingCard from '../../Components/TrendingCard/TrendingCard'
import TrendingCard from 'src/features/Posts/Components/TrendingCard/TrendingCard'
import AuthorCard from './Components/AuthorCard/AuthorCard'
import PageContent from './Components/PageContent/PageContent'
import PostActions from './Components/PostActions/PostActions'
import PostDetailsPageSkeleton from './PostDetailsPage.skeleton'
import styles from './styles.module.scss'
import { lazy, Suspense } from 'react'
import { RotatingLines } from 'react-loader-spinner'
const CommentsSection = lazy(() => import( /* webpackChunkName: "comments_section" */ "src/features/Posts/Components/Comments"))
export default function PostDetailsPage() {
const { type: _type, id } = useParams();
@@ -27,9 +28,6 @@ export default function PostDetailsPage() {
skip: isNaN(Number(id)),
})
const { navHeight } = useAppSelector((state) => ({
navHeight: state.ui.navHeight
}));
if (postDetailsQuery.loading)
return <PostDetailsPageSkeleton />
@@ -50,11 +48,7 @@ export default function PostDetailsPage() {
className={`page-container grid pt-16 w-full gap-32 ${styles.grid}`}
>
<aside id='actions' className='no-scrollbar'>
<div className="sticky"
style={{
top: `${navHeight + 16}px`,
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
}}>
<div className="sticky-side-element">
<PostActions post={post} />
</div>
</aside>
@@ -62,18 +56,18 @@ export default function PostDetailsPage() {
<PageContent post={post} />
<aside id='author' className='no-scrollbar min-w-0'>
<div className="flex flex-col gap-24"
style={{
top: `${navHeight + 16}px`,
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
overflowY: "scroll",
}}>
<div className="flex flex-col gap-24 overflow-y-auto sticky-side-element">
<AuthorCard author={post.author} />
<TrendingCard />
<div className="hidden md:block"><TrendingCard /></div>
</div>
</aside>
<div id="comments">
<CommentsSection id={post.id} type={type as Post_Type} />
<Suspense fallback={
<div className="flex justify-center py-32"><RotatingLines strokeColor='#ddd' width="64" /></div>
}>
<CommentsSection id={post.id} type={type as Post_Type} />
</Suspense>
<div className="md:hidden mt-24"><TrendingCard /></div>
</div>
</div>
</>

View File

@@ -0,0 +1,19 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import AccountCard from './AccountCard';
export default {
title: 'Profiles/Profile Page/Account Card',
component: AccountCard,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof AccountCard>;
const Template: ComponentStory<typeof AccountCard> = (args) => <AccountCard {...args} ></AccountCard>
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -0,0 +1,36 @@
import Button from 'src/Components/Button/Button';
import { useAppDispatch } from 'src/utils/hooks';
import { openModal } from 'src/redux/features/modals.slice';
import Card from 'src/Components/Card/Card';
interface Props {
}
export default function AccountCard({ }: Props) {
const dispatch = useAppDispatch()
const connectNewWallet = () => {
dispatch(openModal({ Modal: "LinkingAccountModal" }))
}
return (
<Card>
<p className="text-body2 font-bold">🔒 Linking Accounts</p>
<div className='mt-24 flex flex-col gap-16'>
<p className="text-body3 font-bold">Linked Wallets</p>
<p className="text-body4 text-gray-600">
These are the wallets that you can login to this account from.
<br />
You can add a new wallet from the button below.
</p>
<Button color='primary' className='' onClick={connectNewWallet}>
Connect new wallet
</Button>
</div>
</Card>
)
}

View File

@@ -0,0 +1,143 @@
import { motion } from 'framer-motion'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
import { useEffect, useState } from "react"
import { Grid } from "react-loader-spinner";
import { CONSTS } from "src/utils";
import { QRCodeSVG } from 'qrcode.react';
import Button from "src/Components/Button/Button";
import { FiCopy } from "react-icons/fi";
import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard";
const fetchLnurlAuth = async () => {
const res = await fetch(CONSTS.apiEndpoint + '/get-login-url?action=link', {
credentials: 'include'
})
const data = await res.json()
return data;
}
const useLnurlQuery = () => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<any>(null);
const [data, setData] = useState<{ lnurl: string, session_token: string }>({ lnurl: '', session_token: '' })
useEffect(() => {
let timeOut: NodeJS.Timeout;
const doFetch = async () => {
const res = await fetchLnurlAuth();
if (!res?.encoded)
setError(true)
else {
setLoading(false);
setData({
lnurl: res.encoded,
session_token: res.session_token
});
timeOut = setTimeout(doFetch, 1000 * 60 * 2)
}
}
doFetch()
return () => clearTimeout(timeOut)
}, [])
return {
loadingLnurl: loading,
error,
data
}
}
export default function LinkingAccountModal({ onClose, direction, ...props }: ModalCard) {
const [copied, setCopied] = useState(false);
const { loadingLnurl, data: { lnurl }, error } = useLnurlQuery();
const clipboard = useCopyToClipboard()
useEffect(() => {
setCopied(false);
}, [lnurl])
const copyToClipboard = () => {
setCopied(true);
clipboard(lnurl);
}
let content = <></>
if (error)
content = <div className="flex flex-col gap-24 items-center">
<p className="text-body3 text-red-500 font-bold">Something wrong happened...</p>
<a href='/login' className="text body4 text-gray-500 hover:underline">Refresh the page</a>
</div>
else if (loadingLnurl)
content = <div className="flex flex-col gap-24 items-center">
<Grid color="var(--primary)" width="150" />
<p className="text-body3 font-bold">Fetching Lnurl-Auth...</p>
</div>
else
content =
<>
<p className="text-body1 font-bolder text-center">
Link your account
</p>
<QRCodeSVG
width={160}
height={160}
value={lnurl}
/>
<p className="text-gray-600 text-body4 text-center">
Scan this code or copy + paste it to your other lightning wallet to be able to login later with it to this account.
<br />
When done, click the button below to close this modal.
</p>
<div className="flex flex-col w-full gap-16">
{/* <a href={lnurl}
className='grow block text-body4 text-center text-white font-bolder bg-primary-500 hover:bg-primary-600 rounded-10 px-16 py-12 active:scale-90 transition-transform'
>Click to connect <IoRocketOutline /></a> */}
<Button
color='gray'
className='grow'
onClick={copyToClipboard}
fullWidth
>{copied ? "Copied" : "Copy"} <FiCopy /></Button>
<Button
color='primary'
onClick={onClose}
fullWidth
className='mt-16'
>
Done?
</Button>
</div>
</>
return (
<motion.div
custom={direction}
variants={modalCardVariants}
initial='initial'
animate="animate"
exit='exit'
className="modal-card w-full max-w-[326px] bg-white border-2 border-gray-200 rounded-16 p-16 flex flex-col gap-16 items-center"
>
{content}
</motion.div>
)
}

View File

@@ -0,0 +1,3 @@
import { lazyModal } from 'src/utils/helperFunctions';
export const { LazyComponent: LinkingAccountModal } = lazyModal(() => import('./LinkingAccountModal'))

View File

@@ -0,0 +1,109 @@
import { Navigate, NavLink, Route, Routes } from "react-router-dom";
import LoadingPage from "src/Components/LoadingPage/LoadingPage";
import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage";
import Slider from "src/Components/Slider/Slider";
import { useProfileQuery } from "src/graphql";
import { useAppSelector, useMediaQuery } from "src/utils/hooks";
import UpdateMyProfileTab from "./UpdateMyProfileTab/UpdateMyProfileTab";
import { Helmet } from 'react-helmet'
import { MEDIA_QUERIES } from "src/utils/theme";
import AccountCard from "./AccountCard/AccountCard";
import PreferencesTab from "./PreferencesTab/PreferencesTab";
import Card from "src/Components/Card/Card";
const links = [
{
text: "👾 My Profile",
path: 'my-profile',
},
// {
// text: "🙍‍♂️ Account",
// path: 'account',
// },
{
text: "⚙️ Preferences",
path: 'preferences',
}
]
export default function EditProfilePage() {
const userId = useAppSelector(state => state.user.me?.id)
const profileQuery = useProfileQuery({
variables: {
profileId: userId!,
},
skip: !userId,
})
const isMediumScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
if (!userId || profileQuery.loading)
return <LoadingPage />
if (!profileQuery.data?.profile)
return <NotFoundPage />
return (
<>
<Helmet>
<title>Settings</title>
<meta property="og:title" content='Settings' />
</Helmet>
<div className="page-container grid grid-cols-1 md:grid-cols-4 gap-24">
<aside>
{isMediumScreen ?
<Card className="sticky-side-element">
<p className="text-body2 font-bolder text-black mb-16">Edit maker profile</p>
<ul className=' flex flex-col gap-8'>
{links.map((link, idx) =>
<li key={idx}>
<NavLink
to={link.path}
className={({ isActive }) => `flex items-start rounded-8 cursor-pointer font-bold p-12
active:scale-95 transition-transform
${isActive ? 'bg-gray-100' : 'hover:bg-gray-50'}
`}
>
{link.text}
</NavLink>
</li>)}
</ul>
</Card>
:
<div className="border-b-2 border-gray-200">
<Slider>
{links.map((link, idx) =>
<NavLink
to={link.path}
key={idx}
className={`flex items-start cursor-pointer font-bold py-12
active:scale-95 transition-transform`}
style={({ isActive }) => ({
boxShadow: isActive ? '0px 2px var(--primary)' : 'none'
})}
>
{link.text}
</NavLink>
)}
</Slider>
</div>
}
</aside>
<main className="md:col-span-3">
<Routes>
<Route index element={<Navigate to='my-profile' />} />
<Route path='my-profile' element={<UpdateMyProfileTab data={profileQuery.data.profile} />} />
<Route path='account' element={<AccountCard />} />
<Route path='preferences' element={<PreferencesTab nostr_prv_key={profileQuery.data.profile.nostr_prv_key} nostr_pub_key={profileQuery.data.profile.nostr_pub_key} isOwner={true} />
} />
</Routes>
</main>
</div>
</>
)
}

View File

@@ -6,22 +6,23 @@ import CopyToClipboard from 'src/Components/CopyToClipboard/CopyToClipboard';
import IconButton from 'src/Components/IconButton/IconButton';
import { CONSTS } from 'src/utils';
import { motion } from "framer-motion";
import Card from 'src/Components/Card/Card';
interface Props {
isOwner?: boolean;
nostr_pub_key: Nullable<string>;
nostr_prv_key: Nullable<string>;
}
export default function CommentsSettingsCard({ nostr_prv_key, nostr_pub_key, isOwner }: Props) {
export default function CommentsSettingsCard({ nostr_prv_key, nostr_pub_key }: Props) {
const [relaysDropdownOpen, toggleRelaysDropdownOpen] = useToggle(false)
return (
<div className="rounded-16 bg-white border-2 border-gray-200 p-24">
<Card>
<p className="text-body2 font-bold">💬 Nostr comments <span className="bg-green-50 text-green-500 text-body5 font-medium py-4 px-12 rounded-48 ml-8">Experimental</span></p>
<p className="mt-8 text-body4 text-gray-600">
Our commenting system is experimental and uses Nostr to store and relay your messages and replies to our own relay, as well as relays ran by other people in the community.
@@ -38,7 +39,8 @@ export default function CommentsSettingsCard({ nostr_prv_key, nostr_pub_key, isO
<input
type={'password'}
className="input-text"
value={nostr_prv_key}
defaultValue={nostr_prv_key}
readOnly
/>
<CopyToClipboard text={nostr_prv_key} />
@@ -50,10 +52,10 @@ export default function CommentsSettingsCard({ nostr_prv_key, nostr_pub_key, isO
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={nostr_pub_key!}
defaultValue={nostr_pub_key!}
readOnly
/>
<CopyToClipboard text={nostr_pub_key ?? ''} />
</div>
@@ -94,6 +96,7 @@ export default function CommentsSettingsCard({ nostr_prv_key, nostr_pub_key, isO
<Button color='gray' fullWidth disabled className='mt-24'>
Connect your Nostr ID (coming soon)
</Button>
</div>
</Card>
)
}

View File

@@ -0,0 +1,21 @@
import { Nullable } from 'remirror';
import CommentsSettingsCard from './CommentsSettingsCard/CommentsSettingsCard';
interface Props {
isOwner?: boolean;
nostr_pub_key: Nullable<string>;
nostr_prv_key: Nullable<string>;
}
export default function PreferencesTab({ nostr_prv_key, nostr_pub_key, isOwner }: Props) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<div className="col-span-2">
<CommentsSettingsCard nostr_prv_key={nostr_prv_key} nostr_pub_key={nostr_pub_key} />
</div>
</div>
)
}

View File

@@ -0,0 +1,71 @@
import React from 'react'
import { Link } from 'react-router-dom'
import Button from 'src/Components/Button/Button'
import Card from 'src/Components/Card/Card'
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
import { useProfileQuery } from 'src/graphql'
import { trimText } from 'src/utils/helperFunctions'
import { useAppSelector } from 'src/utils/hooks'
import { createRoute } from 'src/utils/routing'
interface Props {
isLoading?: boolean;
isDirty?: boolean;
onSubmit?: () => void
onCancel?: () => void;
}
export default function SaveChangesCard(props: Props) {
const userId = useAppSelector(state => state.user.me?.id!)
const profileQuery = useProfileQuery({
variables: {
profileId: userId,
},
})
if (!profileQuery.data?.profile)
return <></>
const clickCancel = () => {
if (window.confirm('You might lose some unsaved changes. Are you sure you want to continue?'))
props.onCancel?.()
}
return (
<Card onlyMd className='flex flex-col gap-24'>
<div className='hidden md:flex gap-8'>
<Link
className='shrink-0'
to={createRoute({ type: 'profile', id: profileQuery.data.profile.id, username: profileQuery.data.profile.name })}>
<Avatar width={48} src={profileQuery.data.profile.avatar!} />
</Link>
<div className='overflow-hidden'>
<p className={`text-body4 text-black font-medium overflow-hidden text-ellipsis`}>{profileQuery.data.profile ? trimText(profileQuery.data.profile.name, 30) : "Anonymouse"}</p>
{profileQuery.data.profile.jobTitle && <p className={`text-body6 text-gray-600`}>{profileQuery.data.profile.jobTitle}</p>}
</div>
{/* {showTimeAgo && <p className={`${nameSize[size]} text-gray-500 ml-auto `}>
{dayjs().diff(props.date, 'hour') < 24 ? `${dayjs().diff(props.date, 'hour')}h ago` : undefined}
</p>} */}
</div>
<p className="hidden md:block text-body5">{trimText(profileQuery.data.profile.bio, 120)}</p>
<div className="flex flex-col gap-16">
<Button
color="primary"
onClick={props.onSubmit}
disabled={!props.isDirty || props.isLoading}
>
Save Changes
</Button>
<Button
color="gray"
onClick={clickCancel}
disabled={!props.isDirty || props.isLoading}
>
Cancel
</Button>
</div>
</Card>
)
}

View File

@@ -0,0 +1,289 @@
import { SubmitHandler, useForm } from "react-hook-form"
import Button from "src/Components/Button/Button";
import { User, useUpdateProfileAboutMutation } from "src/graphql";
import { NotificationsService } from "src/services/notifications.service";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { usePrompt } from "src/utils/hooks";
import SaveChangesCard from "../SaveChangesCard/SaveChangesCard";
import { toast } from "react-toastify";
import Card from "src/Components/Card/Card";
interface Props {
data: Pick<User,
| 'name'
| 'email'
| 'lightning_address'
| 'jobTitle'
| 'avatar'
| 'website'
| 'github'
| 'twitter'
| 'linkedin'
| 'location'
| 'bio'
>,
onClose?: () => void;
}
type IFormInputs = Props['data'];
const schema: yup.SchemaOf<IFormInputs> = yup.object({
name: yup.string().trim().required().min(2),
avatar: yup.string().url().required(),
bio: yup.string().ensure(),
email: yup.string().email().ensure(),
github: yup.string().ensure(),
jobTitle: yup.string().ensure(),
lightning_address: yup
.string()
.test({
name: "is valid lightning_address",
test: async value => {
try {
if (value) {
const [name, domain] = value.split("@");
const lnurl = `https://${domain}/.well-known/lnurlp/${name}`;
const res = await fetch(lnurl);
if (res.status === 200) return true;
}
return true;
} catch (error) {
return false;
}
}
})
.ensure()
.label("lightning address"),
linkedin: yup.string().ensure(),
location: yup.string().ensure(),
twitter: yup.string().ensure(),
website: yup.string().url().ensure(),
}).required();
export default function UpdateMyProfileTab({ data, onClose }: Props) {
const { register, formState: { errors, isDirty, }, handleSubmit, reset } = useForm<IFormInputs>({
defaultValues: data,
resolver: yupResolver(schema),
mode: 'onBlur',
});
const [mutate, mutationStatus] = useUpdateProfileAboutMutation();
usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
const onSubmit: SubmitHandler<IFormInputs> = data => {
const toastId = toast.loading("Saving changes...", NotificationsService.defaultOptions)
mutate({
variables: {
data: {
name: data.name,
avatar: data.avatar,
jobTitle: data.jobTitle,
bio: data.bio,
email: data.email,
github: data.github,
linkedin: data.linkedin,
lightning_address: data.lightning_address,
location: data.location,
twitter: data.twitter,
website: data.website,
}
},
onCompleted: () => {
reset(data);
toast.update(toastId, { render: "Saved changes successfully", type: "success", ...NotificationsService.defaultOptions, isLoading: false });
}
})
.catch(() => {
toast.update(toastId, { render: "A network error happened", type: "error", ...NotificationsService.defaultOptions, isLoading: false });
mutationStatus.reset()
})
};
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<Card className="md:col-span-2" defaultPadding={false}>
<div className="bg-gray-600 relative h-[160px] rounded-t-16">
<div className="absolute left-24 bottom-0 translate-y-1/2">
<Avatar src={data.avatar} width={120} />
</div>
</div>
<div className="p-16 md:p-24 mt-64">
<form onSubmit={handleSubmit(onSubmit)}>
<p className="text-body5 font-medium">
Name
</p>
<div className="input-wrapper mt-8 relative">
<input
autoFocus
type='text'
className="input-text"
placeholder='John Doe'
{...register("name")}
/>
</div>
{errors.name && <p className="input-error">
{errors.name.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Avatar
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='https://images.com/my-avatar.jpg'
{...register("avatar")}
/>
</div>
{errors.avatar && <p className="input-error">
{errors.avatar.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Bio
</p>
<div className="input-wrapper mt-8 relative">
<textarea
rows={3}
className="input-text !p-20"
placeholder='Tell others a little bit about yourself'
{...register("bio")}
/>
</div>
{errors.bio && <p className="input-error">
{errors.bio.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Job Title
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="Back-end Developer"
{...register("jobTitle")}
/>
</div>
{errors.jobTitle && <p className="input-error">
{errors.jobTitle.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Location
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="UK, London"
{...register("location")}
/>
</div>
{errors.location && <p className="input-error">
{errors.location.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Website
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="www.website.io"
{...register("website")}
/>
</div>
{errors.website && <p className="input-error">
{errors.website.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Twitter
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="@johndoe"
{...register("twitter")}
/>
</div>
{errors.twitter && <p className="input-error">
{errors.twitter.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Github
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="johndoe"
{...register("github")}
/>
</div>
{errors.github && <p className="input-error">
{errors.github.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Linkedin
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="www.linkedin.com/in/john-doe"
{...register("linkedin")}
/>
</div>
{errors.linkedin && <p className="input-error">
{errors.linkedin.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Lightning address
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="johndoe@lnd.com"
{...register("lightning_address")}
/>
</div>
{errors.lightning_address && <p className="input-error">
{errors.lightning_address.message}
</p>}
<p className="text-body6 text-gray-400 mt-8 max-w-[70ch]">
Your lightning address is used to send the votes you get on your posts, comments, apps...etc, directly to you.
</p>
</form>
</div>
</Card>
<div className="self-start sticky-side-element">
<SaveChangesCard
isLoading={mutationStatus.loading}
isDirty={isDirty}
onSubmit={handleSubmit(onSubmit)}
onCancel={() => reset()}
/>
</div>
</div>
)
}

View File

@@ -3,8 +3,7 @@ import { User } from "src/graphql"
import { trimText, withHttp } from "src/utils/helperFunctions"
import { FiGithub, FiGlobe, FiLinkedin, FiTwitter } from 'react-icons/fi'
import Button from "src/Components/Button/Button";
import { useToggle } from "@react-hookz/web";
import UpdateAboutForm from "./UpdateAboutForm";
import Card from "src/Components/Card/Card";
interface Props {
isOwner?: boolean;
@@ -52,62 +51,56 @@ export default function AboutCard({ user, isOwner }: Props) {
}
];
const [editMode, toggleEditMode] = useToggle(false);
return (
<div className="rounded-16 bg-white border-2 border-gray-200">
<Card defaultPadding={false}>
<div className="bg-gray-600 relative h-[160px] rounded-t-16">
<div className="absolute left-24 bottom-0 translate-y-1/2">
<Avatar src={user.avatar} width={120} />
</div>
</div>
<div className="h-64 flex justify-end items-center px-24">
{(isOwner && !editMode) && <Button size="sm" color="gray" onClick={() => toggleEditMode(true)}>Edit Profile</Button>}
{(isOwner) && <Button size="sm" color="gray" href='/edit-profile'>Edit Profile</Button>}
</div>
<div className="p-24 pt-0">
{editMode === true ?
<div className="flex flex-col gap-16">
<h1 className="text-h2 font-bolder break-words">
{user.name}
</h1>
<UpdateAboutForm data={user} onClose={toggleEditMode} />
{links.some(link => link.hasValue) && <div className="flex flex-wrap gap-16">
{links.filter(link => link.hasValue || isOwner).map((link, idx) => link.hasValue ?
<a
key={idx}
href={link.url!}
className="text-body4 text-primary-700 font-medium"
target='_blank'
rel="noreferrer">
<link.icon className="scale-125 mr-8" /> <span className="align-middle">{link.text}</span>
</a> :
<p
key={idx}
className="text-body4 text-primary-700 font-medium"
>
<link.icon className="scale-125 mr-8" /> <span className="align-middle">---</span>
</p>)}
</div>}
:
{(user.jobTitle || isOwner) && <p className="text-body4 font-medium">
{user.jobTitle ? user.jobTitle : "No job title added"}
</p>}
<div className="flex flex-col gap-16">
<h1 className="text-h2 font-bolder">
{trimText(user.name, 20)}
</h1>
{(user.lightning_address || isOwner) && <p className="text-body5 font-medium">
{user.lightning_address ? `${user.lightning_address}` : "⚡ No lightning address"}
</p>}
{links.some(link => link.hasValue) && <div className="flex flex-wrap gap-16">
{links.filter(link => link.hasValue || isOwner).map((link, idx) => link.hasValue ?
<a
key={idx}
href={link.url!}
className="text-body4 text-primary-700 font-medium"
target='_blank'
rel="noreferrer">
<link.icon className="scale-125 mr-8" /> <span className="align-middle">{link.text}</span>
</a> :
<p
key={idx}
className="text-body4 text-primary-700 font-medium"
>
<link.icon className="scale-125 mr-8" /> <span className="align-middle">---</span>
</p>)}
</div>}
{(user.jobTitle || isOwner) && <p className="text-body4 font-medium">
{user.jobTitle ? user.jobTitle : "No job title added"}
</p>}
{(user.lightning_address || isOwner) && <p className="text-body5 font-medium">
{user.lightning_address ? `${user.lightning_address}` : "⚡ No lightning address"}
</p>}
{(user.bio || isOwner) && <p className="text-body5 font-medium">
{user.bio ? user.bio : "No bio added"}
</p>}
</div>
}
{(user.bio || isOwner) && <p className="text-body5 font-medium">
{user.bio ? user.bio : "No bio added"}
</p>}
</div>
</div>
</div>
</Card>
)
}

View File

@@ -1,261 +0,0 @@
import { ComponentProps } from "react"
import { SubmitHandler, useForm } from "react-hook-form"
import Button from "src/Components/Button/Button";
import { useUpdateProfileAboutMutation } from "src/graphql";
import { NotificationsService } from "src/services/notifications.service";
import AboutCard from "./AboutCard"
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
interface Props {
data: ComponentProps<typeof AboutCard>['user'],
onClose?: () => void;
}
type IFormInputs = Props['data'];
const schema: yup.SchemaOf<IFormInputs> = yup.object({
name: yup.string().trim().required().min(2),
avatar: yup.string().url().required(),
bio: yup.string().ensure(),
email: yup.string().email().ensure(),
github: yup.string().ensure(),
jobTitle: yup.string().ensure(),
lightning_address: yup
.string()
.test({
name: "is valid lightning_address",
test: async value => {
try {
if (value) {
const [name, domain] = value.split("@");
const lnurl = `https://${domain}/.well-known/lnurlp/${name}`;
const res = await fetch(lnurl);
if (res.status === 200) return true;
}
return false;
} catch (error) {
return false;
}
}
})
.ensure()
.label("lightning address"),
linkedin: yup.string().ensure(),
location: yup.string().ensure(),
twitter: yup.string().ensure(),
website: yup.string().url().ensure(),
}).required();
export default function UpdateAboutForm({ data, onClose }: Props) {
const { register, formState: { errors }, handleSubmit } = useForm<IFormInputs>({
defaultValues: data,
resolver: yupResolver(schema),
mode: 'onBlur',
});
const [mutate, mutationStatus] = useUpdateProfileAboutMutation({
onCompleted: () => {
onClose?.()
}
});
const onSubmit: SubmitHandler<IFormInputs> = data => {
mutate({
variables: {
data: {
name: data.name,
avatar: data.avatar,
jobTitle: data.jobTitle,
bio: data.bio,
email: data.email,
github: data.github,
linkedin: data.linkedin,
lightning_address: data.lightning_address,
location: data.location,
twitter: data.twitter,
website: data.website,
}
}
}).catch(() => {
NotificationsService.error('A network error happened');
mutationStatus.reset()
})
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<p className="text-body5 mt-16 font-medium">
Name
</p>
<div className="input-wrapper mt-8 relative">
<input
autoFocus
type='text'
className="input-text"
placeholder='John Doe'
{...register("name")}
/>
</div>
{errors.name && <p className="input-error">
{errors.name.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Avatar
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='https://images.com/my-avatar.jpg'
{...register("avatar")}
/>
</div>
{errors.avatar && <p className="input-error">
{errors.avatar.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Bio
</p>
<div className="input-wrapper mt-8 relative">
<textarea
rows={3}
className="input-text !p-20"
placeholder='Tell others a little bit about yourself'
{...register("bio")}
/>
</div>
{errors.bio && <p className="input-error">
{errors.bio.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Job Title
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="Back-end Developer"
{...register("jobTitle")}
/>
</div>
{errors.jobTitle && <p className="input-error">
{errors.jobTitle.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Location
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="UK, London"
{...register("location")}
/>
</div>
{errors.location && <p className="input-error">
{errors.location.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Website
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="www.website.io"
{...register("website")}
/>
</div>
{errors.website && <p className="input-error">
{errors.website.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Twitter
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="@johndoe"
{...register("twitter")}
/>
</div>
{errors.twitter && <p className="input-error">
{errors.twitter.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Github
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="johndoe"
{...register("github")}
/>
</div>
{errors.github && <p className="input-error">
{errors.github.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Linkedin
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="www.linkedin.com/in/john-doe"
{...register("linkedin")}
/>
</div>
{errors.linkedin && <p className="input-error">
{errors.linkedin.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Lightning address
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="johndoe@lnd.com"
{...register("lightning_address")}
/>
</div>
{errors.lightning_address && <p className="input-error">
{errors.lightning_address.message}
</p>}
<div className="mt-24 flex gap-16 justify-end">
<Button
color='gray'
disabled={mutationStatus.loading}
onClick={onClose}
>
Cancel
</Button>
<Button
type='submit'
color='primary'
isLoading={mutationStatus.loading}
disabled={mutationStatus.loading}
>
Save changes
</Button>
</div>
</form>
)
}

View File

@@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet'
import { useAppSelector } from 'src/utils/hooks';
import styles from './styles.module.scss'
import StoriesCard from "./StoriesCard/StoriesCard"
import CommentsSettingsCard from "./CommentsSettingsCard/CommentsSettingsCard"
export default function ProfilePage() {
@@ -44,10 +43,6 @@ export default function ProfilePage() {
<main className="flex flex-col gap-24">
<AboutCard user={profileQuery.data.profile} isOwner={isOwner} />
<StoriesCard stories={profileQuery.data.profile.stories} isOwner={isOwner} />
{
isOwner &&
<CommentsSettingsCard nostr_prv_key={profileQuery.data.profile.nostr_prv_key} nostr_pub_key={profileQuery.data.profile.nostr_pub_key} isOwner={isOwner} />
}
</main>
<aside></aside>
</div>

View File

@@ -2,6 +2,7 @@ import React from 'react'
import { Link } from 'react-router-dom'
import Badge from 'src/Components/Badge/Badge'
import Button from 'src/Components/Button/Button'
import Card from 'src/Components/Card/Card'
import { Story } from 'src/features/Posts/types'
import { getDateDifference } from 'src/utils/helperFunctions'
import { Tag } from 'src/utils/interfaces'
@@ -24,7 +25,7 @@ interface Props {
export default function StoriesCard({ stories, isOwner }: Props) {
return (
<div className="rounded-16 bg-white border-2 border-gray-200 p-24">
<Card>
<p className="text-body2 font-bold">Stories ({stories.length})</p>
{stories.length > 0 &&
<ul className="">
@@ -64,6 +65,6 @@ export default function StoriesCard({ stories, isOwner }: Props) {
</Button>}
</div>
}
</div>
</Card>
)
}

View File

@@ -36,7 +36,7 @@ export default function CategoryPage() {
<title>{`${data?.getCategory.title!} Lightning Products`}</title>
<meta property="og:title" content={`${data?.getCategory.title!} Lightning Products`} />
</Helmet>
<div className='px-32'>
<div className='page-container'>
<HeaderImage
isLoading={loading}
title={data?.getCategory.title!}

View File

@@ -3,6 +3,7 @@ import { FiArrowLeft } from 'react-icons/fi'
import Skeleton from 'react-loading-skeleton'
import { Link } from 'react-router-dom'
import ASSETS from 'src/assets'
import { PAGES_ROUTES } from 'src/utils/routing'
type Props = {
isLoading: boolean
@@ -33,7 +34,7 @@ export default function HeaderImage(props: Props) {
<img src={img.startsWith('https://via.placeholder.com/') ? DEFAULT_IMG : img} alt={`${title} cover`} className='absolute inset-0 w-full h-full object-cover z-[-1]' />
<div className='absolute inset-0 w-full h-full bg-black bg-opacity-50 z-[-1]' />
<Link
to='/'
to={PAGES_ROUTES.projects.default}
className="
w-[48px] h-[48px] bg-white hover:bg-gray-200
absolute top-24 left-24 md:top-1/2 md:left-40 md:-translate-y-1/2

View File

@@ -1,7 +1,8 @@
import Slider from 'src/Components/Slider/Slider'
import { useNavigate } from 'react-router-dom';
import { useAllCategoriesQuery } from 'src/graphql';
import { FaChevronLeft, FaChevronRight } from 'react-icons/fa';
import { useCarousel } from 'src/utils/hooks';
const colors = [
'#FDF2F8',
@@ -18,8 +19,14 @@ const colors = [
export default function Categories() {
const { viewportRef, scrollSlides, canScrollNext, canScrollPrev, isClickAllowed } = useCarousel({
align: 'start', slidesToScroll: 2,
containScroll: "trimSnaps",
})
const { data, loading } = useAllCategoriesQuery();
const navigate = useNavigate();
if (loading || !data)
return <div className="flex gap-12">
{Array(5).fill(0).map((_, idx) =>
@@ -34,16 +41,28 @@ export default function Categories() {
return (
<Slider>
{data?.allCategories.map((category, idx) =>
<button
key={category.id}
onClick={() => navigate(`/products/category/${category.id}`)}
className=' block p-16 rounded-16 hover:bg-gray-100 active:bg-gray-200 active:scale-90 transition-transform'
style={{ backgroundColor: colors[idx % colors.length] }}
>{category.icon} {category.title}</button>
)}
</Slider>
<div className="relative group">
<div className="overflow-hidden" ref={viewportRef}>
<div className="select-none w-full flex gap-16">
{data?.allCategories.map((category, idx) =>
<button
key={category.id}
onClick={() => isClickAllowed() && navigate(`/projects/category/${category.id}`)}
className='min-w-max block p-16 rounded-16 hover:bg-gray-100 active:bg-gray-200 active:scale-90 transition-transform'
style={{ backgroundColor: colors[idx % colors.length] }}
>{category.icon} {category.title}</button>
)}
</div>
</div>
<button className={`absolute text-body6 w-[28px] aspect-square flex justify-center items-center left-0 -translate-x-1/2 top-1/2 -translate-y-1/2 rounded-full bg-white text-gray-400 opacity-0 ${canScrollPrev && 'group-hover:opacity-100'} active:scale-90 transition-opacity border border-gray-200 shadow-md`} onClick={() => scrollSlides(-1)}>
<FaChevronLeft />
</button>
<button className={`absolute text-body6 w-[28px] aspect-square flex justify-center items-center right-0 translate-x-1/2 top-1/2 -translate-y-1/2 rounded-full bg-white text-gray-400 opacity-0 ${canScrollNext && 'group-hover:opacity-100'} active:scale-90 transition-opacity border border-gray-200 shadow-md`} onClick={() => scrollSlides(1)}>
<FaChevronRight />
</button>
</div>
)
}

View File

@@ -15,7 +15,7 @@ export default function ExplorePage() {
<Header />
<div className="my-32 overflow-hidden">
<div className="my-32">
<Categories />
</div>
<div className="w-full overflow-hidden">

View File

@@ -1,8 +1,6 @@
const CustomDot = ({ onClick, ...rest }: any) => {
const {
active,
} = rest;
const CustomDot = ({ onClick, active, ...rest }: any) => {
// onMove means if dragging or swiping in progress.
// active is provided by this lib for checking if the item is active or not.
return (

View File

@@ -1,18 +1,17 @@
import { useMediaQuery } from "@react-hookz/web";
import Carousel from "react-multi-carousel";
import Assets from "src/assets";
import Button from "src/Components/Button/Button";
import THEME from "src/utils/theme";
import { MEDIA_QUERIES } from "src/utils/theme/media_queries";
import CustomDot from "./CustomDot/CustomDot";
import styles from './styles.module.css'
import useEmblaCarousel from 'embla-carousel-react'
import { useCallback, useEffect, useState } from "react";
const headerLinks = [
{
title: <p className="text-body1 font-bolder text-white">Explore a fun directory of lightning web apps</p>,
img: Assets.Images_ExploreHeader1,
link: {
content: "Submit app",
content: "Submit project",
url: "https://form.jotform.com/220301236112030",
},
},
@@ -30,61 +29,78 @@ const headerLinks = [
},
];
const responsive = {
desktop: {
breakpoint: { max: 5000, min: THEME.screens.md },
items: 2
},
mobile: {
breakpoint: { max: 768, min: 0 },
items: 1
}
}
export default function Header() {
const isDesktop = useMediaQuery(MEDIA_QUERIES.isMedium);
const [emblaRef, emblaApi] = useEmblaCarousel({
align: 'start',
breakpoints: {
[MEDIA_QUERIES.isMedium]: {
draggable: false
}
}
})
const [selectedIndex, setSelectedIndex] = useState(0);
const [scrollSnaps, setScrollSnaps] = useState<number[]>([]);
const onSelect = useCallback(() => {
if (!emblaApi) return;
setSelectedIndex(emblaApi.selectedScrollSnap());
}, [emblaApi, setSelectedIndex]);
useEffect(() => {
if (!emblaApi) return;
onSelect();
setScrollSnaps(emblaApi.scrollSnapList());
emblaApi.on("select", onSelect);
}, [emblaApi, setScrollSnaps, onSelect]);
return (
<Carousel
showDots={!isDesktop}
arrows={false}
responsive={responsive}
customDot={<CustomDot />}
className={styles.header}
containerClass='!overflow-hidden'
>
<div className=" rounded-20 md:mr-20 h-[280px] relative overflow-hidden p-24 flex flex-col items-start justify-end">
<img
className="w-full h-full object-cover absolute top-0 left-0 z-[-2]"
src={headerLinks[0].img}
alt=""
/>
<div className="w-full h-full object-cover bg-gradient-to-t from-gray-900 absolute top-0 left-0 z-[-1]"></div>
<div className="max-w-[90%]">
{headerLinks[0].title}
</div>
<div className="relative group">
<div className="overflow-hidden" ref={emblaRef}>
<div className="w-full flex gap-16">
<div className="flex-[0_0_100%] md:flex-[0_0_calc(50%-8px)] rounded-20 h-[280px] relative overflow-hidden p-24 flex flex-col items-start justify-end">
<img
className="w-full h-full object-cover absolute top-0 left-0 z-[-2]"
src={headerLinks[0].img}
alt=""
/>
<div className="w-full h-full object-cover bg-gradient-to-t from-gray-900 absolute top-0 left-0 z-[-1]"></div>
<div className="max-w-[90%]">
{headerLinks[0].title}
</div>
<Button href={headerLinks[0].link.url} newTab color="white" className="mt-24">
{headerLinks[0].link.content}
</Button>
</div>
<div className="rounded-20 md:ml-20 h-[280px] relative overflow-hidden p-24 flex flex-col items-start justify-end">
<img
className="w-full h-full object-cover absolute top-0 left-0 z-[-2]"
src={headerLinks[1].img}
alt=""
/>
<div className="w-full h-full object-cover bg-gradient-to-t from-gray-900 absolute top-0 left-0 z-[-1]"></div>
<div className="max-w-[90%]">
{headerLinks[1].title}
<Button href={headerLinks[0].link.url} newTab color="white" className="mt-24">
{headerLinks[0].link.content}
</Button>
</div>
<div className="flex-[0_0_100%] md:flex-[0_0_calc(50%-8px)] rounded-20 h-[280px] relative overflow-hidden p-24 flex flex-col items-start justify-end">
<img
className="w-full h-full object-cover absolute top-0 left-0 z-[-2]"
src={headerLinks[1].img}
alt=""
/>
<div className="w-full h-full object-cover bg-gradient-to-t from-gray-900 absolute top-0 left-0 z-[-1]"></div>
<div className="max-w-[90%]">
{headerLinks[1].title}
</div>
<Button color="white" href={headerLinks[1].link.url} newTab className="mt-24">
{headerLinks[1].link.content}
</Button>
</div>
</div>
<Button color="white" href={headerLinks[1].link.url} newTab className="mt-24">
{headerLinks[1].link.content}
</Button>
</div>
</Carousel>
<div className="absolute inset-x-0 bottom-8 flex justify-center gap-4 md:hidden">
{scrollSnaps.map((_, index) => (
<CustomDot
key={index}
active={index === selectedIndex}
/>
))}
</div>
</div>
);
}

View File

@@ -1,14 +1,11 @@
import { ReactNode, useCallback, useLayoutEffect, useEffect, useRef, } from "react";
import { ReactNode, useCallback, useEffect, useRef, useState, } from "react";
import { ProjectCard } from "src/utils/interfaces";
import Carousel from 'react-multi-carousel';
import { MdDoubleArrow, } from 'react-icons/md';
import { useAppDispatch } from "src/utils/hooks";
import { useAppDispatch, useCarousel, useResizeListener } from "src/utils/hooks";
import { openModal } from "src/redux/features/modals.slice";
import { useResizeListener } from 'src/utils/hooks'
import { IoIosArrowBack, IoIosArrowForward } from "react-icons/io";
import './style.css';
import { Link } from "react-router-dom";
import ProjectCardMini from "src/features/Projects/Components/ProjectCardMini/ProjectCardMini";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
interface Props {
title: string | ReactNode,
@@ -16,14 +13,6 @@ interface Props {
projects: ProjectCard[]
}
const responsive = {
all: {
breakpoint: { max: 5000, min: 0 },
items: calcNumItems(),
slidesToSlide: Math.round(calcNumItems())
}
}
function calcNumItems(width = Math.min(window.innerWidth - 32, 1440)) {
const items = ((width / (296 + 20)));
@@ -32,40 +21,26 @@ function calcNumItems(width = Math.min(window.innerWidth - 32, 1440)) {
export default function ProjectsRow({ title, link, projects }: Props) {
const [slidesToScroll, setSlidesToScroll] = useState(1)
const rowRef = useRef<HTMLDivElement>(null)
const { viewportRef, scrollSlides, canScrollNext, canScrollPrev, isClickAllowed } = useCarousel({
align: 'start',
slidesToScroll,
containScroll: "trimSnaps",
})
const dispatch = useAppDispatch()
let drag = useRef(false);
const rowRef = useRef<HTMLDivElement>(null!);
const recalcItemsCnt = useCallback(
() => {
if (rowRef.current) {
const count = calcNumItems(rowRef.current.clientWidth);
responsive.all.items = count;
responsive.all.slidesToSlide = Math.round(count)
}
},
[],
);
useLayoutEffect(recalcItemsCnt, [recalcItemsCnt]);
useResizeListener(recalcItemsCnt)
useEffect(() => {
const mousedownListener = () => drag.current = false
const mousemoveListener = () => drag.current = true
document.addEventListener('mousedown', mousedownListener);
document.addEventListener('mousemove', mousemoveListener);
return () => {
document.removeEventListener('mousedown', mousedownListener);
document.removeEventListener('mousemove', mousemoveListener);
}
}, []);
const recalcSlidesToScroll = useCallback(() => {
if (rowRef.current)
setSlidesToScroll(Math.floor(calcNumItems(rowRef.current.clientWidth)))
}, [])
useEffect(recalcSlidesToScroll, [recalcSlidesToScroll])
useResizeListener(recalcSlidesToScroll);
if (projects.length === 0)
@@ -73,8 +48,9 @@ export default function ProjectsRow({ title, link, projects }: Props) {
const handleClick = (projectId: number) => {
if (!drag.current) {
if (isClickAllowed()) {
dispatch(openModal({ Modal: "ProjectDetailsCard", props: { projectId } }))
}
}
@@ -89,35 +65,35 @@ export default function ProjectsRow({ title, link, projects }: Props) {
<MdDoubleArrow className='text-gray-200 ml-8 hover:cursor-pointer transform scale-y-110 scale-x-125 origin-left' />
</Link>}
</h3>
<div ref={rowRef} className="">
<Carousel
showDots={false}
autoPlay={false}
// arrows={false}
responsive={responsive}
// centerMode
itemClass='pb-[1px]'
containerClass='group'
customLeftArrow={
<button className='carousel-btns opacity-0 group-hover:opacity-100 transition-opacity w-64 h-full absolute top-0 left-0 rounded-l-12 bg-gradient-to-r from-gray-700 text-white' >
<IoIosArrowBack className='scale-150' />
</button>
}
customRightArrow={
<button className='carousel-btns opacity-0 group-hover:opacity-100 transition-opacity w-64 h-full absolute top-0 right-0 rounded-r-12 bg-gradient-to-l from-gray-700 text-white' >
<IoIosArrowForward className='scale-150' />
</button>
}
<div className="relative group" ref={rowRef}>
<div className="overflow-hidden" ref={viewportRef}>
<div className="w-full flex gap-16">
{projects.map((project, idx) =>
<div key={project.id} className='flex-[0_0_100%] max-w-[296px]' >
<ProjectCardMini project={project} onClick={handleClick} />
</div>
)}
</div>
</div>
<button
className={`absolute inset-y-0 w-64 left-0 opacity-0 transition-opacity
rounded-l-12 bg-gradient-to-r from-gray-700 text-white
${canScrollPrev ? "group-hover:opacity-100" : ""}
`}
onClick={() => scrollSlides(-1)}
>
{projects.map((project, idx) =>
<div key={project.id} className='max-w-[296px]' >
<ProjectCardMini project={project} onClick={handleClick} />
</div>
)}
</Carousel>
<FaChevronLeft />
</button>
<button
className={`absolute inset-y-0 w-64 right-0 opacity-0 transition-opacity
rounded-r-12 bg-gradient-to-l from-gray-700 text-white
${canScrollNext ? "group-hover:opacity-100" : ""}
`}
onClick={() => scrollSlides(1)}
>
<FaChevronRight />
</button>
</div>
</div>
</div >
)
}

View File

@@ -1,9 +0,0 @@
@media (pointer: coarse) {
.carousel-btns {
display: none;
}
.react-multi-carousel-list {
overflow: visible;
}
}

View File

@@ -20,20 +20,21 @@ export default function ProjectsSection() {
return (
<div className='mt-32 lg:mt-48'>
<ProjectsRow title={<><span className="align-middle mr-8">Hottest</span> <MdLocalFireDepartment className='inline-block text-fire scale-125 ' /></>}
link='/products/hottest'
link='/projects/hottest'
projects={data.hottestProjects} />
<ProjectsRow title="Recently added"
projects={data.newProjects} />
<ProjectsRow title={shockTheWebCategory?.title}
link={`/products/category/${shockTheWebCategory?.id}`}
projects={shockTheWebCategory?.project!} />
{shockTheWebCategory &&
<ProjectsRow title={shockTheWebCategory.title}
link={`/projects/category/${shockTheWebCategory.id}`}
projects={shockTheWebCategory.project!} />}
{restCategories.map(({ id, title, project, }) => {
if (project)
return <ProjectsRow
key={id}
link={`/products/category/${id}`}
link={`/projects/category/${id}`}
title={title}
projects={project} />
else return null

View File

@@ -24,7 +24,7 @@ export default function HottestPage() {
<title>{`Hottest Lightning Products`}</title>
<meta property="og:title" content={`Hottest Lightning Products`} />
</Helmet>
<div className='px-32'>
<div className='page-container'>
<HeaderImage
isLoading={loading}
title={"Hottest Projects"}

View File

@@ -6,10 +6,12 @@ import App from './App';
if (process.env.REACT_APP_ENABLE_MOCKS) {
const { worker } = require('./mocks/browser')
worker.start({
onUnhandledRequest: 'bypass'
})
import(/* webpackChunkName: "mocks_server" */ './mocks/browser')
.then(({ worker }) => {
worker.start({
onUnhandledRequest: 'bypass'
})
})
}

View File

@@ -6,7 +6,7 @@ export const user: User = {
email: "mtg0987654321@gmail.com",
avatar: "https://avatars.dicebear.com/api/bottts/Mtgmtg.svg",
bio: "Lorem asiop asklh kluiw wekjhl shkj kljhsva klu khsc klhlkbs mjklwqr kmlk sadlfui mewr qiumnk, asdjomi cskhsdf.",
name: "Mtg",
name: "123123124asdfsadfsa8d7fsadfasdf",
github: "MTG2000",
jobTitle: "Front-end Web Developer",
join_date: new Date(2021).toISOString(),

View File

@@ -4,12 +4,15 @@ import { ProjectDetailsCard } from "src/features/Projects/pages/ProjectPage/Proj
import VoteCard from "src/features/Projects/pages/ProjectPage/VoteCard/VoteCard";
import { InsertImageModal } from 'src/Components/Inputs/TextEditor/InsertImageModal'
import { InsertVideoModal } from 'src/Components/Inputs/TextEditor/InsertVideoModal'
import { InsertLinkModal } from 'src/Components/Inputs/TextEditor/InsertLinkModal'
import { Claim_FundWithdrawCard, Claim_CopySignatureCard, Claim_GenerateSignatureCard, Claim_SubmittedCard } from "src/features/Projects/pages/ProjectPage/ClaimProject";
import { ModalCard } from "src/Components/Modals/ModalsContainer/ModalsContainer";
import { ConfirmModal } from "src/Components/Modals/ConfirmModal";
import { LinkingAccountModal } from "src/features/Profiles/pages/EditProfilePage/AccountCard/LinkingAccountModal";
import { ComponentProps } from "react";
import { generateId } from "src/utils/helperFunctions";
import { NoWeblnModal } from "src/Components/Modals/NoWeblnModal";
export enum Direction {
START,
@@ -32,10 +35,13 @@ export const ALL_MODALS = {
Claim_SubmittedCard,
Claim_FundWithdrawCard,
ConfirmModal,
NoWeblnModal,
LinkingAccountModal,
// Text Editor Modals
InsertImageModal,
InsertVideoModal,
InsertLinkModal,
}
type ExcludeBaseModalProps<U> = Omit<U, keyof ModalCard>

View File

@@ -1,9 +1,47 @@
import { toast, ToastOptions } from "react-toastify"
const DEBUG = process.env.NODE_ENV === 'development'
interface AlertOptions {
onComplete?: () => void
autoClose?: number
}
export class NotificationsService {
static error(msg: string, options?: Partial<{ error: any }>) {
if (options?.error && DEBUG) console.log(options?.error)
alert(msg)
static defaultOptions: ToastOptions = {
position: "bottom-center",
autoClose: 4000,
hideProgressBar: false,
closeOnClick: true,
rtl: false,
pauseOnFocusLoss: true,
draggable: true,
pauseOnHover: true,
theme: 'light',
}
static success(msg: string, options?: AlertOptions) {
toast.success(msg, {
onClose: options?.onComplete,
autoClose: options?.autoClose ?? 2500,
icon: "✅"
})
}
static info(msg: string, options?: AlertOptions) {
toast.info(msg, {
onClose: options?.onComplete,
autoClose: options?.autoClose ?? 2500,
})
}
static error(msg: string, options?: AlertOptions & Partial<{ error: any }>) {
if (options?.error && DEBUG) console.log(options?.error)
toast.error(msg, {
onClose: options?.onComplete,
autoClose: options?.autoClose ?? 2500,
})
}
}

View File

@@ -1,6 +1,7 @@
import { requestProvider, MissingProviderError, WebLNProvider } from 'webln';
import { store } from '../redux/store'
import { connectWallet as connectWalletStore } from '../redux/features/wallet.slice'
import { openModal } from 'src/redux/features/modals.slice';
class _Wallet_Service {
@@ -10,20 +11,19 @@ class _Wallet_Service {
async getWebln() {
if (!this.isConnected) await this.connectWallet();
return this.webln as WebLNProvider;
return this.webln;
}
init() {
const connectedPreviously = localStorage.getItem('wallet-connected')
if (connectedPreviously)
this.connectWallet();
// const connectedPreviously = localStorage.getItem('wallet-connected')
// if (connectedPreviously)
// this.connectWallet();
}
async connectWallet() {
try {
const webln = await requestProvider();
store.dispatch(connectWalletStore())
localStorage.setItem('wallet-connected', 'yes')
this.webln = webln;
this.isConnected = false;
}
@@ -35,11 +35,13 @@ class _Wallet_Service {
message = "Check out https://getalby.com to get a web enabled lightning wallet";
}
console.log(message);
localStorage.removeItem('wallet-connected')
// Show the error (though you should probably use something better than alert!)
alert(message);
store.dispatch(openModal({
Modal: "NoWeblnModal"
}))
}
}

View File

@@ -2,7 +2,8 @@
$screen-xs-min: 320px;
@import "./tw.scss", "./shared.scss", "./vendors.scss", "./scrollbar.scss";
@import "./tw.scss", "./shared.scss", "./vendors.scss", "./scrollbar.scss",
"./ui_state.scss";
@import "/src/styles/mixins/index.scss";
html {
@@ -14,23 +15,31 @@ body {
background-color: #ffffff;
}
.content-container,
.page-container {
--padding: 16px;
@include gt-sm {
--padding: 24px;
}
width: calc(min(100%, 1440px) - 2 * var(--padding));
@include gt-md {
--padding: 32px;
}
width: calc(min(100% - 2 * var(--padding), 1440px));
margin: 0 auto;
}
.page-container {
padding: 32px 0;
}
@media screen and (min-width: 780px) {
.page-container {
width: calc(min(100% - 64px, 1440px));
margin: 0 auto;
}
}
// @media screen and (min-width: 780px) {
// .page-container {
// width: calc(min(100% - 64px, 1440px));
// margin: 0 auto;
// }
// }
svg {
display: inline-block;

View File

@@ -1,15 +1,21 @@
.input-removed-arrows::-webkit-outer-spin-button,
.input-removed-arrows::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
.input-removed-arrows[type="number"] {
-moz-appearance: textfield;
-moz-appearance: textfield;
}
button[disabled]{
opacity: .5;
pointer-events: none;
}
button[disabled] {
opacity: 0.5;
pointer-events: none;
}
.sticky-side-element {
position: sticky;
top: calc(var(--navHeight) + 16px);
max-height: calc(100vh - var(--navHeight) - 16px);
}

3
src/styles/ui_state.scss Normal file
View File

@@ -0,0 +1,3 @@
:root {
--navHeight: 0;
}

View File

@@ -8,11 +8,13 @@ import { useCallback, useLayoutEffect } from 'react';
import { setIsMobileScreen } from 'src/redux/features/ui.slice';
import { isMobileScreen } from './helperFunctions';
import ReactTooltip from 'react-tooltip';
import { ToastContainer } from 'react-toastify';
import 'react-multi-carousel/lib/styles.css';
import 'react-toastify/dist/ReactToastify.css';
import 'react-loading-skeleton/dist/skeleton.css'
import THEME from './theme';
import ErrorBoundary from 'src/Components/ErrorBoundary/ErrorBoundary';
import { NotificationsService } from 'src/services';
THEME.injectStyles();
let basename = '/';
@@ -56,6 +58,11 @@ export default function Wrapper(props: any) {
effect='solid'
delayShow={1000}
/>
<ToastContainer
{...NotificationsService.defaultOptions}
newestOnTop={false}
limit={2}
/>
</>
)
}

View File

@@ -61,7 +61,8 @@ export function lazyModal<T extends ComponentType<any>>
return { LazyComponent, preload };
}
export function trimText(text: string, length: number) {
export function trimText(text: string | undefined | null, length: number) {
if (!text) return '';
return text.slice(0, length) + (text.length > length ? "..." : "")
}

View File

@@ -10,3 +10,6 @@ export * from './useWindowSize'
export * from './useMediaQuery'
export * from './useCurrentSection'
export * from './usePreload'
export * from './useCarousel'
export * from './usePrompt'

View File

@@ -0,0 +1,43 @@
import { useCallback, useEffect, useState, } from "react";
import useEmblaCarousel from 'embla-carousel-react'
export const useCarousel = (...props: Parameters<typeof useEmblaCarousel>) => {
const [viewportRef, emblaApi] = useEmblaCarousel({ ...props[0], slidesToScroll: 1 });
const [canScrollNext, setCanScrollNext] = useState(false);
const [canScrollPrev, setCanScrollPrev] = useState(false);
const onSelect = useCallback(() => {
if (!emblaApi) return;
setCanScrollNext(emblaApi.canScrollNext())
setCanScrollPrev(emblaApi.canScrollPrev())
}, [emblaApi,]);
useEffect(() => {
if (!emblaApi) return;
onSelect();
emblaApi.on("select", onSelect);
}, [emblaApi, onSelect]);
const slidesToScroll = Number(props[0]?.slidesToScroll ?? 1);
const scrollSlides = useCallback((direction = 1) => {
if (emblaApi) emblaApi.scrollTo(emblaApi.selectedScrollSnap() + direction * slidesToScroll)
}, [emblaApi, slidesToScroll]);
const isClickAllowed = useCallback(() => !!emblaApi?.clickAllowed(), [emblaApi]);
return {
viewportRef,
canScrollNext,
canScrollPrev,
isClickAllowed,
scrollSlides,
emblaApi,
}
}

View File

@@ -10,8 +10,8 @@ export const useCurrentSection = () => {
return 'blog';
if (location.pathname.startsWith('/hackathons'))
return 'hackathons';
if (location.pathname.startsWith('/products'))
return 'products';
if (location.pathname.startsWith('/apps'))
return 'apps';
if (location.pathname.startsWith('/donate'))
return 'donate';
return 'other'

View File

@@ -1,7 +1,7 @@
import { QueryResult } from "@apollo/client";
import { useCallback, useState } from "react";
export const useInfiniteQuery = (query: QueryResult, dataField: string) => {
export const useInfiniteQuery = (query: QueryResult<any, any>, dataField: string) => {
const [fetchingMore, setFetchingMore] = useState(false)
const [reachedLastPage, setReachedLastPage] = useState(false)

View File

@@ -0,0 +1,56 @@
/**
* These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
* Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
* Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
*/
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
/**
* Blocks all navigation attempts. This is useful for preventing the page from
* changing until some condition is met, like saving form data.
*
* @param blocker
* @param when
* @see https://reactrouter.com/api/useBlocker
*/
export function useBlocker(blocker: any, when = true) {
const { navigator } = useContext(NavigationContext);
useEffect(() => {
if (!when) return;
const unblock = (navigator as any).block((tx: any) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
/**
* Prompts the user with an Alert before they leave the current screen.
*
* @param message
* @param when
*/
export function usePrompt(message: string, when = true) {
const blocker = useCallback(
(tx: any) => {
// eslint-disable-next-line no-alert
if (window.confirm(message)) tx.retry();
},
[message]
);
useBlocker(blocker, when);
}

View File

@@ -7,7 +7,7 @@ export const useResizeListener = (
) => {
options.debounce = options.debounce ?? 250;
const func = useDebouncedCallback(listener, [], options.debounce)
const func = useDebouncedCallback(listener, [listener], options.debounce)
useEffect(() => {
window.addEventListener("resize", func);

View File

@@ -2,7 +2,7 @@
import { gql } from '@apollo/client';
import { useCallback, useState } from 'react';
import { useConfirmVoteMutation, useVoteMutation, Vote_Item_Type } from 'src/graphql';
import { Wallet_Service } from 'src/services';
import { NotificationsService, Wallet_Service } from 'src/services';
export enum PaymentStatus {
DEFAULT,
@@ -41,81 +41,93 @@ export const useVote = (params: Params) => {
if (!itemId || !itemType) return;
setPaymentStatus(PaymentStatus.FETCHING_PAYMENT_DETAILS)
voteMutaion({
variables: {
itemId,
itemType,
amountInSat: amount
},
onCompleted: async (votingData) => {
try {
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
const webln = await Wallet_Service.getWebln()
const paymentResponse = await webln.sendPayment(votingData.vote.payment_request);
setPaymentStatus(PaymentStatus.PAID);
//Confirm Voting payment
confirmVote({
variables: {
paymentRequest: votingData.vote.payment_request,
preimage: paymentResponse.preimage
},
onCompleted: () => {
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
onSuccess?.(votingData.vote.amount_in_sat);
onSetteled?.()
},
update(cache, { data }) {
try {
const { item_id, item_type, amount_in_sat } = data!.confirmVote;
const { votes_count } = cache.readFragment({
id: `${item_type}:${item_id}`,
fragment: gql`
fragment My${item_type} on ${item_type} {
votes_count
}`
}) ?? {};
cache.writeFragment({
id: `${item_type}:${item_id}`,
fragment: gql`
fragment My${item_type} on ${item_type} {
votes_count
}
`,
data: {
votes_count: votes_count + amount_in_sat
},
})
} catch (error) {
onError?.(error)
}
},
onError: (error) => {
setPaymentStatus(PaymentStatus.NETWORK_ERROR);
onError?.(error);
onSetteled?.();
alert("A network error happened while confirming the payment...")
}
})
} catch (error) {
setPaymentStatus(PaymentStatus.CANCELED);
onError?.(error);
onSetteled?.();
alert("Payment rejected by user")
Wallet_Service.getWebln()
.then(webln => {
if (!webln) {
onError?.(new Error('No WebLN Detetcted'))
onSetteled?.()
return
}
},
onError: (error) => {
console.log(error);
setPaymentStatus(PaymentStatus.NETWORK_ERROR);
onError?.(error);
onSetteled?.();
alert("A network error happened...")
}
})
setPaymentStatus(PaymentStatus.FETCHING_PAYMENT_DETAILS)
voteMutaion({
variables: {
itemId,
itemType,
amountInSat: amount
},
onCompleted: async (votingData) => {
try {
setPaymentStatus(PaymentStatus.AWAITING_PAYMENT);
const paymentResponse = await webln.sendPayment(votingData.vote.payment_request);
setPaymentStatus(PaymentStatus.PAID);
//Confirm Voting payment
confirmVote({
variables: {
paymentRequest: votingData.vote.payment_request,
preimage: paymentResponse.preimage
},
onCompleted: () => {
setPaymentStatus(PaymentStatus.PAYMENT_CONFIRMED);
onSuccess?.(votingData.vote.amount_in_sat);
onSetteled?.()
},
update(cache, { data }) {
try {
const { item_id, item_type, amount_in_sat } = data!.confirmVote;
const { votes_count } = cache.readFragment({
id: `${item_type}:${item_id}`,
fragment: gql`
fragment My${item_type} on ${item_type} {
votes_count
}`
}) ?? {};
cache.writeFragment({
id: `${item_type}:${item_id}`,
fragment: gql`
fragment My${item_type} on ${item_type} {
votes_count
}
`,
data: {
votes_count: votes_count + amount_in_sat
},
})
} catch (error) {
onError?.(error)
}
},
onError: (error) => {
setPaymentStatus(PaymentStatus.NETWORK_ERROR);
onError?.(error);
onSetteled?.();
NotificationsService.error("A network error happened while confirming the payment...")
}
})
} catch (error) {
setPaymentStatus(PaymentStatus.CANCELED);
onError?.(error);
onSetteled?.();
NotificationsService.error("Payment rejected by user")
}
},
onError: (error) => {
console.log(error);
setPaymentStatus(PaymentStatus.NETWORK_ERROR);
onError?.(error);
onSetteled?.();
NotificationsService.error("A network error happened...")
}
})
})
}, [confirmVote, voteMutaion, params.itemId, params.itemType, params.onError, params.onSetteled, params.onSuccess]);

View File

@@ -2,32 +2,32 @@ import { toSlug } from "../helperFunctions";
type RouteOptions =
| {
type: 'post',
type: "post",
id: string | number,
postType: string,
title?: string,
username?: string,
}
| {
type: 'story',
type: "story",
id: string | number,
title?: string,
username?: string,
}
| {
type: 'bounty',
type: "bounty",
id: string | number,
title?: string,
username?: string,
}
| {
type: 'question',
type: "question",
id: string | number,
title?: string,
username?: string,
}
| {
type: 'profile',
type: "profile",
id: string | number,
username?: string,
}
@@ -35,27 +35,54 @@ type RouteOptions =
export function createRoute(options: RouteOptions) {
if (options.type === 'post')
if (options.type === "post")
return `/blog/post/${options.postType.toLowerCase()}/${options.id}`
+ (options.title ? `/${toSlug(options.title)}` : "");
if (options.type === 'story')
if (options.type === "story")
return `/blog/post/story/${options.id}`
+ (options.title ? `/${toSlug(options.title)}` : "");
if (options.type === 'bounty')
if (options.type === "bounty")
return `/blog/post/bounty/${options.id}`
+ (options.title ? `/${toSlug(options.title)}` : "");
if (options.type === 'question')
if (options.type === "question")
return `/blog/post/question/${options.id}`
+ (options.title ? `/${toSlug(options.title)}` : "");
if (options.type === 'profile')
if (options.type === "profile")
return `/profile/${options.id}`
+ (options.username ? `/${toSlug(options.username)}` : "");
return ''
return ""
}
export const PAGES_ROUTES = {
projects: {
default: "/projects",
hottest: "/projects/hottest",
byCategoryId: "/projects/category/:id"
},
blog: {
feed: "/blog",
postById: "/blog/post/:type/:id/*",
createPost: "/blog/create-post"
},
hackathons: {
default: "/hackathons"
},
donate: {
default: "/donate"
},
profile: {
editProfile: "/edit-profile/*",
byId: "/profile/:id/*",
},
auth: {
login: "/login",
logout: "/logout",
}
}

View File

@@ -13,7 +13,6 @@ import THEME from '../theme';
// Add the global stuff first (index.ts)
// -------------------------------------------
import "src/styles/index.scss";
import "react-multi-carousel/lib/styles.css";
import 'react-loading-skeleton/dist/skeleton.css'
import { ApolloProvider } from '@apollo/client';
import { apolloClient } from '../apollo';