mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-18 05:44:29 +01:00
merge: pull request #118 from peakshift/dev
Navigation component, redesigned profile edit, better carousel
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
165
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
31
package.json
31
package.json
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.5.10",
|
||||
"@apollo/client": "^3.6.9",
|
||||
"@hookform/resolvers": "^2.8.8",
|
||||
"@noble/secp256k1": "^1.6.3",
|
||||
"@prisma/client": "^3.12.0",
|
||||
@@ -37,6 +37,7 @@
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.1",
|
||||
"dompurify": "^2.3.10",
|
||||
"embla-carousel-react": "^7.0.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"express": "^4.18.1",
|
||||
"express-session": "^1.17.3",
|
||||
@@ -71,13 +72,12 @@
|
||||
"react-loader-spinner": "^6.0.0-0",
|
||||
"react-loading-skeleton": "^3.1.0",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-multi-carousel": "^2.8.0",
|
||||
"react-query": "^3.35.0",
|
||||
"react-redux": "^8.0.0",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-select": "^5.3.2",
|
||||
"react-toastify": "^9.0.8",
|
||||
"react-tooltip": "^4.2.21",
|
||||
"react-topbar-progress-indicator": "^4.1.1",
|
||||
"remirror": "^1.0.77",
|
||||
@@ -90,23 +90,22 @@
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"scripts": {
|
||||
"client:prod-server": "env-cmd -f ./environments/.dev.prod-server.env react-scripts start",
|
||||
"client:preview-server": "env-cmd -f ./environments/.dev.preview-server.env react-scripts start",
|
||||
"client:mocks": "env-cmd -f ./environments/.dev.mock-server.env react-scripts start",
|
||||
"client:dev-server": "env-cmd -f ./environments/.dev.server.env react-scripts start",
|
||||
"server:dev": "serverless offline",
|
||||
"client:prod-server": "env-cmd -f ./envs/client/.dev.prod-server.env react-scripts start",
|
||||
"client:preview-server": "env-cmd -f ./envs/client/.dev.preview-server.env react-scripts start",
|
||||
"client:mocks": "env-cmd -f ./envs/client/.dev.mock-server.env react-scripts start",
|
||||
"client:dev-server": "env-cmd -f ./envs/client/.dev.server.env react-scripts start",
|
||||
"server:dev": "env-cmd -f ./envs/server/local.env serverless offline",
|
||||
"server:preview": "env-cmd -f ./envs/server/preview.env serverless offline",
|
||||
"server:prod": "env-cmd -f ./envs/server/prod.env serverless offline",
|
||||
"generate-graphql": "graphql-codegen",
|
||||
"storybook": "env-cmd -f ./envs/client/.dev.preview-server.env start-storybook -p 6006 -s public",
|
||||
"storybook:mocks": "env-cmd -f ./envs/client/.dev.mock-server.env start-storybook -p 6006 -s public",
|
||||
"build": "react-scripts build",
|
||||
"build:mocks": "env-cmd -f ./environments/.prod.mock-server.env react-scripts build",
|
||||
"build:mocks": "env-cmd -f ./envs/client/.prod.mock-server.env react-scripts build",
|
||||
"build-storybook": "env-cmd -f ./envs/client/.dev.preview-server.env build-storybook -s public",
|
||||
"build-storybook:mocks": "env-cmd -f ./envs/client/.prod.mock-server.env build-storybook -s public",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"predeploy": "env-cmd -f ./environments/.prod.github.env npm run build",
|
||||
"deploy": "gh-pages -d build",
|
||||
"only-deploy": "gh-pages -d build",
|
||||
"storybook": "env-cmd -f ./environments/.dev.preview-server.env start-storybook -p 6006 -s public",
|
||||
"storybook:mocks": "env-cmd -f ./environments/.dev.mock-server.env start-storybook -p 6006 -s public",
|
||||
"build-storybook": "env-cmd -f ./environments/.prod.preview-server.env build-storybook -s public",
|
||||
"build-storybook:mocks": "env-cmd -f ./environments/.prod.mock-server.env build-storybook -s public",
|
||||
"db:migrate-dev": "prisma migrate dev",
|
||||
"db:migrate-deploy": "prisma migrate deploy",
|
||||
"db:reset": "prisma migrate reset",
|
||||
|
||||
@@ -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;
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
// -----------------
|
||||
|
||||
@@ -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
51
prisma/seed/helpers.js
Normal 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,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
51
src/App.tsx
51
src/App.tsx
@@ -10,25 +10,26 @@ import { setUser } from "./redux/features/user.slice";
|
||||
import ProtectedRoute from "./Components/ProtectedRoute/ProtectedRoute";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { NavbarLayout } from "./utils/routing/layouts";
|
||||
import { Loadable } from "./utils/routing";
|
||||
import { Loadable, PAGES_ROUTES } from "./utils/routing";
|
||||
|
||||
|
||||
|
||||
// Pages
|
||||
const FeedPage = Loadable(React.lazy(() => import("./features/Posts/pages/FeedPage/FeedPage")))
|
||||
const PostDetailsPage = Loadable(React.lazy(() => import("./features/Posts/pages/PostDetailsPage/PostDetailsPage")))
|
||||
const CreatePostPage = Loadable(React.lazy(() => import("./features/Posts/pages/CreatePostPage/CreatePostPage")))
|
||||
const FeedPage = Loadable(React.lazy(() => import( /* webpackChunkName: "feed_page" */ "./features/Posts/pages/FeedPage/FeedPage")))
|
||||
const PostDetailsPage = Loadable(React.lazy(() => import( /* webpackChunkName: "post_details_page" */ "./features/Posts/pages/PostDetailsPage/PostDetailsPage")))
|
||||
const CreatePostPage = Loadable(React.lazy(() => import( /* webpackChunkName: "create_post_page" */ "./features/Posts/pages/CreatePostPage/CreatePostPage")))
|
||||
|
||||
const HottestPage = Loadable(React.lazy(() => import("src/features/Projects/pages/HottestPage/HottestPage")))
|
||||
const CategoryPage = Loadable(React.lazy(() => import("src/features/Projects/pages/CategoryPage/CategoryPage")))
|
||||
const ExplorePage = Loadable(React.lazy(() => import("src/features/Projects/pages/ExplorePage")))
|
||||
const HottestPage = Loadable(React.lazy(() => import( /* webpackChunkName: "hottest_page" */ "src/features/Projects/pages/HottestPage/HottestPage")))
|
||||
const CategoryPage = Loadable(React.lazy(() => import( /* webpackChunkName: "category_page" */ "src/features/Projects/pages/CategoryPage/CategoryPage")))
|
||||
const ExplorePage = Loadable(React.lazy(() => import( /* webpackChunkName: "explore_page" */ "src/features/Projects/pages/ExplorePage")))
|
||||
|
||||
const HackathonsPage = Loadable(React.lazy(() => import("./features/Hackathons/pages/HackathonsPage/HackathonsPage")))
|
||||
const HackathonsPage = Loadable(React.lazy(() => import( /* webpackChunkName: "hackathons_page" */ "./features/Hackathons/pages/HackathonsPage/HackathonsPage")))
|
||||
|
||||
const DonatePage = Loadable(React.lazy(() => import("./features/Donations/pages/DonatePage/DonatePage")))
|
||||
const LoginPage = Loadable(React.lazy(() => import("./features/Auth/pages/LoginPage/LoginPage")))
|
||||
const LogoutPage = Loadable(React.lazy(() => import("./features/Auth/pages/LogoutPage/LogoutPage")))
|
||||
const ProfilePage = Loadable(React.lazy(() => import("./features/Profiles/pages/ProfilePage/ProfilePage")))
|
||||
const DonatePage = Loadable(React.lazy(() => import( /* webpackChunkName: "donate_page" */ "./features/Donations/pages/DonatePage/DonatePage")))
|
||||
const LoginPage = Loadable(React.lazy(() => import( /* webpackChunkName: "login_page" */ "./features/Auth/pages/LoginPage/LoginPage")))
|
||||
const LogoutPage = Loadable(React.lazy(() => import( /* webpackChunkName: "logout_page" */ "./features/Auth/pages/LogoutPage/LogoutPage")))
|
||||
const ProfilePage = Loadable(React.lazy(() => import( /* webpackChunkName: "profile_page" */ "./features/Profiles/pages/ProfilePage/ProfilePage")))
|
||||
const EditProfilePage = Loadable(React.lazy(() => import( /* webpackChunkName: "edit_profile_page" */ "./features/Profiles/pages/EditProfilePage/EditProfilePage")))
|
||||
|
||||
|
||||
|
||||
@@ -90,25 +91,27 @@ function App() {
|
||||
</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>
|
||||
|
||||
23
src/Components/Card/Card.stories.tsx
Normal file
23
src/Components/Card/Card.stories.tsx
Normal 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
|
||||
}
|
||||
33
src/Components/Card/Card.tsx
Normal file
33
src/Components/Card/Card.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: InsertLinkModal } = lazyModal(() => import('./InsertLinkModal'))
|
||||
68
src/Components/Inputs/TextEditor/ToolButton/LinkToolBtn.tsx
Normal file
68
src/Components/Inputs/TextEditor/ToolButton/LinkToolBtn.tsx
Normal 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>
|
||||
)
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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))}
|
||||
|
||||
69
src/Components/Modals/NoWeblnModal/NoWeblnModal.tsx
Normal file
69
src/Components/Modals/NoWeblnModal/NoWeblnModal.tsx
Normal 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 you’re browsing on mobile.
|
||||
</h3>
|
||||
<p className="text-body4 text-gray-600 mt-8">
|
||||
In order to use BOLT🔩FUN’s voting button, you need to use a lightning browser wallet like Alby. You can download the extension on your desktop and try again.
|
||||
</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 don’t have Alby installed
|
||||
</h3>
|
||||
<p className="text-body4 text-gray-600 mt-8">
|
||||
In order to use BOLT🔩FUN’s voting button, you’ll 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>
|
||||
)
|
||||
}
|
||||
BIN
src/Components/Modals/NoWeblnModal/alby.png
Normal file
BIN
src/Components/Modals/NoWeblnModal/alby.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
4
src/Components/Modals/NoWeblnModal/index.ts
Normal file
4
src/Components/Modals/NoWeblnModal/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: NoWeblnModal } = lazyModal(() => import('./NoWeblnModal'))
|
||||
BIN
src/Components/Modals/NoWeblnModal/nut.png
Normal file
BIN
src/Components/Modals/NoWeblnModal/nut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"> • <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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as CommentsSection } from './CommentsSection/CommentsSection'
|
||||
export { default } from './CommentsSection/CommentsSection'
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
:
|
||||
<>
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> */}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { lazyModal } from 'src/utils/helperFunctions';
|
||||
|
||||
export const { LazyComponent: LinkingAccountModal } = lazyModal(() => import('./LinkingAccountModal'))
|
||||
109
src/features/Profiles/pages/EditProfilePage/EditProfilePage.tsx
Normal file
109
src/features/Profiles/pages/EditProfilePage/EditProfilePage.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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!}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 >
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
@media (pointer: coarse) {
|
||||
.carousel-btns {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-multi-carousel-list {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
3
src/styles/ui_state.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
:root {
|
||||
--navHeight: 0;
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 ? "..." : "")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,3 +10,6 @@ export * from './useWindowSize'
|
||||
export * from './useMediaQuery'
|
||||
export * from './useCurrentSection'
|
||||
export * from './usePreload'
|
||||
export * from './useCarousel'
|
||||
export * from './usePrompt'
|
||||
|
||||
|
||||
43
src/utils/hooks/useCarousel.ts
Normal file
43
src/utils/hooks/useCarousel.ts
Normal 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,
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
56
src/utils/hooks/usePrompt.tsx
Normal file
56
src/utils/hooks/usePrompt.tsx
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user