├── .env.example
├── .eslintrc
├── .github
├── LICENSE
└── README.md
├── .gitignore
├── .prettierrc
├── LICENSE
├── docs
└── pull_request_template.md
├── next-seo.config.js
├── next-sitemap.config.js
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── browserconfig.xml
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── images
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── check_green_circle.svg
│ ├── default-avatar-0.jpeg
│ ├── default-avatar-1.jpeg
│ ├── default-avatar-10.jpeg
│ ├── default-avatar-11.jpeg
│ ├── default-avatar-2.jpeg
│ ├── default-avatar-3.jpeg
│ ├── default-avatar-4.jpeg
│ ├── default-avatar-5.jpeg
│ ├── default-avatar-6.jpeg
│ ├── default-avatar-7.jpeg
│ ├── default-avatar-8.jpeg
│ ├── default-avatar-9.jpeg
│ ├── logo.svg
│ ├── logo192.png
│ ├── logo512.png
│ ├── matic.png
│ ├── mstile-144x144.png
│ ├── mstile-150x150.png
│ ├── mstile-310x150.png
│ ├── mstile-310x310.png
│ ├── mstile-70x70.png
│ ├── purple_checkmark.svg
│ ├── safari-pinned-tab.svg
│ ├── sismo-badges
│ │ ├── sismo_10jobs.svg
│ │ ├── sismo_10jobs4stars.svg
│ │ ├── sismo_10jobs5stars.svg
│ │ ├── sismo_10jobs_graphic.svg
│ │ ├── sismo_10jobs_php.svg
│ │ ├── sismo_10jobs_sol.svg
│ │ ├── sismo_1earned.svg
│ │ ├── sismo_1jobs.svg
│ │ ├── sismo_1jobs_graphic.svg
│ │ ├── sismo_1jobs_php.svg
│ │ ├── sismo_1jobs_sol.svg
│ │ ├── sismo_200earned.svg
│ │ ├── sismo_20jobs.svg
│ │ ├── sismo_20jobs4stars.svg
│ │ ├── sismo_20jobs5stars.svg
│ │ ├── sismo_20jobs_graphic.svg
│ │ ├── sismo_20jobs_php.svg
│ │ ├── sismo_20jobs_sol.svg
│ │ ├── sismo_500earned.svg
│ │ ├── sismo_5jobs4stars.svg
│ │ ├── sismo_5jobs5stars.svg
│ │ ├── sismo_talentofmonth_graphic.svg
│ │ ├── sismo_talentofmonth_php.svg
│ │ ├── sismo_talentofmonth_sol.svg
│ │ ├── sismo_workedforaave.svg
│ │ ├── sismo_workedfordoge.svg
│ │ └── sismo_workedforgitcoin.svg
│ └── zkpow
│ │ ├── feature-1.png
│ │ ├── feature-2.png
│ │ ├── hands.webp
│ │ ├── hero.png
│ │ ├── indie.webp
│ │ ├── logo.svg
│ │ ├── ninja.webp
│ │ ├── php.webp
│ │ ├── sismo_badge_template.svg
│ │ ├── vault.webp
│ │ ├── zkpow.webp
│ │ ├── zkpow_mini.svg
│ │ └── zkpow_w.webp
├── manifest.json
├── robots.txt
└── site.webmanifest
├── src
├── chains.ts
├── components
│ ├── Back.tsx
│ ├── ConnectBlock.tsx
│ ├── DisputeButton.tsx
│ ├── Form
│ │ ├── EvidenceForm.tsx
│ │ ├── ProfileForm.tsx
│ │ ├── ProposalForm.tsx
│ │ ├── ReleaseForm.tsx
│ │ ├── ReviewForm.tsx
│ │ ├── SearchServiceButton.tsx
│ │ ├── SearchTalentButton.tsx
│ │ ├── ServiceForm.tsx
│ │ ├── SubmitButton.tsx
│ │ ├── TalentLayerIdForm.tsx
│ │ ├── handle-price.tsx
│ │ └── skills-input.tsx
│ ├── HelpPopover.tsx
│ ├── Home
│ │ ├── CreateId.tsx
│ │ ├── SearchService.tsx
│ │ └── SearchTalent.tsx
│ ├── Layout
│ │ ├── Logo.tsx
│ │ ├── SideBottom.tsx
│ │ ├── SideLink.tsx
│ │ └── SideMenu.tsx
│ ├── Loading.tsx
│ ├── Modal
│ │ ├── DelegateModal.tsx
│ │ ├── PaymentModal.tsx
│ │ ├── ReviewModal.tsx
│ │ └── ValidateProposalModal.tsx
│ ├── MultiStepsTransactionToast.tsx
│ ├── NetworkLink.tsx
│ ├── NetworkSwitch.tsx
│ ├── ProposalItem.tsx
│ ├── ReviewItem.tsx
│ ├── ServiceDetail.tsx
│ ├── ServiceItem.tsx
│ ├── ServiceStatus.tsx
│ ├── Stars.tsx
│ ├── Step.tsx
│ ├── Steps.tsx
│ ├── TimeoutCountDown.tsx
│ ├── ToastStep.tsx
│ ├── TransactionToast.tsx
│ ├── UserAccount.tsx
│ ├── UserDetail.tsx
│ ├── UserGains.tsx
│ ├── UserIncomes.tsx
│ ├── UserItem.tsx
│ ├── UserPayments.tsx
│ ├── UserProposalItem.tsx
│ ├── UserProposals.tsx
│ ├── UserServiceItem.tsx
│ ├── UserServices.tsx
│ ├── UserSubMenu.tsx
│ ├── newlayout
│ │ ├── container.tsx
│ │ ├── footer.tsx
│ │ └── navbar.tsx
│ └── request.ts
├── config.ts
├── context
│ └── talentLayer.tsx
├── contracts
│ ├── ABI
│ │ ├── ERC20.json
│ │ ├── TalentLayerArbitrator.json
│ │ ├── TalentLayerEscrow.json
│ │ ├── TalentLayerID.json
│ │ ├── TalentLayerPlatformID.json
│ │ ├── TalentLayerReview.json
│ │ └── TalentLayerService.json
│ ├── acceptProposal.tsx
│ ├── disputes.tsx
│ ├── executePayment.tsx
│ └── toggleDelegation.tsx
├── hooks
│ ├── useAllowedToken.ts
│ ├── useAllowedTokens.ts
│ ├── useArbitrationCost.ts
│ ├── useAsync.ts
│ ├── useEvidences.ts
│ ├── useFees.ts
│ ├── useFilteredServices.ts
│ ├── useIpfsFile.ts
│ ├── useIpfsJsonData.ts
│ ├── usePaymentsByService.ts
│ ├── usePaymentsForUser.ts
│ ├── useProposalById.ts
│ ├── useProposalsByService.ts
│ ├── useProposalsByUser.ts
│ ├── useReviewsByService.ts
│ ├── useServiceById.ts
│ ├── useServiceDetails.ipfsExample.ts
│ ├── useServices.ts
│ ├── useTotalGainByUser.ts
│ ├── useTransactionsById.ts
│ ├── useUserByAddress.ts
│ ├── useUserById.ts
│ ├── useUsers.ts
│ └── workx
│ │ └── useWorkxSkills.tsx
├── modules
│ ├── Disputes
│ │ ├── components
│ │ │ ├── DisputeStatusCard.tsx
│ │ │ ├── DisputeStatusDetail.tsx
│ │ │ ├── EvidenceDetails.tsx
│ │ │ ├── EvidenceModal.tsx
│ │ │ ├── Evidences.tsx
│ │ │ ├── FileDropper.tsx
│ │ │ └── MetaEvidenceModal.tsx
│ │ └── utils
│ │ │ ├── dispute.ts
│ │ │ └── types.ts
│ ├── Lens
│ │ ├── LensModule.tsx
│ │ ├── components
│ │ │ ├── UserLensFeed.tsx
│ │ │ └── UserLensProfile.tsx
│ │ ├── hooks
│ │ │ ├── useLensFeed.ts
│ │ │ └── useLensUsers.ts
│ │ ├── queries
│ │ │ ├── lensFeedData.ts
│ │ │ └── lensProfileData.ts
│ │ └── utils
│ │ │ ├── date.ts
│ │ │ ├── graphql.ts
│ │ │ ├── ipfs.ts
│ │ │ └── types.ts
│ ├── Messaging
│ │ ├── components
│ │ │ ├── CardHeader.tsx
│ │ │ ├── ConnectButton.tsx
│ │ │ ├── ContactButton.tsx
│ │ │ ├── ConversationCard.tsx
│ │ │ ├── ConversationList.tsx
│ │ │ ├── Dashboard.tsx
│ │ │ ├── MessageCard.tsx
│ │ │ ├── MessageComposer.tsx
│ │ │ └── MessageList.tsx
│ │ ├── context
│ │ │ ├── XmtpContext.tsx
│ │ │ └── messging.tsx
│ │ ├── hooks
│ │ │ ├── useSendMessage.ts
│ │ │ ├── useStreamConversations.ts
│ │ │ └── useStreamMessages.ts
│ │ └── utils
│ │ │ ├── messaging.ts
│ │ │ └── types.ts
│ ├── Poh
│ │ ├── PohModule.tsx
│ │ ├── components
│ │ │ └── UserPohProfile.tsx
│ │ ├── hooks
│ │ │ └── usePohUsers.ts
│ │ ├── queries
│ │ │ └── pohProfileData.ts
│ │ └── utils
│ │ │ ├── graphql.ts
│ │ │ └── types.ts
│ ├── Sismo
│ │ ├── components
│ │ │ ├── SismoBadgeCard.tsx
│ │ │ ├── SismoGroupCard.tsx
│ │ │ ├── SismoHelpPopover.tsx
│ │ │ └── UserBadges.tsx
│ │ ├── hooks
│ │ │ ├── useIsUserInSismoGroup.ts
│ │ │ ├── useSismoBadgesPerAddress.ts
│ │ │ └── useSismoGroupData.ts
│ │ ├── queries
│ │ │ └── sismo.ts
│ │ └── utils
│ │ │ ├── graphql.ts
│ │ │ ├── rest.ts
│ │ │ ├── sismoGroupsData.ts
│ │ │ └── types.ts
│ └── WorkX
│ │ ├── hooks
│ │ └── use-workx-skills.ts
│ │ ├── queries
│ │ └── global.ts
│ │ └── utils
│ │ ├── graphql.ts
│ │ └── types.ts
├── pages
│ ├── About.tsx
│ ├── Layout.tsx
│ ├── _app.tsx
│ ├── _document.tsx
│ ├── api
│ │ ├── delegate
│ │ │ ├── create-service.ts
│ │ │ ├── create-update-proposal.ts
│ │ │ ├── mint-id.ts
│ │ │ ├── mint-review.ts
│ │ │ ├── release-reimburse.ts
│ │ │ ├── update-profile-data.ts
│ │ │ └── update-service.ts
│ │ ├── get-skills.ts
│ │ ├── services
│ │ │ ├── filter.json
│ │ │ ├── filtered.ts
│ │ │ └── request.ts
│ │ └── utils
│ │ │ └── delegate.ts
│ ├── dashboard
│ │ ├── incomes.tsx
│ │ └── index.tsx
│ ├── dispute
│ │ └── [id].tsx
│ ├── index.tsx
│ ├── messaging
│ │ ├── [address].tsx
│ │ └── index.tsx
│ ├── profile
│ │ ├── [id].tsx
│ │ └── edit.tsx
│ ├── services
│ │ ├── [id].tsx
│ │ ├── [id]
│ │ │ └── proposal.tsx
│ │ ├── create.tsx
│ │ ├── edit
│ │ │ └── [id].tsx
│ │ └── index.tsx
│ └── talents.tsx
├── queries
│ ├── evidences.ts
│ ├── fees.ts
│ ├── global.ts
│ ├── payments.ts
│ ├── proposals.ts
│ ├── reviews.ts
│ ├── services.ts
│ ├── transactions.ts
│ └── users.ts
├── styles
│ └── globals.css
├── types.ts
└── utils
│ ├── conversion.ts
│ ├── dates.ts
│ ├── graphql.ts
│ ├── index.ts
│ ├── ipfs.ts
│ ├── signature.ts
│ ├── toast.tsx
│ └── web3.ts
├── tailwind.config.js
├── tsconfig.json
└── vercel.json
/.env.example:
--------------------------------------------------------------------------------
1 | # ========== CONNECT ==============
2 | NEXT_PUBLIC_WALLECT_CONNECT_PROJECT_ID=xxxxx
3 | NEXT_PUBLIC_NETWORK_ID=80001
4 |
5 | # ========== PLATFORM ==============
6 | NEXT_PUBLIC_PLATFORM_ID=4
7 |
8 | # ========== IPFS ==============
9 | NEXT_PUBLIC_INFURA_ID=xxxxx
10 | NEXT_PUBLIC_INFURA_SECRET=xxxxxx
11 | NEXT_PUBLIC_IPFS_BASE_URL=https://ipfs.io/ipfs/
12 |
13 | # ========== SIGNATURE ==============
14 | NEXT_PUBLIC_SIGNATURE_API_URL=https://api.defender.openzeppelin.com/autotasks/4b1688f9-01a4-435d-89ae-d05e0aa0a53b/runs/webhook/b9818d77-3c43-4c3d-bfb8-4c77036de92f/ACXhXsQoCKP8zZVE26gFbh
15 | NEXT_PUBLIC_BACKEND_RPC_URL=https://rpc-mumbai.maticvigil.com/v1/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
16 |
17 | # ========== MESSENGING ==============
18 | ## Can be 'prod', 'staging' or 'dev' for PUSH || Can be 'production', 'local' or 'dev' for XMTP
19 | NEXT_PUBLIC_MESSENGING_ENV='dev'
20 |
21 | # ========== DELEGATION ==============
22 | ## Active the delegate feature for service / proposal / release / review (the proposal validation won't be delegate)
23 | NEXT_PUBLIC_ACTIVE_DELEGATE=false
24 | ## Active the delegate feature for minting ID - will call a backend api and call the smartcontract function mintForAddress
25 | NEXT_PUBLIC_ACTIVE_DELEGATE_MINT=false
26 | ## This seed phrase is only used for delegate purpose
27 | NEXT_PRIVATE_DELEGATE_SEED_PHRASE="add you seed phrase here only for delegate purpose"
28 | ## Public address
29 | NEXT_PUBLIC_DELEGATE_ADDRESS="0xaddyouraddresshereonlyfordelegatepurpose"
30 |
31 | # ========== OPEN DATA APIs ==============
32 | NEXT_PUBLIC_WORKX_API_URL=https://api.workpi.com/graphql
33 | NEXT_PUBLIC_LENS_URL=https://api.lens.dev/
34 | NEXT_PUBLIC_POH_SUBGRAPH_URL=https://api.thegraph.com/subgraphs/name/kleros/proof-of-humanity-mainnet
35 | NEXT_PUBLIC_SISMO_GRAPH_API=https://api.sismo.io/
36 |
37 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "jest": true
6 | },
7 | "root": true,
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/recommended",
11 | "plugin:prettier/recommended"
12 | ],
13 | "parser": "@typescript-eslint/parser",
14 | "parserOptions": { "project": ["./tsconfig.json"] },
15 | "plugins": [
16 | "@typescript-eslint"
17 | ],
18 | "ignorePatterns": ["src/**/*.test.ts"]
19 | }
--------------------------------------------------------------------------------
/.github/README.md:
--------------------------------------------------------------------------------
1 | # Indie Frontend for a TalentLayer-Integrated Job Marketplace
2 |
3 | ## About Indie
4 |
5 | Indie is an open-source fork-able codebase that is available for marketplaces and other platforms integrating with [TalentLayer](https://docs.talentlayer.org/) to borrow from and use to get inspired.
6 |
7 | Indie lays the groundwork for what will eventually become the TalentLayer SDK.
8 |
9 | View a video demo of the Indie Frontend [here](https://youtu.be/8Y6E282Nwtc).
10 |
11 | ## Requirements
12 |
13 | - Node & npm
14 |
15 | ## Steps by steps setup
16 |
17 | - `npm install`
18 | - `cp .env.example .env`
19 | - `npm run dev`
20 |
21 | ## Stacks
22 |
23 | - [NextJS](https://nextjs.org)
24 | - [ReactJS](https://reactjs.org)
25 | - [TypeScript](https://www.typescriptlang.org)
26 | - [TailwindCSS](https://tailwindcss.com)
27 | - [Ethers.js](https://docs.ethers.io/v5)
28 | - [Wagmi](https://wagmi.sh)
29 | - [WalletConnect - Web3Modal](https://github.com/WalletConnect/web3modal/blob/V2/docs/react.md)
30 | - [Heroicons](https://heroicons.com/)
31 | - [Headlessui](https://headlessui.com/)
32 |
33 | ## Dev stacks
34 |
35 | - [ESLint](https://eslint.org)
36 | - [Prettier](https://prettier.io)
37 |
38 | ## VSCode useful plugins
39 |
40 | - Tailwind CSS IntelliSense: https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss
41 |
42 |
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | .env
27 | .next
28 |
29 | # typescript
30 | *.tsbuildinfo
31 | next-env.d.ts
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSameLine": true,
4 | "singleQuote": true,
5 | "jsxSingleQuote": true,
6 | "semi": true,
7 | "useTabs": false,
8 | "tabWidth": 2,
9 | "trailingComma": "all",
10 | "printWidth": 100,
11 | "quoteProps": "as-needed",
12 | "endOfLine": "auto"
13 | }
14 |
--------------------------------------------------------------------------------
/docs/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # PULL REQUEST CHECK LIST
2 |
3 | - If you have new .env variable, check if you add it in the .env.example file
4 | - Check if you have some prettier issues
5 | - Complete the README file with correct data if needed
6 | - if any changes have been made in contract, please update the abis
7 |
--------------------------------------------------------------------------------
/next-seo.config.js:
--------------------------------------------------------------------------------
1 | const title = 'TL indie';
2 | const description = 'TL indie is a forkable dapp ';
3 | const url = 'https://claim.talentlayer.org';
4 |
5 | // eslint-disable-next-line import/no-anonymous-default-export
6 | export default {
7 | title,
8 | description,
9 | canonical: url,
10 | openGraph: {
11 | type: 'website',
12 | locale: 'en_US',
13 | site_name: 'TL indie',
14 | title,
15 | description,
16 | images: [
17 | {
18 | url: `https://claim.talentlayer.org/images/cover.jpeg`,
19 | width: 2000,
20 | height: 1142,
21 | alt: 'TalentLayer profile',
22 | type: 'image/jpeg',
23 | },
24 | ],
25 | },
26 | twitter: {
27 | handle: '@TalentLayer',
28 | site: '@TalentLayer',
29 | cardType: 'summary_large_image',
30 | },
31 | };
32 |
--------------------------------------------------------------------------------
/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next-sitemap').IConfig} */
2 | module.exports = {
3 | siteUrl: 'https://claim.talentlayer.org',
4 | generateRobotsTxt: true,
5 | generateIndexSitemap: false,
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | swcMinify: true,
5 | images: {
6 | domains: ['ipfs.io'],
7 | }
8 | };
9 |
10 | module.exports = {
11 | webpack: config => {
12 | config.module.rules.push({
13 | test: /\.svg$/i,
14 | issuer: /\.[jt]sx?$/,
15 | use: [{ loader: '@svgr/webpack', options: { icon: true } }],
16 | });
17 |
18 | return config;
19 | },
20 | ...nextConfig,
21 | };
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web3-boilerplate",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@enzoferey/ethers-error-parser": "^0.2.2",
7 | "@headlessui/react": "^1.7.3",
8 | "@heroicons/react": "^2.0.12",
9 | "@types/node": "^16.18.0",
10 | "@types/react": "^18.0.23",
11 | "@types/react-dom": "^18.0.7",
12 | "@web3modal/ethereum": "^2.0.0-beta.5",
13 | "@web3modal/react": "^2.0.0-beta.5",
14 | "@xmtp/xmtp-js": "7.13.1",
15 | "axios": "^1.1.3",
16 | "ethers": "^5.7.2",
17 | "formik": "^2.2.9",
18 | "heroicons-react": "^1.4.1",
19 | "ipfs-http-client": "^59.0.0",
20 | "next": "^13.3.4",
21 | "next-seo": "^6.0.0",
22 | "next-themes": "^0.2.1",
23 | "nextjs-google-analytics": "^2.3.3",
24 | "react": "^18.2.0",
25 | "react-dom": "^18.2.0",
26 | "react-router-dom": "^6.4.2",
27 | "react-toastify": "^9.1.1",
28 | "wagmi": "^0.8.5",
29 | "yup": "^0.32.11"
30 | },
31 | "scripts": {
32 | "dev": "next dev",
33 | "build": "next build",
34 | "start": "next start",
35 | "format": "prettier --config .prettierrc 'src/**/*.{js,ts,tsx}' --write",
36 | "lint": "tsc --noEmit && eslint",
37 | "lint:fix": "tsc --noEmit && eslint --fix"
38 | },
39 | "eslintConfig": {
40 | "extends": [
41 | "react-app"
42 | ]
43 | },
44 | "browserslist": {
45 | "production": [
46 | ">0.2%",
47 | "not dead",
48 | "not op_mini all"
49 | ],
50 | "development": [
51 | "last 1 chrome version",
52 | "last 1 firefox version",
53 | "last 1 safari version"
54 | ]
55 | },
56 | "devDependencies": {
57 | "@tailwindcss/forms": "^0.5.3",
58 | "@tailwindcss/line-clamp": "^0.4.2",
59 | "@typescript-eslint/eslint-plugin": "^5.41.0",
60 | "@typescript-eslint/parser": "^5.41.0",
61 | "autoprefixer": "^10.4.12",
62 | "eslint": "^8.26.0",
63 | "eslint-config-prettier": "^8.5.0",
64 | "eslint-plugin-prettier": "^4.2.1",
65 | "postcss": "^8.4.18",
66 | "prettier": "2.7.1",
67 | "tailwindcss": "^3.2.1",
68 | "typescript": "^4.8.4"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #333333
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/images/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/images/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/images/default-avatar-0.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-0.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-1.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-1.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-10.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-10.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-11.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-11.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-2.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-3.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-3.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-4.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-4.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-5.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-5.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-6.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-6.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-7.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-7.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-8.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-8.jpeg
--------------------------------------------------------------------------------
/public/images/default-avatar-9.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/default-avatar-9.jpeg
--------------------------------------------------------------------------------
/public/images/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/logo192.png
--------------------------------------------------------------------------------
/public/images/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/logo512.png
--------------------------------------------------------------------------------
/public/images/matic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/matic.png
--------------------------------------------------------------------------------
/public/images/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/mstile-144x144.png
--------------------------------------------------------------------------------
/public/images/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/mstile-150x150.png
--------------------------------------------------------------------------------
/public/images/mstile-310x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/mstile-310x150.png
--------------------------------------------------------------------------------
/public/images/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/mstile-310x310.png
--------------------------------------------------------------------------------
/public/images/mstile-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/mstile-70x70.png
--------------------------------------------------------------------------------
/public/images/purple_checkmark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.14, written by Peter Selinger 2001-2017
9 |
10 |
12 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/public/images/zkpow/feature-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/feature-1.png
--------------------------------------------------------------------------------
/public/images/zkpow/feature-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/feature-2.png
--------------------------------------------------------------------------------
/public/images/zkpow/hands.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/hands.webp
--------------------------------------------------------------------------------
/public/images/zkpow/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/hero.png
--------------------------------------------------------------------------------
/public/images/zkpow/indie.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/indie.webp
--------------------------------------------------------------------------------
/public/images/zkpow/ninja.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/ninja.webp
--------------------------------------------------------------------------------
/public/images/zkpow/php.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/php.webp
--------------------------------------------------------------------------------
/public/images/zkpow/vault.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/vault.webp
--------------------------------------------------------------------------------
/public/images/zkpow/zkpow.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/zkpow.webp
--------------------------------------------------------------------------------
/public/images/zkpow/zkpow_w.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TalentLayer-Labs/indie-frontend/9cc87f0a69413e2c9a55add300d3a16ca94bcd0e/public/images/zkpow/zkpow_w.webp
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "TL Workshop",
3 | "name": "Talent Layer Workshop",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TalentLayer indie",
3 | "short_name": "TL indie",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#333333",
17 | "background_color": "#333333",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/src/chains.ts:
--------------------------------------------------------------------------------
1 | import { chain } from 'wagmi';
2 |
3 | export const customChains = {
4 | ...chain,
5 | fuji: {
6 | id: 43_113,
7 | name: 'Fuji',
8 | network: 'fuji',
9 | nativeCurrency: {
10 | decimals: 18,
11 | name: 'Avalanche',
12 | symbol: 'AVAX',
13 | },
14 | rpcUrls: {
15 | default: 'https://api.avax-test.network/ext/C/rpc',
16 | },
17 | blockExplorers: {
18 | default: { name: 'testnet.snowTrace', url: 'https://testnet.snowtrace.io/' },
19 | },
20 | testnet: false,
21 | },
22 | local: {
23 | id: 1337,
24 | name: 'localhost',
25 | network: 'localhost',
26 | nativeCurrency: {
27 | decimals: 18,
28 | name: 'Ethereum',
29 | symbol: 'ETH',
30 | },
31 | rpcUrls: {
32 | default: 'https://api.avax-test.network/ext/C/rpc',
33 | },
34 | blockExplorers: {
35 | default: { name: 'testnet.snowTrace', url: 'https://testnet.snowtrace.io/' },
36 | },
37 | testnet: false,
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/Back.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 |
3 | function Back() {
4 | const router = useRouter();
5 |
6 | return (
7 |
8 |
9 |
10 | router.back()}
13 | className='text-sm font-medium text-gray-700 hover:text-gray-900 inline-flex items-center'>
14 |
18 |
19 |
20 | Back
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | export default Back;
29 |
--------------------------------------------------------------------------------
/src/components/ConnectBlock.tsx:
--------------------------------------------------------------------------------
1 | import { ConnectButton } from '@web3modal/react';
2 |
3 | function ConnectBlock() {
4 | return (
5 |
6 |
7 | In order to create your profile, post a job, access messaging... you need first to connect
8 | your wallet
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default ConnectBlock;
16 |
--------------------------------------------------------------------------------
/src/components/Form/SearchServiceButton.tsx:
--------------------------------------------------------------------------------
1 | import { FormEvent, useCallback, useEffect, useState } from 'react';
2 | import { useRouter } from 'next/router';
3 |
4 | function SearchServiceButton(props?: { value?: string }) {
5 | const router = useRouter();
6 | const [searchQuery, setSearchQuery] = useState('');
7 |
8 | useEffect(() => {
9 | setSearchQuery(props!.value || '');
10 | }, [props!.value]);
11 |
12 | const handleSubmit = useCallback((e: FormEvent) => {
13 | e.preventDefault();
14 | const formElm = e.target as HTMLFormElement;
15 | const searchQueryRef = formElm.querySelector('input')!.value;
16 | if (searchQueryRef.length > 0) {
17 | router.push({
18 | pathname: '/services',
19 | query: { search: searchQueryRef },
20 | });
21 | } else router.push('/services');
22 | }, []);
23 |
24 | return (
25 |
60 | );
61 | }
62 |
63 | export default SearchServiceButton;
64 |
--------------------------------------------------------------------------------
/src/components/Form/SearchTalentButton.tsx:
--------------------------------------------------------------------------------
1 | import { FormEvent, useCallback, useEffect, useState } from 'react';
2 | import { useRouter } from 'next/router';
3 |
4 | function SearchTalentButton(props?: { value?: string }) {
5 | const router = useRouter();
6 | const [searchQuery, setSearchQuery] = useState('');
7 |
8 | useEffect(() => {
9 | setSearchQuery(props!.value || '');
10 | }, [props!.value]);
11 |
12 | const handleSubmit = useCallback((e: FormEvent) => {
13 | e.preventDefault();
14 | const formElm = e.target as HTMLFormElement;
15 | const searchQueryRef = formElm.querySelector('input')!.value;
16 | if (searchQueryRef.length > 0) {
17 | router.push({
18 | pathname: '/talents',
19 | query: { search: searchQueryRef },
20 | });
21 | } else router.push('/talents');
22 | }, []);
23 |
24 | return (
25 |
61 | );
62 | }
63 |
64 | export default SearchTalentButton;
65 |
--------------------------------------------------------------------------------
/src/components/Form/handle-price.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | export function HandlePrice({ handle }: { handle: string }) {
4 | const length = handle.length;
5 | const price = length > 4 ? 1 : 200 / Math.pow(2, handle.length - 1);
6 | return (
7 |
8 |
{price} MATIC
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/HelpPopover.tsx:
--------------------------------------------------------------------------------
1 | import { QuestionMarkCircle } from 'heroicons-react';
2 | import { useState } from 'react';
3 |
4 | function HelpPopover(props: { children: React.ReactNode }) {
5 | const [showHelp, setShowHelp] = useState(false);
6 |
7 | return (
8 |
9 |
10 | {
13 | setShowHelp(!showHelp);
14 | e.preventDefault();
15 | }}>
16 |
17 |
18 |
19 |
23 |
{props.children}
24 |
25 |
26 | );
27 | }
28 |
29 | export default HelpPopover;
30 |
--------------------------------------------------------------------------------
/src/components/Home/CreateId.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import TalentLayerContext from '../../context/talentLayer';
3 | import TalentLayerIdForm from '../Form/TalentLayerIdForm';
4 |
5 | function CreateId() {
6 | const { user } = useContext(TalentLayerContext);
7 |
8 | if (user) {
9 | return null;
10 | }
11 |
12 | return (
13 | <>
14 |
15 |
16 |
17 |
18 | Create Your TalentLayer ID
19 |
20 |
21 |
22 | Own your reputation as an indie freelancer.
23 |
24 | Onboard your clients, leave mutual reviews, and grow your reputation.
25 |
26 |
27 |
28 |
29 |
30 |
31 | >
32 | );
33 | }
34 |
35 | export default CreateId;
36 |
--------------------------------------------------------------------------------
/src/components/Home/SearchService.tsx:
--------------------------------------------------------------------------------
1 | import SearchServiceButton from '../Form/SearchServiceButton';
2 |
3 | function SearchService() {
4 | return (
5 |
6 |
7 |
8 |
9 | Find your Next Job Now
10 |
11 |
12 | Earn money doing what you love. Find a job that fits your skills and schedule.
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default SearchService;
22 |
--------------------------------------------------------------------------------
/src/components/Home/SearchTalent.tsx:
--------------------------------------------------------------------------------
1 | import SearchTalentButton from '../Form/SearchTalentButton';
2 |
3 | function SearchTalent() {
4 | return (
5 |
6 |
7 |
8 |
9 | Search a Talent
10 |
11 |
12 | Hire the best freelance, verified their reviews and start working together.
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | export default SearchTalent;
22 |
--------------------------------------------------------------------------------
/src/components/Layout/Logo.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 |
3 | function Logo() {
4 | return (
5 |
6 |
7 | TLindie
8 |
9 |
10 | );
11 | }
12 |
13 | export default Logo;
14 |
--------------------------------------------------------------------------------
/src/components/Layout/SideLink.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 |
3 | function SideLink({ children, href }: { children: React.ReactNode; href: string }) {
4 | const router = useRouter();
5 | let className =
6 | router.asPath === href ? 'bg-indigo-800 text-white' : 'text-indigo-100 hover:bg-indigo-700';
7 |
8 | className += ' group flex items-center px-2 py-2 text-base font-medium rounded-md';
9 |
10 | const handleClick = (e: any) => {
11 | e.preventDefault();
12 | router.push(href);
13 | };
14 |
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | }
21 |
22 | export default SideLink;
23 |
--------------------------------------------------------------------------------
/src/components/Layout/SideMenu.tsx:
--------------------------------------------------------------------------------
1 | import { ChatBubbleLeftIcon } from '@heroicons/react/20/solid';
2 | import {
3 | HomeIcon,
4 | MagnifyingGlassIcon,
5 | PlusIcon,
6 | PresentationChartBarIcon,
7 | SparklesIcon,
8 | } from '@heroicons/react/24/outline';
9 | import SideLink from './SideLink';
10 |
11 | const navigation = [
12 | { name: 'Presentation', href: '/', icon: HomeIcon, current: false },
13 | { name: 'Your dashboard', href: '/dashboard', icon: PresentationChartBarIcon, current: true },
14 | { name: 'Find jobs', href: '/services', icon: MagnifyingGlassIcon, current: false },
15 | { name: 'Post a job', href: '/services/create', icon: PlusIcon, current: false },
16 | { name: 'Find talents', href: '/talents', icon: SparklesIcon, current: false },
17 | { name: 'Messaging', href: '/messaging', icon: ChatBubbleLeftIcon, current: false },
18 | ];
19 |
20 | function SideMenu() {
21 | return (
22 |
23 | {navigation.map(item => (
24 |
25 |
26 | {item.name}
27 |
28 | ))}
29 |
30 | );
31 | }
32 |
33 | export default SideMenu;
34 |
--------------------------------------------------------------------------------
/src/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | function Loading({ size = '8' }: { size?: string }) {
2 | return (
3 |
4 |
9 |
13 |
17 |
18 |
Loading...
19 |
20 | );
21 | }
22 |
23 | export default Loading;
24 |
--------------------------------------------------------------------------------
/src/components/Modal/ReviewModal.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { IService, IUser } from '../../types';
3 | import ReviewForm from '../Form/ReviewForm';
4 |
5 | function ReviewModal({ service, userToReview }: { service: IService; userToReview: IUser }) {
6 | const [show, setShow] = useState(false);
7 |
8 | return (
9 | <>
10 | setShow(true)}
12 | className='block text-green-600 bg-green-50 hover:bg-green-500 hover:text-white rounded-lg px-5 py-2.5 text-center'
13 | type='button'
14 | data-modal-toggle='defaultModal'>
15 | Create a review
16 |
17 |
18 |
22 |
23 |
24 |
25 |
Create a review
26 |
setShow(false)}
28 | type='button'
29 | className='text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center '
30 | data-modal-toggle='defaultModal'>
31 |
36 |
40 |
41 | Close modal
42 |
43 |
44 |
45 |
46 | How did the job go with {userToReview.handle}?
47 |
48 | {show &&
}
49 |
50 |
51 |
52 |
53 | >
54 | );
55 | }
56 |
57 | export default ReviewModal;
58 |
--------------------------------------------------------------------------------
/src/components/MultiStepsTransactionToast.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react';
2 | import ToastStep from './ToastStep';
3 | import { useNetwork } from 'wagmi';
4 |
5 | function MultiStepsTransactionToast({
6 | transactionHash,
7 | currentStep,
8 | hasOffchainData = true,
9 | }: {
10 | transactionHash: string;
11 | currentStep: number;
12 | hasOffchainData?: boolean;
13 | }) {
14 | const network = useNetwork();
15 | const renderTransaction = useCallback(() => {
16 | return (
17 |
21 |
22 | Follow on {network.chain?.blockExplorers?.default.name}
23 |
24 |
25 | );
26 | }, [transactionHash]);
27 |
28 | const steps = [
29 | {
30 | title: 'Execute the transaction',
31 | status: currentStep > 1 ? 'complete' : 'current',
32 | render: renderTransaction,
33 | },
34 | {
35 | title: 'Synchronize onChain data',
36 | status: currentStep > 2 ? 'complete' : currentStep == 2 ? 'current' : 'upcomming',
37 | },
38 | {
39 | title: 'Synchronize offChain data',
40 | status: currentStep > 3 ? 'complete' : currentStep == 3 ? 'current' : 'upcomming',
41 | },
42 | ];
43 |
44 | if (!hasOffchainData) {
45 | steps.splice(2, 1);
46 | }
47 |
48 | return (
49 |
50 |
51 |
52 | {steps.map((step, index) => (
53 |
54 | {step.render || null}
55 |
56 | ))}
57 |
58 |
59 |
60 | );
61 | }
62 | export default MultiStepsTransactionToast;
63 |
--------------------------------------------------------------------------------
/src/components/NetworkLink.tsx:
--------------------------------------------------------------------------------
1 | import { useNetwork, useSwitchNetwork } from 'wagmi';
2 |
3 | function NetworkLink({ chaindId, chainName }: { chaindId: number; chainName: string }) {
4 | const { switchNetwork } = useSwitchNetwork({
5 | chainId: chaindId,
6 | });
7 | const network = useNetwork();
8 |
9 | if (!switchNetwork) {
10 | return null;
11 | }
12 |
13 | return (
14 | {
16 | switchNetwork();
17 | }}
18 | className={`cursor-pointer text-gray-700 block px-4 py-2 text-sm' ${
19 | network?.chain?.id === chaindId ? 'bg-gray-100 ' : ''
20 | }`}>
21 | {chainName}
22 |
23 | );
24 | }
25 |
26 | export default NetworkLink;
27 |
--------------------------------------------------------------------------------
/src/components/NetworkSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { Menu, Transition } from '@headlessui/react';
2 | import { ChevronDownIcon } from '@heroicons/react/24/outline';
3 | import { Fragment } from 'react';
4 | import { useNetwork } from 'wagmi';
5 | import NetworkLink from './NetworkLink';
6 |
7 | const chainIdToName = (chainId: number) => {
8 | switch (chainId) {
9 | case 1:
10 | return 'Ethereum';
11 | case 5:
12 | return 'Goerli';
13 | case 1337:
14 | return 'Localhost';
15 | case 43113:
16 | return 'Fuji';
17 | case 80001:
18 | return 'Mumbai';
19 | case 137:
20 | return 'Polygon';
21 | default:
22 | return 'Unknown';
23 | }
24 | };
25 |
26 | function NetworkSwitch() {
27 | const network = useNetwork();
28 |
29 | if (network?.chain === undefined) {
30 | return null;
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 | {network?.chain?.id ? chainIdToName(network.chain.id) : 'Select a network'}
38 |
39 |
40 |
41 |
42 |
43 |
51 |
52 |
53 |
54 |
59 |
60 |
61 |
62 |
63 |
64 | );
65 | }
66 |
67 | export default NetworkSwitch;
68 |
--------------------------------------------------------------------------------
/src/components/ReviewItem.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { IReview } from '../types';
3 | import { formatDate } from '../utils/dates';
4 |
5 | function ReviewItem({ review }: { review: IReview }) {
6 | if (!review) {
7 | return null;
8 | }
9 |
10 | return (
11 |
12 |
13 |
14 |
15 |
22 |
23 |
{review.to.handle}
24 |
25 | Review created the {formatDate(Number(review.createdAt) * 1000)}
26 |
27 |
28 |
29 |
30 |
31 |
32 | Rating: {review.rating}
33 |
34 |
35 | Message: {review.description?.content}
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | export default ReviewItem;
45 |
--------------------------------------------------------------------------------
/src/components/ServiceItem.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { IService } from '../types';
3 | import { renderTokenAmountFromConfig } from '../utils/conversion';
4 | import { formatDate } from '../utils/dates';
5 | import Image from 'next/image';
6 |
7 | function ServiceItem({ service }: { service: IService }) {
8 | return (
9 |
10 |
11 |
12 |
13 |
20 |
21 |
{service.description?.title}
22 |
23 | created by {service.buyer.handle} the {formatDate(Number(service.createdAt) * 1000)}
24 |
25 |
26 |
27 |
28 |
29 |
30 | {service.description?.keywords_raw?.split(',').map((keyword, i) => (
31 |
34 | {keyword}
35 |
36 | ))}
37 |
38 |
39 | About: {service.description?.about}
40 |
41 |
42 |
43 |
44 |
45 | {service.description?.rateToken && service.description?.rateAmount && (
46 |
47 | {renderTokenAmountFromConfig(
48 | service.description.rateToken,
49 | service.description.rateAmount,
50 | )}
51 |
52 | )}
53 |
56 | Show details
57 |
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | export default ServiceItem;
65 |
--------------------------------------------------------------------------------
/src/components/ServiceStatus.tsx:
--------------------------------------------------------------------------------
1 | import { ServiceStatusEnum } from '../types';
2 |
3 | function ServiceStatus({ status }: { status: ServiceStatusEnum }) {
4 | let color;
5 | switch (status) {
6 | case ServiceStatusEnum.Opened:
7 | color = 'indigo';
8 | break;
9 | case ServiceStatusEnum.Confirmed:
10 | color = 'green';
11 | break;
12 | case ServiceStatusEnum.Finished:
13 | color = 'gray';
14 | break;
15 | case ServiceStatusEnum.Cancelled:
16 | color = 'red';
17 | break;
18 | case ServiceStatusEnum.Uncompleted:
19 | color = 'red';
20 | break;
21 | }
22 |
23 | return (
24 |
26 | {status}
27 |
28 | );
29 | }
30 |
31 | export default ServiceStatus;
32 |
--------------------------------------------------------------------------------
/src/components/Stars.tsx:
--------------------------------------------------------------------------------
1 | function Stars({ rating, numReviews }: { rating: number; numReviews: number }) {
2 | return (
3 |
4 | {[...Array(5)].map((_, i) => (
5 |
11 | First star
12 |
13 |
14 | ))}
15 |
16 |
17 | {rating}/5
18 |
19 |
20 | {numReviews} review{numReviews > 1 ? 's' : ''}
21 |
22 |
23 | );
24 | }
25 |
26 | export default Stars;
27 |
--------------------------------------------------------------------------------
/src/components/Steps.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import TalentLayerContext from '../context/talentLayer';
3 | import ConnectBlock from './ConnectBlock';
4 | import TalentLayerIdForm from './Form/TalentLayerIdForm';
5 | import Step from './Step';
6 |
7 | function Steps({ targetTitle }: { targetTitle: string }) {
8 | const { account, user } = useContext(TalentLayerContext);
9 |
10 | const isLastStep = !!user;
11 | if (isLastStep) {
12 | return null;
13 | }
14 |
15 | return (
16 | <>
17 |
18 |
19 |
24 |
29 |
35 |
36 |
37 |
38 | {!account?.isConnected && }
39 | {account?.isConnected && !user && }
40 | >
41 | );
42 | }
43 |
44 | export default Steps;
45 |
--------------------------------------------------------------------------------
/src/components/TimeoutCountDown.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | interface TimeLeft {
4 | days: number;
5 | hours: number;
6 | minutes: number;
7 | seconds: number;
8 | }
9 | function TimeOutCountDown({ targetDate }: { targetDate: number }) {
10 | const [timeLeft, setTimeLeft] = useState();
11 | const calculateTimeLeft = (): TimeLeft => {
12 | const difference = targetDate - Date.now();
13 | let timeLeft: TimeLeft = { days: 0, hours: 0, minutes: 0, seconds: 0 };
14 | if (difference > 0) {
15 | timeLeft = {
16 | days: Math.floor(difference / (1000 * 60 * 60 * 24)),
17 | hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
18 | minutes: Math.floor((difference / 1000 / 60) % 60),
19 | seconds: Math.floor((difference / 1000) % 60),
20 | };
21 | }
22 | return timeLeft;
23 | };
24 |
25 | useEffect(() => {
26 | const timer = setTimeout(() => {
27 | setTimeLeft(calculateTimeLeft());
28 | }, 1000);
29 |
30 | return () => clearTimeout(timer);
31 | });
32 |
33 | return (
34 | <>
35 |
36 | Timeout:
37 |
38 |
41 | {timeLeft && timeLeft.days !== 0 && (
42 |
43 |
{timeLeft.days}
44 |
days
45 |
46 | )}
47 |
48 | {timeLeft && (timeLeft.hours !== 0 || timeLeft.days !== 0) && (
49 |
50 |
51 |
{timeLeft.hours}
52 |
hours
53 |
54 | )}
55 |
56 | {timeLeft && (timeLeft.minutes !== 0 || timeLeft.hours !== 0 || timeLeft.days !== 0) && (
57 |
58 |
{timeLeft.minutes}
59 |
min
60 |
61 | )}
62 |
63 | {timeLeft?.days === 0 && (
64 |
65 |
{timeLeft.seconds}
66 |
sec
67 |
68 | )}
69 |
70 | {!timeLeft && Fee timeout passed
}
71 | >
72 | );
73 | }
74 |
75 | export default TimeOutCountDown;
76 |
--------------------------------------------------------------------------------
/src/components/ToastStep.tsx:
--------------------------------------------------------------------------------
1 | import { CheckCircleIcon } from '@heroicons/react/24/outline';
2 |
3 | function ToastStep({
4 | status,
5 | title,
6 | children,
7 | }: {
8 | status: string;
9 | title: string;
10 | children: (() => JSX.Element) | null;
11 | }) {
12 | return (
13 |
14 | {status === 'complete' ? (
15 |
16 |
17 |
18 |
19 |
20 |
21 | {title}
22 |
23 |
24 | <>{children && children()}>
25 |
26 | ) : status === 'current' ? (
27 |
28 |
29 |
30 |
31 |
32 | {title}
33 | <>{children && children()}>
34 |
35 | ) : (
36 |
37 |
38 |
41 |
42 | {title}
43 |
44 |
45 | <>{children && children()}>
46 |
47 | )}
48 |
49 | );
50 | }
51 |
52 | export default ToastStep;
53 |
--------------------------------------------------------------------------------
/src/components/TransactionToast.tsx:
--------------------------------------------------------------------------------
1 | import { useNetwork } from 'wagmi';
2 |
3 | function TransactionToast({
4 | message,
5 | transactionHash,
6 | }: {
7 | message: string;
8 | transactionHash: string;
9 | }) {
10 | const network = useNetwork();
11 | return (
12 |
16 | New transaction
17 | {message}
18 |
19 | Follow on {network.chain?.blockExplorers?.default.name}
20 |
21 |
22 | );
23 | }
24 | export default TransactionToast;
25 |
--------------------------------------------------------------------------------
/src/components/UserGains.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import useTotalGainByUser from '../hooks/useTotalGainByUser';
3 | import { IUser } from '../types';
4 | import { renderTokenAmount } from '../utils/conversion';
5 |
6 | interface IProps {
7 | user: IUser;
8 | }
9 |
10 | function UserGains({ user }: IProps) {
11 | const userGains = useTotalGainByUser(user.id);
12 | if (userGains.length === 0) {
13 | return null;
14 | }
15 |
16 | return (
17 | <>
18 |
19 | Your total gain
20 |
21 |
22 | {userGains.map((gain, i) => {
23 | return (
24 |
27 |
40 |
41 |
42 | {renderTokenAmount(gain.token, gain.totalGain)}
43 |
44 |
45 |
46 | );
47 | })}
48 |
49 | >
50 | );
51 | }
52 |
53 | export default UserGains;
54 |
--------------------------------------------------------------------------------
/src/components/UserItem.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import Link from 'next/link';
3 | import { useContext } from 'react';
4 | import TalentLayerContext from '../context/talentLayer';
5 | import useUserById from '../hooks/useUserById';
6 | import { IUser } from '../types';
7 | import Loading from './Loading';
8 | import Stars from './Stars';
9 |
10 | function UserItem({ user }: { user: IUser }) {
11 | const { user: currentUser } = useContext(TalentLayerContext);
12 | const userDescription = user?.id ? useUserById(user?.id)?.description : null;
13 |
14 | if (!user?.id) {
15 | return ;
16 | }
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
30 |
31 |
{user.handle}
32 |
{userDescription?.title || '-'}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
42 | View profile
43 |
44 | {currentUser?.id === user.id && (
45 |
48 | Edit profile
49 |
50 | )}
51 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default UserItem;
58 |
--------------------------------------------------------------------------------
/src/components/UserPayments.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import usePaymentsForUser from '../hooks/usePaymentsForUser';
3 | import { IUser } from '../types';
4 | import { renderTokenAmount } from '../utils/conversion';
5 | import { formatStringCompleteDate } from '../utils/dates';
6 | import Link from 'next/link';
7 |
8 | function UserPayments({ user }: { user: IUser }) {
9 | const { payments } = usePaymentsForUser(user.id, 10);
10 |
11 | if (payments.length === 0) {
12 | return null;
13 | }
14 | return (
15 | <>
16 |
17 | Your last incomes
18 |
19 |
20 | {payments.map((payment, i) => {
21 | return (
22 |
26 |
39 |
40 |
41 | {renderTokenAmount(payment.rateToken, payment.amount)}
42 |
43 |
44 |
45 | received the {formatStringCompleteDate(payment.createdAt)}
46 |
47 |
48 |
49 |
50 | );
51 | })}
52 |
53 | >
54 | );
55 | }
56 |
57 | export default UserPayments;
58 |
--------------------------------------------------------------------------------
/src/components/UserProposals.tsx:
--------------------------------------------------------------------------------
1 | import useProposalsByUser from '../hooks/useProposalsByUser';
2 | import { IUser } from '../types';
3 | import UserProposalItem from './UserProposalItem';
4 |
5 | interface IProps {
6 | user: IUser;
7 | }
8 |
9 | function UserProposals({ user }: IProps) {
10 | const proposals = useProposalsByUser(user.id);
11 |
12 | if (proposals.length === 0) {
13 | return null;
14 | }
15 |
16 | return (
17 | <>
18 |
19 | Your pending Proposals
20 |
21 |
22 | {proposals.map((proposal, i) => {
23 | return ;
24 | })}
25 |
26 | >
27 | );
28 | }
29 |
30 | export default UserProposals;
31 |
--------------------------------------------------------------------------------
/src/components/UserServices.tsx:
--------------------------------------------------------------------------------
1 | import useServices from '../hooks/useServices';
2 | import { IUser } from '../types';
3 | import UserServiceItem from './UserServiceItem';
4 |
5 | interface IProps {
6 | user: IUser;
7 | type: 'buyer' | 'seller';
8 | }
9 |
10 | function UserServices({ user, type }: IProps) {
11 | const { services } = useServices(
12 | undefined,
13 | type == 'buyer' ? user.id : undefined,
14 | type == 'seller' ? user.id : undefined,
15 | );
16 |
17 | if (services.length === 0) {
18 | return null;
19 | }
20 |
21 | return (
22 | <>
23 |
24 | {type == 'buyer' ? 'Jobs posted' : 'Jobs applied'}
25 |
26 |
27 | {services.map((service, i) => {
28 | return ;
29 | })}
30 |
31 |
32 | {services.length === 20 && (
33 |
36 | Load More
37 |
38 | )}
39 | >
40 | );
41 | }
42 |
43 | export default UserServices;
44 |
--------------------------------------------------------------------------------
/src/components/UserSubMenu.tsx:
--------------------------------------------------------------------------------
1 | import { Menu } from '@headlessui/react';
2 | import Link from 'next/link';
3 | import { useRouter } from 'next/router';
4 | import { useDisconnect } from 'wagmi';
5 |
6 | function UserSubMenu() {
7 | const router = useRouter();
8 | const { disconnect } = useDisconnect();
9 |
10 | return (
11 | <>
12 |
13 | {({ active }) => (
14 |
17 | Edit my profile
18 |
19 | )}
20 |
21 |
22 |
23 | {({ active }) => (
24 | {
26 | event.preventDefault();
27 | disconnect();
28 | router.push('/');
29 | }}
30 | className={`block px-4 py-2 text-sm text-left text-red-700 w-full ${
31 | active ? 'bg-gray-100' : ''
32 | }`}>
33 | Log out
34 |
35 | )}
36 |
37 | >
38 | );
39 | }
40 |
41 | export default UserSubMenu;
42 |
--------------------------------------------------------------------------------
/src/components/newlayout/container.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | interface ContainerProps {
4 | children: ReactNode;
5 | className?: string;
6 | }
7 |
8 | export const Container = ({ children, className }: ContainerProps) => {
9 | return {children}
;
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/newlayout/footer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container } from './container';
3 |
4 | export const Footer = () => {
5 | return (
6 |
7 |
8 |
9 |
10 | COPYRIGHT © 2023 TALENTLAYER
11 |
12 |
13 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/newlayout/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { Transition } from '@headlessui/react';
2 | import Image from 'next/image';
3 | import { Container } from './container';
4 |
5 | export const Navbar = () => {
6 | return (
7 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/context/talentLayer.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, ReactNode, useEffect, useMemo, useState } from 'react';
2 | import { useAccount } from 'wagmi';
3 | import { getUserByAddress } from '../queries/users';
4 | import { IAccount, IUser } from '../types';
5 | import { config } from '../config';
6 |
7 | const TalentLayerContext = createContext<{
8 | user?: IUser;
9 | account?: IAccount;
10 | isActiveDelegate: boolean;
11 | }>({
12 | user: undefined,
13 | account: undefined,
14 | isActiveDelegate: false,
15 | });
16 |
17 | const TalentLayerProvider = ({ children }: { children: ReactNode }) => {
18 | const [user, setUser] = useState();
19 | const account = useAccount();
20 | const [isActiveDelegate, setIsActiveDelegate] = useState(false);
21 |
22 | useEffect(() => {
23 | const fetchData = async () => {
24 | if (!account.address || !account.isConnected) {
25 | return;
26 | }
27 |
28 | try {
29 | const response = await getUserByAddress(account.address);
30 | if (response?.data?.data?.users[0] !== null) {
31 | setUser(response.data.data.users[0]);
32 | setIsActiveDelegate(
33 | process.env.NEXT_PUBLIC_ACTIVE_DELEGATE &&
34 | response.data.data.users[0].delegates &&
35 | response.data.data.users[0].delegates.indexOf(
36 | (process.env.NEXT_PUBLIC_DELEGATE_ADDRESS as string).toLowerCase(),
37 | ) !== -1,
38 | );
39 | }
40 | } catch (err: any) {
41 | // eslint-disable-next-line no-console
42 | console.error(err);
43 | }
44 | };
45 | fetchData();
46 | }, [account.address, account.isConnected, isActiveDelegate]);
47 |
48 | const value = useMemo(() => {
49 | return {
50 | user,
51 | account: account ? account : undefined,
52 | isActiveDelegate,
53 | };
54 | }, [account.address, user?.id, isActiveDelegate]);
55 |
56 | return {children} ;
57 | };
58 |
59 | export { TalentLayerProvider };
60 |
61 | export default TalentLayerContext;
62 |
--------------------------------------------------------------------------------
/src/contracts/executePayment.tsx:
--------------------------------------------------------------------------------
1 | import { Provider } from '@wagmi/core';
2 | import { BigNumber, Contract, Signer, ethers } from 'ethers';
3 | import { toast } from 'react-toastify';
4 | import TransactionToast from '../components/TransactionToast';
5 | import { config } from '../config';
6 | import TalentLayerEscrow from './ABI/TalentLayerEscrow.json';
7 | import { showErrorTransactionToast } from '../utils/toast';
8 | import { delegateReleaseOrReimburse } from '../components/request';
9 |
10 | export const executePayment = async (
11 | userAddress: string,
12 | signer: Signer,
13 | provider: Provider,
14 | profileId: string,
15 | transactionId: string,
16 | amount: BigNumber,
17 | isBuyer: boolean,
18 | isActiveDelegate: boolean,
19 | ): Promise => {
20 | try {
21 | let tx: ethers.providers.TransactionResponse;
22 |
23 | if (isActiveDelegate) {
24 | const response = await delegateReleaseOrReimburse(
25 | userAddress,
26 | profileId,
27 | parseInt(transactionId, 10),
28 | amount.toString(),
29 | isBuyer,
30 | );
31 | tx = response.data.transaction;
32 | } else {
33 | const talentLayerEscrow = new Contract(
34 | config.contracts.talentLayerEscrow,
35 | TalentLayerEscrow.abi,
36 | signer,
37 | );
38 | tx = isBuyer
39 | ? await talentLayerEscrow.release(profileId, parseInt(transactionId, 10), amount.toString())
40 | : await talentLayerEscrow.reimburse(
41 | profileId,
42 | parseInt(transactionId, 10),
43 | amount.toString(),
44 | );
45 | }
46 |
47 | const message = isBuyer
48 | ? 'Your payment release is in progress'
49 | : 'Your payment reimbursement is in progress';
50 |
51 | const receipt = await toast.promise(provider.waitForTransaction(tx.hash), {
52 | pending: {
53 | render() {
54 | return ;
55 | },
56 | },
57 | success: isBuyer ? 'Payment release validated' : 'Payment reimbursement validated',
58 | error: 'An error occurred while validating your transaction',
59 | });
60 | if (receipt.status !== 1) {
61 | throw new Error('Approve Transaction failed');
62 | }
63 | } catch (error: any) {
64 | showErrorTransactionToast(error);
65 | }
66 | };
67 |
--------------------------------------------------------------------------------
/src/contracts/toggleDelegation.tsx:
--------------------------------------------------------------------------------
1 | import { Provider } from '@wagmi/core';
2 | import { ethers } from 'ethers';
3 | import { createMultiStepsTransactionToast, showErrorTransactionToast } from '../utils/toast';
4 |
5 | export const toggleDelegation = async (
6 | user: string,
7 | DelegateAddress: string,
8 | provider: Provider,
9 | validateState: boolean,
10 | contract: ethers.Contract,
11 | ): Promise => {
12 | try {
13 | let tx: ethers.providers.TransactionResponse;
14 | let toastMessages;
15 | if (validateState === true) {
16 | tx = await contract.addDelegate(user, DelegateAddress);
17 | toastMessages = {
18 | pending: 'Submitting the delegation...',
19 | success: 'Congrats! the delegation is active',
20 | error: 'An error occurred while delegation process',
21 | };
22 | } else {
23 | tx = await contract.removeDelegate(user, DelegateAddress);
24 | toastMessages = {
25 | pending: 'Canceling the delegation...',
26 | success: 'The delegation has been canceled',
27 | error: 'An error occurred while canceling the delegation',
28 | };
29 | }
30 |
31 | await createMultiStepsTransactionToast(toastMessages, provider, tx, 'Delegation');
32 | } catch (error) {
33 | showErrorTransactionToast(error);
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/src/hooks/useAllowedToken.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { IToken } from '../types';
3 | import { getAllowedToken } from '../queries/global';
4 |
5 | const useAllowedToken = (address: string): IToken | undefined => {
6 | const [allowedToken, setAllowedToken] = useState();
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | if (address) {
12 | const response = await getAllowedToken(address);
13 | console.log('response', response);
14 | if (response?.data?.data?.tokens) {
15 | console.log('response.data.data.tokens[0]', response.data.data.tokens[0]);
16 | setAllowedToken(response.data.data.tokens[0]);
17 | }
18 | }
19 | } catch (error: any) {
20 | // eslint-disable-next-line no-console
21 | console.error(error);
22 | }
23 | };
24 | fetchData();
25 | }, []);
26 |
27 | return allowedToken;
28 | };
29 |
30 | export default useAllowedToken;
31 |
--------------------------------------------------------------------------------
/src/hooks/useAllowedTokens.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { IToken } from '../types';
3 | import { getAllowedTokenList } from '../queries/global';
4 |
5 | const useAllowedTokens = (): IToken[] => {
6 | const [allowedTokens, setAllowedTokens] = useState([]);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await getAllowedTokenList();
12 | if (response?.data?.data?.tokens) {
13 | setAllowedTokens(response.data.data.tokens);
14 | }
15 | } catch (error: any) {
16 | // eslint-disable-next-line no-console
17 | console.error(error);
18 | }
19 | };
20 | fetchData();
21 | }, []);
22 |
23 | return allowedTokens;
24 | };
25 |
26 | export default useAllowedTokens;
27 |
--------------------------------------------------------------------------------
/src/hooks/useArbitrationCost.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { BigNumber, Contract, ethers } from 'ethers';
3 | import TalentLayerArbitrator from '../contracts/ABI/TalentLayerArbitrator.json';
4 | import { useSigner } from 'wagmi';
5 |
6 | const useArbitrationCost = (arbitratorAddress: string | undefined): BigNumber | null => {
7 | const [arbitrationCost, setArbitrationCost] = useState(null);
8 | const { data: signer } = useSigner({
9 | chainId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID as string),
10 | });
11 |
12 | useEffect(() => {
13 | const fetchData = async () => {
14 | try {
15 | if (signer && arbitratorAddress && arbitratorAddress !== ethers.constants.AddressZero) {
16 | const talentLayerArbitrator = new Contract(
17 | arbitratorAddress,
18 | TalentLayerArbitrator.abi,
19 | signer,
20 | );
21 | const response = await talentLayerArbitrator.arbitrationPrice(
22 | process.env.NEXT_PUBLIC_PLATFORM_ID,
23 | );
24 | if (response) {
25 | setArbitrationCost(response);
26 | }
27 | }
28 | } catch (err: any) {
29 | // eslint-disable-next-line no-console
30 | console.error(err);
31 | }
32 | };
33 | fetchData();
34 | }, [arbitratorAddress]);
35 |
36 | return arbitrationCost;
37 | };
38 |
39 | export default useArbitrationCost;
40 |
--------------------------------------------------------------------------------
/src/hooks/useAsync.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react';
2 |
3 | export const useAsync = (asyncFunction: () => Promise, immediate = true) => {
4 | const [status, setStatus] = useState('idle');
5 | const [value, setValue] = useState(null);
6 | const [error, setError] = useState(null);
7 | // The execute function wraps asyncFunction and
8 | // handles setting state for pending, value, and error.
9 | // useCallback ensures the below useEffect is not called
10 | // on every render, but only if asyncFunction changes.
11 | const execute = useCallback(() => {
12 | setStatus('pending');
13 | setValue(null);
14 | setError(null);
15 | return asyncFunction()
16 | .then((response: any) => {
17 | setValue(response);
18 | setStatus('success');
19 | })
20 | .catch((error: any) => {
21 | setError(error);
22 | setStatus('error');
23 | });
24 | }, [asyncFunction]);
25 | // Call execute if we want to fire it right away.
26 | // Otherwise execute can be called later, such as
27 | // in an onClick handler.
28 | useEffect(() => {
29 | if (immediate) {
30 | execute();
31 | }
32 | }, [execute, immediate]);
33 | return { execute, status, value, error };
34 | };
35 |
--------------------------------------------------------------------------------
/src/hooks/useEvidences.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { IEvidence } from '../types';
3 | import { getEvidencesTransactionId } from '../queries/evidences';
4 |
5 | const useEvidences = (transactionID?: string): IEvidence[] => {
6 | const [evidences, setEvidences] = useState([]);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | if (!transactionID) return;
12 | const response = await getEvidencesTransactionId(transactionID);
13 |
14 | if (response?.data?.data?.evidences) {
15 | setEvidences(response.data.data.evidences);
16 | }
17 | } catch (error: any) {
18 | // eslint-disable-next-line no-console
19 | console.error(error);
20 | }
21 | };
22 | fetchData();
23 | }, [transactionID]);
24 |
25 | return evidences;
26 | };
27 |
28 | export default useEvidences;
29 |
--------------------------------------------------------------------------------
/src/hooks/useFees.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getProtocolAndPlatformsFees } from '../queries/fees';
3 | import { IFees } from '../types';
4 |
5 | const useFees = (
6 | originServicePlatformId: string,
7 | originValidatedProposalPlatformId: string,
8 | ): IFees => {
9 | const [fees, setFees] = useState({
10 | protocolEscrowFeeRate: 0,
11 | originServiceFeeRate: 0,
12 | originValidatedProposalFeeRate: 0,
13 | });
14 |
15 | useEffect(() => {
16 | const fees: IFees = {
17 | protocolEscrowFeeRate: 0,
18 | originServiceFeeRate: 0,
19 | originValidatedProposalFeeRate: 0,
20 | };
21 | const fetchData = async () => {
22 | try {
23 | const response = await getProtocolAndPlatformsFees(
24 | originServicePlatformId,
25 | originValidatedProposalPlatformId,
26 | );
27 | const data = response.data.data;
28 |
29 | if (data) {
30 | fees.protocolEscrowFeeRate = data.protocols[0].protocolEscrowFeeRate;
31 | fees.originServiceFeeRate = data.servicePlatform.originServiceFeeRate;
32 | fees.originValidatedProposalFeeRate =
33 | data.proposalPlatform.originValidatedProposalFeeRate;
34 | }
35 |
36 | setFees(fees);
37 | } catch (err: any) {
38 | // eslint-disable-next-line no-console
39 | console.error(err);
40 | }
41 | };
42 | fetchData();
43 | }, [originServicePlatformId, originValidatedProposalPlatformId]);
44 |
45 | return fees;
46 | };
47 |
48 | export default useFees;
49 |
--------------------------------------------------------------------------------
/src/hooks/useFilteredServices.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { IService, ServiceStatusEnum } from '../types';
3 | import { getFilteredServicesByKeywords } from '../pages/api/services/request';
4 |
5 | const useFilteredServices = (
6 | serviceStatus?: ServiceStatusEnum,
7 | buyerId?: string,
8 | sellerId?: string,
9 | searchQuery?: string,
10 | numberPerPage?: number,
11 | ): {
12 | hasMoreData: boolean;
13 | loading: boolean;
14 | services: IService[];
15 | loadMore: () => void;
16 | } => {
17 | const [services, setServices] = useState([]);
18 | const [hasMoreData, setHasMoreData] = useState(true);
19 | const [loading, setLoading] = useState(false);
20 | const [offset, setOffset] = useState(0);
21 |
22 | useEffect(() => {
23 | setServices([]);
24 | setOffset(0);
25 | }, [searchQuery]);
26 |
27 | useEffect(() => {
28 | const fetchData = async () => {
29 | try {
30 | setLoading(true);
31 | let newServices: IService[] = [];
32 |
33 | const response = await getFilteredServicesByKeywords(
34 | serviceStatus,
35 | buyerId,
36 | sellerId,
37 | numberPerPage,
38 | offset,
39 | searchQuery,
40 | );
41 |
42 | newServices = response?.data?.services;
43 |
44 | if (offset === 0) {
45 | setServices(newServices || []);
46 | } else {
47 | setServices([...services, ...newServices]);
48 | }
49 | if (numberPerPage && newServices.length < numberPerPage) {
50 | setHasMoreData(false);
51 | } else {
52 | setHasMoreData(true);
53 | }
54 | } catch (err: any) {
55 | // eslint-disable-next-line no-console
56 | console.error(err);
57 | } finally {
58 | setLoading(false);
59 | }
60 | };
61 | fetchData();
62 | }, [numberPerPage, offset, searchQuery]);
63 |
64 | const loadMore = () => {
65 | numberPerPage ? setOffset(offset + numberPerPage) : '';
66 | };
67 |
68 | return { hasMoreData: hasMoreData, services, loading, loadMore };
69 | };
70 |
71 | export default useFilteredServices;
72 |
--------------------------------------------------------------------------------
/src/hooks/useIpfsFile.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { readFileFromIpfs } from '../utils/ipfs';
3 |
4 | const useIpfsFile = (cid: false | string): any => {
5 | const [file, setFile] = useState(null);
6 |
7 | useEffect(() => {
8 | const fetchData = async () => {
9 | try {
10 | if (!cid) {
11 | return;
12 | }
13 | const response = await readFileFromIpfs(cid);
14 | if (response) {
15 | setFile(response);
16 | }
17 | } catch (err: any) {
18 | // eslint-disable-next-line no-console
19 | console.error(err);
20 | }
21 | };
22 | fetchData();
23 | }, [cid]);
24 |
25 | return file;
26 | };
27 |
28 | export default useIpfsFile;
29 |
--------------------------------------------------------------------------------
/src/hooks/useIpfsJsonData.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { readFromIpfs } from '../utils/ipfs';
3 |
4 | const useIpfsJsonData = (cid: false | string): any => {
5 | const [file, setFile] = useState(null);
6 |
7 | useEffect(() => {
8 | const fetchData = async () => {
9 | try {
10 | if (!cid) {
11 | return;
12 | }
13 | const response = await readFromIpfs(cid);
14 | if (response) {
15 | setFile(response);
16 | }
17 | } catch (err: any) {
18 | // eslint-disable-next-line no-console
19 | console.error(err);
20 | }
21 | };
22 | fetchData();
23 | }, [cid]);
24 |
25 | return file;
26 | };
27 |
28 | export default useIpfsJsonData;
29 |
--------------------------------------------------------------------------------
/src/hooks/usePaymentsByService.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getPaymentsByService } from '../queries/payments';
3 | import { IPayment, PaymentTypeEnum } from '../types';
4 |
5 | const usePaymentsByService = (id: string, paymentType?: PaymentTypeEnum): IPayment[] => {
6 | const [payments, setPayments] = useState([]);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await getPaymentsByService(id, paymentType);
12 |
13 | if (response?.data?.data?.payments) {
14 | setPayments(response.data.data.payments);
15 | }
16 | } catch (error: any) {
17 | // eslint-disable-next-line no-console
18 | console.error(error);
19 | }
20 | };
21 | fetchData();
22 | }, [id]);
23 |
24 | return payments;
25 | };
26 |
27 | export default usePaymentsByService;
28 |
--------------------------------------------------------------------------------
/src/hooks/usePaymentsForUser.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getPaymentsForUser } from '../queries/payments';
3 | import { IPayment } from '../types';
4 |
5 | const usePaymentsForUser = (
6 | id: string,
7 | numberPerPage: number,
8 | startDate?: string,
9 | endDate?: string,
10 | ): { hasMoreData: boolean; loading: boolean; payments: IPayment[]; loadMore: () => void } => {
11 | const [payments, setPayments] = useState([]);
12 | const [hasMoreData, setHasMoreData] = useState(true);
13 | const [loading, setLoading] = useState(false);
14 | const [offset, setOffset] = useState(1);
15 |
16 | const total = offset * numberPerPage;
17 |
18 | const start = startDate ? new Date(startDate).getTime() / 1000 : '';
19 | const end = endDate ? new Date(endDate).getTime() / 1000 : '';
20 |
21 | useEffect(() => {
22 | const fetchData = async () => {
23 | try {
24 | setLoading(true);
25 | const response = await getPaymentsForUser(id, total, 0, start.toString(), end.toString());
26 |
27 | if (response && response.data && response.data.data) {
28 | setPayments([...response.data.data.payments]);
29 |
30 | if (response.data.data.payments.length < total) {
31 | setHasMoreData(false);
32 | }
33 | }
34 | } catch (error: any) {
35 | // eslint-disable-next-line no-console
36 | console.error(error);
37 | } finally {
38 | setLoading(false);
39 | }
40 | };
41 | fetchData();
42 | }, [total, id, start, end]);
43 |
44 | useEffect(() => {
45 | if (!!start && !!end) {
46 | setOffset(1);
47 | setHasMoreData(true);
48 | }
49 | }, [start, end]);
50 |
51 | const loadMore = () => {
52 | setOffset(offset + 1);
53 | };
54 |
55 | return { payments, hasMoreData: hasMoreData, loading, loadMore };
56 | };
57 |
58 | export default usePaymentsForUser;
59 |
--------------------------------------------------------------------------------
/src/hooks/useProposalById.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getProposalById } from '../queries/proposals';
3 | import { IProposal } from '../types';
4 |
5 | const useProposalById = (id?: string | undefined): IProposal | undefined => {
6 | const [proposal, setProposal] = useState();
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | if (id) {
11 | try {
12 | const response = await getProposalById(id);
13 | if (response?.data?.data?.proposals[0]) {
14 | setProposal(response.data.data.proposals[0]);
15 | }
16 | } catch (error: any) {
17 | // eslint-disable-next-line no-console
18 | console.error(error);
19 | }
20 | } else {
21 | setProposal(undefined);
22 | }
23 | };
24 | fetchData();
25 | }, [id]);
26 |
27 | return proposal;
28 | };
29 |
30 | export default useProposalById;
31 |
--------------------------------------------------------------------------------
/src/hooks/useProposalsByService.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getAllProposalsByServiceId } from '../queries/proposals';
3 | import { IProposal } from '../types';
4 |
5 | const useProposalsByService = (serviceId?: string | undefined): IProposal[] => {
6 | const [proposals, setProposals] = useState([]);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | if (serviceId) {
11 | try {
12 | const response = await getAllProposalsByServiceId(serviceId);
13 |
14 | if (response?.data?.data?.proposals) {
15 | setProposals(response.data.data.proposals);
16 | }
17 | } catch (error: any) {
18 | // eslint-disable-next-line no-console
19 | console.error(error);
20 | }
21 | } else {
22 | setProposals([]);
23 | }
24 | };
25 | fetchData();
26 | }, [serviceId]);
27 |
28 | return proposals;
29 | };
30 |
31 | export default useProposalsByService;
32 |
--------------------------------------------------------------------------------
/src/hooks/useProposalsByUser.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getAllProposalsByUser } from '../queries/proposals';
3 | import { IProposal } from '../types';
4 |
5 | const useProposalsByUser = (id?: string | undefined): IProposal[] => {
6 | const [proposals, setProposals] = useState([]);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | if (id) {
11 | try {
12 | const response = await getAllProposalsByUser(id);
13 |
14 | if (response?.data?.data?.proposals) {
15 | setProposals(response.data.data.proposals);
16 | }
17 | } catch (error: any) {
18 | // eslint-disable-next-line no-console
19 | console.error(error);
20 | }
21 | } else {
22 | setProposals([]);
23 | }
24 | };
25 | fetchData();
26 | }, [id]);
27 |
28 | return proposals;
29 | };
30 |
31 | export default useProposalsByUser;
32 |
--------------------------------------------------------------------------------
/src/hooks/useReviewsByService.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getReviewsByService } from '../queries/reviews';
3 | import { IReview } from '../types';
4 |
5 | const useReviewsByService = (serviceId: string): { reviews: IReview[] } => {
6 | const [reviews, setReviews] = useState([]);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await getReviewsByService(serviceId);
12 | if (response?.data?.data?.reviews) {
13 | setReviews(response.data.data.reviews);
14 | }
15 | } catch (err: any) {
16 | // eslint-disable-next-line no-console
17 | console.error(err);
18 | }
19 | };
20 | fetchData();
21 | }, [serviceId]);
22 |
23 | return { reviews };
24 | };
25 |
26 | export default useReviewsByService;
27 |
--------------------------------------------------------------------------------
/src/hooks/useServiceById.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getServiceById } from '../queries/services';
3 | import { IService } from '../types';
4 |
5 | const useServiceById = (serviceId: string): IService | null => {
6 | const [user, setUser] = useState(null);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await getServiceById(serviceId);
12 | if (response?.data?.data?.service) {
13 | setUser(response.data.data.service);
14 | }
15 | } catch (err: any) {
16 | // eslint-disable-next-line no-console
17 | console.error(err);
18 | }
19 | };
20 | fetchData();
21 | }, [serviceId]);
22 |
23 | return user;
24 | };
25 |
26 | export default useServiceById;
27 |
--------------------------------------------------------------------------------
/src/hooks/useServiceDetails.ipfsExample.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { IServiceDetails } from '../types';
3 |
4 | /**
5 | * Example of ipfs query implementation for a service
6 | * Useful only if you need query non indexed data
7 | * @param cid
8 | */
9 | const useServiceDetails = (cid: string): IServiceDetails | null => {
10 | const [serviceDetails, setServiceDetails] = useState(null);
11 |
12 | useEffect(() => {
13 | const fetchData = async () => {
14 | try {
15 | const fullSeviceDetailsUri = `${process.env.NEXT_PUBLIC_IPFS_BASE_URL}${cid}`;
16 |
17 | const response = await fetch(fullSeviceDetailsUri);
18 |
19 | const data: IServiceDetails = await response.json();
20 | if (data) {
21 | setServiceDetails(data);
22 | }
23 | } catch (err: any) {
24 | // eslint-disable-next-line no-console
25 | console.error(err);
26 | }
27 | };
28 | fetchData();
29 | }, [cid]);
30 |
31 | return serviceDetails;
32 | };
33 |
34 | export default useServiceDetails;
35 |
--------------------------------------------------------------------------------
/src/hooks/useTotalGainByUser.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getUserTotalGains } from '../queries/users';
3 | import { IUserGain } from '../types';
4 |
5 | const useTotalGainByUser = (id?: string | undefined): IUserGain[] => {
6 | const [userGains, setUserGains] = useState([]);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | if (id) {
11 | try {
12 | const response = await getUserTotalGains(id);
13 |
14 | if (response?.data?.data?.user?.totalGains) {
15 | setUserGains(response.data.data.user.totalGains);
16 | }
17 | } catch (error: any) {
18 | // eslint-disable-next-line no-console
19 | console.error(error);
20 | }
21 | } else {
22 | setUserGains([]);
23 | }
24 | };
25 | fetchData();
26 | }, [id]);
27 |
28 | return userGains;
29 | };
30 |
31 | export default useTotalGainByUser;
32 |
--------------------------------------------------------------------------------
/src/hooks/useTransactionsById.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { ITransaction } from '../types';
3 | import { getTransactionById } from '../queries/transactions';
4 |
5 | const useTransactionsById = (id?: string | undefined): ITransaction | undefined => {
6 | const [transaction, setTransaction] = useState();
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | if (id) {
11 | try {
12 | const response = await getTransactionById(id);
13 | if (response?.data?.data?.transactions[0]) {
14 | setTransaction(response.data.data.transactions[0]);
15 | }
16 | } catch (error: any) {
17 | // eslint-disable-next-line no-console
18 | console.error(error);
19 | }
20 | } else {
21 | setTransaction(undefined);
22 | }
23 | };
24 | fetchData();
25 | }, [id]);
26 |
27 | return transaction;
28 | };
29 |
30 | export default useTransactionsById;
31 |
--------------------------------------------------------------------------------
/src/hooks/useUserByAddress.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getUserByAddress, getUserById } from '../queries/users';
3 | import { IUser } from '../types';
4 |
5 | const useUserByAddress = (address: string): IUser | null => {
6 | const [user, setUser] = useState(null);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | if (!address) {
12 | return;
13 | }
14 | const response = await getUserByAddress(address);
15 | if (response?.data?.data?.users) {
16 | setUser(response.data.data.users[0]);
17 | }
18 | } catch (err: any) {
19 | // eslint-disable-next-line no-console
20 | console.error(err);
21 | }
22 | };
23 | fetchData();
24 | }, [address]);
25 |
26 | return user;
27 | };
28 |
29 | export default useUserByAddress;
30 |
--------------------------------------------------------------------------------
/src/hooks/useUserById.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getUserById } from '../queries/users';
3 | import { IUser } from '../types';
4 |
5 | const useUserById = (userId: string): IUser | null => {
6 | const [user, setUser] = useState(null);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await getUserById(userId);
12 | if (response?.data?.data?.user) {
13 | setUser(response.data.data.user);
14 | }
15 | } catch (err: any) {
16 | // eslint-disable-next-line no-console
17 | console.error(err);
18 | }
19 | };
20 | fetchData();
21 | }, [userId]);
22 |
23 | return user;
24 | };
25 |
26 | export default useUserById;
27 |
--------------------------------------------------------------------------------
/src/hooks/useUsers.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getUsers } from '../queries/users';
3 | import { IUser } from '../types';
4 |
5 | const useUsers = (
6 | searchQuery?: string,
7 | numberPerPage?: number,
8 | ): { hasMoreData: boolean; loading: boolean; users: IUser[]; loadMore: () => void } => {
9 | const [users, setUsers] = useState([]);
10 | const [hasMoreData, setHasMoreData] = useState(true);
11 | const [loading, setLoading] = useState(false);
12 | const [offset, setOffset] = useState(0);
13 |
14 | useEffect(() => {
15 | setUsers([]);
16 | setOffset(0);
17 | }, [searchQuery]);
18 |
19 | useEffect(() => {
20 | const fetchData = async () => {
21 | try {
22 | setLoading(true);
23 | const response = await getUsers(numberPerPage, offset, searchQuery);
24 |
25 | if (offset === 0) {
26 | setUsers(response.data.data.users || []);
27 | } else {
28 | setUsers([...users, ...response.data.data.users]);
29 | }
30 |
31 | if (numberPerPage && response?.data?.data?.users.length < numberPerPage) {
32 | setHasMoreData(false);
33 | } else {
34 | setHasMoreData(true);
35 | }
36 | } catch (err: any) {
37 | // eslint-disable-next-line no-console
38 | console.error(err);
39 | } finally {
40 | setLoading(false);
41 | }
42 | };
43 | fetchData();
44 | }, [numberPerPage, offset, searchQuery]);
45 |
46 | const loadMore = () => {
47 | numberPerPage ? setOffset(offset + numberPerPage) : '';
48 | };
49 |
50 | return { users, hasMoreData: hasMoreData, loading, loadMore };
51 | };
52 |
53 | export default useUsers;
54 |
--------------------------------------------------------------------------------
/src/hooks/workx/useWorkxSkills.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback, Dispatch, SetStateAction } from 'react';
2 | import axios from 'axios';
3 | import { Skill } from '../../modules/WorkX/utils/types';
4 |
5 | export const MINIMUM_QUERY_LENGTH = 2;
6 |
7 | export const useWorkxSkills = (): {
8 | skills: Skill[];
9 | query: string;
10 | fetchData: (name: string) => void;
11 | setQuery: Dispatch>;
12 | } => {
13 | const [query, setQuery] = useState('');
14 | const [skills, setSkills] = useState([]);
15 |
16 | const fetchData = useCallback(async (query: string) => {
17 | try {
18 | if (query.length < MINIMUM_QUERY_LENGTH) return;
19 | const response = await axios.get('/api/get-skills', { params: { name: query } });
20 | if (response?.data?.skills?.length > 0) {
21 | setSkills(response.data.skills);
22 | return;
23 | }
24 |
25 | setSkills([]);
26 | } catch (err: any) {
27 | // eslint-disable-next-line no-console
28 | console.error(err);
29 | }
30 | }, []);
31 |
32 | useEffect(() => {
33 | fetchData(query);
34 | }, [fetchData]);
35 |
36 | return { skills: skills, fetchData, query, setQuery };
37 | };
38 |
--------------------------------------------------------------------------------
/src/modules/Disputes/components/DisputeStatusCard.tsx:
--------------------------------------------------------------------------------
1 | import DisputeStatusDetail from './DisputeStatusDetail';
2 | import { ITransaction, IUser, TransactionStatusEnum } from '../../../types';
3 | import TimeOutCountDown from '../../../components/TimeoutCountDown';
4 | import DisputeButton from '../../../components/DisputeButton';
5 | import { BigNumber } from 'ethers';
6 |
7 | function DisputeStatusCard({
8 | transaction,
9 | user,
10 | arbitrationFee,
11 | }: {
12 | transaction: ITransaction;
13 | user: IUser;
14 | arbitrationFee: BigNumber;
15 | }) {
16 | const isSender = (): boolean => {
17 | return !!user && !!transaction && user.id === transaction.sender.id;
18 | };
19 |
20 | const isReceiver = (): boolean => {
21 | return !!user && !!transaction && user.id === transaction.receiver.id;
22 | };
23 | const getTargetDate = () => {
24 | if (
25 | isSender() &&
26 | transaction &&
27 | (transaction.senderFeePaidAt || transaction.receiverFeePaidAt)
28 | ) {
29 | return (
30 | (Number(transaction.senderFeePaidAt) + Number(transaction.arbitrationFeeTimeout)) * 1000
31 | );
32 | }
33 |
34 | if (
35 | isReceiver() &&
36 | transaction &&
37 | (transaction.senderFeePaidAt || transaction.receiverFeePaidAt)
38 | ) {
39 | return (
40 | (Number(transaction.senderFeePaidAt || transaction.receiverFeePaidAt) +
41 | Number(transaction.arbitrationFeeTimeout)) *
42 | 1000
43 | );
44 | }
45 | return 0;
46 | };
47 |
48 | return (
49 | <>
50 |
51 |
52 |
53 | {(transaction.status === TransactionStatusEnum.WaitingReceiver ||
54 | transaction.status === TransactionStatusEnum.WaitingSender) && (
55 |
56 | )}
57 |
58 |
Date.now()}
63 | />
64 | {transaction && transaction.ruling && (
65 |
66 | {transaction.ruling === 1 ? 'Sender Wins' : 'Receiver Wins'}
67 |
68 | )}
69 |
70 | >
71 | );
72 | }
73 |
74 | export default DisputeStatusCard;
75 |
--------------------------------------------------------------------------------
/src/modules/Disputes/components/DisputeStatusDetail.tsx:
--------------------------------------------------------------------------------
1 | import { ITransaction } from '../../../types';
2 | import { formatRateAmount } from '../../../utils/web3';
3 | import { BigNumber } from 'ethers';
4 |
5 | function DisputeStatusDetail({
6 | transaction,
7 | arbitrationFee,
8 | }: {
9 | transaction: ITransaction;
10 | arbitrationFee: BigNumber | null;
11 | }) {
12 | return (
13 |
14 |
15 | Status: {transaction?.status}
16 |
17 |
18 | Arbitration fee: {' '}
19 | {arbitrationFee &&
20 | transaction?.token &&
21 | formatRateAmount(
22 | arbitrationFee.toString(),
23 | transaction?.token.address,
24 | transaction?.token.decimals,
25 | ).exactValue}{' '}
26 | MATIC
27 |
28 |
29 | Buyer fee: {' '}
30 | {transaction?.senderFeePaidAt ? (
31 | Paid
32 | ) : (
33 | Not paid
34 | )}
35 |
36 |
37 | Seller fee: {' '}
38 | {transaction?.receiverFeePaidAt ? (
39 | Paid
40 | ) : (
41 | Not paid
42 | )}
43 |
44 |
45 | );
46 | }
47 |
48 | export default DisputeStatusDetail;
49 |
--------------------------------------------------------------------------------
/src/modules/Disputes/components/EvidenceDetails.tsx:
--------------------------------------------------------------------------------
1 | import MetaEvidenceModal from './MetaEvidenceModal';
2 | import useEvidences from '../../../hooks/useEvidences';
3 | import { IProposal, ITransaction } from '../../../types';
4 | import Evidences from './Evidences';
5 | import { ethers } from 'ethers';
6 |
7 | function EvidenceDetails({
8 | transaction,
9 | proposal,
10 | }: {
11 | transaction: ITransaction;
12 | proposal: IProposal;
13 | }) {
14 | const evidences = useEvidences(transaction.id);
15 |
16 | const buyerEvidences = evidences.filter(
17 | evidence => evidence.party.id === proposal?.service.buyer.id,
18 | );
19 | const sellerEvidences = evidences.filter(evidence => evidence.party.id === proposal?.seller.id);
20 | return (
21 |
22 |
27 |
28 | {transaction.receiver && proposal.service && proposal.description && (
29 |
30 | Meta evidence:
31 |
46 |
47 | )}
48 |
49 | );
50 | }
51 |
52 | export default EvidenceDetails;
53 |
--------------------------------------------------------------------------------
/src/modules/Disputes/components/Evidences.tsx:
--------------------------------------------------------------------------------
1 | import EvidenceModal from './EvidenceModal';
2 | import { IEvidence } from '../../../types';
3 |
4 | function Evidences({
5 | evidences,
6 | partyHandle,
7 | title,
8 | }: {
9 | evidences: IEvidence[];
10 | title: string;
11 | partyHandle?: string;
12 | }) {
13 | return (
14 | <>
15 | {title}
16 |
17 | {evidences.length > 0 &&
18 | evidences.map(evidence => {
19 | return (
20 | evidence.description && (
21 |
29 | )
30 | );
31 | })}
32 |
33 | >
34 | );
35 | }
36 |
37 | export default Evidences;
38 |
--------------------------------------------------------------------------------
/src/modules/Disputes/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type IMetaEvidence = {
2 | fileURI: string;
3 | fileHash: string;
4 | fileTypeExtension: string;
5 | category: string;
6 | title: string;
7 | description: string;
8 | aliases: {
9 | [string: string]: string;
10 | };
11 | question: string;
12 | rulingOptions: {
13 | type: string;
14 | precision: number;
15 | titles: string[];
16 | descriptions: string[];
17 | };
18 | evidenceDisplayInterfaceURI: string;
19 | evidenceDisplayInterfaceHash: string;
20 | dynamicScriptURI: string;
21 | dynamicScriptHash: string;
22 | };
23 |
24 | export type IERC1497Evidence = {
25 | fileUri: string;
26 | fileHash: string;
27 | fileTypeExtension: string;
28 | name: string;
29 | description: string;
30 | };
31 |
--------------------------------------------------------------------------------
/src/modules/Lens/LensModule.tsx:
--------------------------------------------------------------------------------
1 | import UserLensFeed from '../../modules/Lens/components/UserLensFeed';
2 | import UserLensProfile from '../../modules/Lens/components/UserLensProfile';
3 | import useLensUser from './hooks/useLensUsers';
4 |
5 | interface IProps {
6 | address: string;
7 | }
8 |
9 | function LensModule({ address }: IProps) {
10 | const { lensUser } = useLensUser(address);
11 |
12 | if (!lensUser?.id) {
13 | return null;
14 | }
15 |
16 | return (
17 |
18 |
19 | Lens Profile
20 |
21 |
22 | <>
23 |
24 |
25 |
26 |
27 |
28 |
29 | >
30 |
31 |
32 | );
33 | }
34 |
35 | export default LensModule;
36 |
--------------------------------------------------------------------------------
/src/modules/Lens/components/UserLensProfile.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { buildMediaUrl } from '../utils/ipfs';
3 | import { IlensUser } from '../utils/types';
4 | interface IProps {
5 | lensUser: IlensUser;
6 | }
7 |
8 | function UserLensProfile({ lensUser }: IProps) {
9 | return (
10 | <>
11 | {lensUser?.id && (
12 |
13 | {lensUser?.picture.original.url && (
14 |
21 | )}
22 |
{lensUser?.name}
23 |
@{lensUser?.handle}
24 |
25 |
26 |
27 | {lensUser?.bio}
28 |
29 |
30 |
38 |
39 |
40 | {lensUser?.stats.totalFollowers} Followers
41 |
42 |
43 |
44 | {lensUser?.stats.totalFollowing} Following
45 |
46 |
47 |
48 | )}
49 | >
50 | );
51 | }
52 | export default UserLensProfile;
53 |
--------------------------------------------------------------------------------
/src/modules/Lens/hooks/useLensFeed.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getLensFeedData } from '../queries/lensFeedData';
3 | import { IlensFeed } from '../utils/types';
4 |
5 | const useLensFeed = (userProfileId: string | undefined): { lensFeed: IlensFeed | undefined } => {
6 | const [lensFeed, setLensFeed] = useState();
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | if (!userProfileId) {
12 | return;
13 | }
14 | const response = await getLensFeedData(userProfileId);
15 |
16 | if (response?.data?.data?.publications.items.length > 0) {
17 | setLensFeed(response?.data?.data?.publications.items);
18 | }
19 | } catch (err: any) {
20 | // eslint-disable-next-line no-console
21 | console.error(err);
22 | }
23 | };
24 | fetchData();
25 | }, [userProfileId]);
26 |
27 | return { lensFeed };
28 | };
29 |
30 | export default useLensFeed;
31 |
--------------------------------------------------------------------------------
/src/modules/Lens/hooks/useLensUsers.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getLensProfileInfo } from '../queries/lensProfileData';
3 | import { IlensUser } from '../utils/types';
4 |
5 | const useLensUser = (address: string): { lensUser: IlensUser | undefined } => {
6 | const [lensUser, setLensUser] = useState();
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await getLensProfileInfo(address);
12 |
13 | if (response?.data?.data?.defaultProfile) {
14 | setLensUser(response.data.data.defaultProfile);
15 | }
16 | } catch (err: any) {
17 | // eslint-disable-next-line no-console
18 | console.error(err);
19 | }
20 | };
21 | fetchData();
22 | }, [address]);
23 |
24 | return { lensUser };
25 | };
26 |
27 | export default useLensUser;
28 |
--------------------------------------------------------------------------------
/src/modules/Lens/queries/lensFeedData.ts:
--------------------------------------------------------------------------------
1 | import { processLensRequest } from '../utils/graphql';
2 |
3 | /*
4 | * @doc: https://api.lens.dev/ & https://docs.lens.xyz/docs/get-publications
5 | */
6 | export const getLensFeedData = (userProfileId: string): Promise => {
7 | const query = `
8 | {
9 | publications(
10 | request: {
11 | profileId: "${userProfileId}"
12 | publicationTypes: [POST]
13 | limit: 2
14 | }
15 | ) {
16 | items {
17 | __typename
18 | ... on Post {
19 | id
20 | metadata {
21 | name
22 | description
23 | content
24 | media {
25 | original {
26 | url
27 | mimeType
28 | }
29 | }
30 | attributes {
31 | displayType
32 | traitType
33 | value
34 | }
35 | }
36 | stats {
37 | totalAmountOfMirrors
38 | totalAmountOfCollects
39 | totalAmountOfComments
40 | totalUpvotes
41 | totalDownvotes
42 | }
43 | createdAt
44 | }
45 | }
46 | }
47 | }
48 | `;
49 | return processLensRequest(query);
50 | };
51 |
--------------------------------------------------------------------------------
/src/modules/Lens/queries/lensProfileData.ts:
--------------------------------------------------------------------------------
1 | import { processLensRequest } from '../utils/graphql';
2 |
3 | /*
4 | * @doc: https://api.lens.dev/ & https://docs.lens.xyz/docs/get-default-profile
5 | */
6 | export const getLensProfileInfo = (userAddress: string): Promise => {
7 | const query = `
8 | {
9 | defaultProfile(request: { ethereumAddress: "${userAddress}"}) {
10 | id
11 | name
12 | bio
13 | handle
14 | picture {
15 | ... on MediaSet {
16 | original {
17 | url
18 | mimeType
19 | }
20 | }
21 | }
22 | stats {
23 | totalFollowers
24 | totalFollowing
25 | totalPosts
26 | totalComments
27 | totalMirrors
28 | totalPublications
29 | totalCollects
30 | }
31 | }
32 | }
33 | `;
34 | return processLensRequest(query);
35 | };
36 |
--------------------------------------------------------------------------------
/src/modules/Lens/utils/date.ts:
--------------------------------------------------------------------------------
1 | export const timeSince = (date: string) => {
2 | const comparedTo = new Date(date).getTime();
3 | const now = new Date().getTime();
4 | const seconds = Math.floor((now - comparedTo) / 1000);
5 | let interval = seconds / 31536000;
6 |
7 | if (interval > 1) {
8 | return Math.floor(interval) + ' years';
9 | }
10 | interval = seconds / 2592000;
11 | if (interval > 1) {
12 | return Math.floor(interval) + ' months';
13 | }
14 | interval = seconds / 86400;
15 | if (interval > 1) {
16 | return Math.floor(interval) + ' days';
17 | }
18 | interval = seconds / 3600;
19 | if (interval > 1) {
20 | return Math.floor(interval) + ' hours';
21 | }
22 | interval = seconds / 60;
23 | if (interval > 1) {
24 | return Math.floor(interval) + ' minutes';
25 | }
26 | return Math.floor(seconds) + ' seconds';
27 | };
28 |
--------------------------------------------------------------------------------
/src/modules/Lens/utils/graphql.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import axios from 'axios';
3 |
4 | export const processLensRequest = async (query: string): Promise => {
5 | try {
6 | return await axios.post(process.env.NEXT_PUBLIC_LENS_URL as string, { query });
7 | } catch (err) {
8 | console.error(err);
9 | return null;
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/modules/Lens/utils/ipfs.ts:
--------------------------------------------------------------------------------
1 | export const buildMediaUrl = (url: string): string => {
2 | if (url?.includes('ipfs://')) {
3 | return `https://ipfs.io/ipfs/${url?.replace('ipfs://', '') || ''}`;
4 | }
5 | return url;
6 | };
7 |
--------------------------------------------------------------------------------
/src/modules/Lens/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type IlensUser = {
2 | id: string;
3 | name: string;
4 | bio: string;
5 | handle: string;
6 | picture: ILensMedia;
7 | stats: {
8 | totalCollects: number;
9 | totalComments: number;
10 | totalFollowers: number;
11 | totalFollowing: number;
12 | totalMirrors: number;
13 | totalPosts: number;
14 | totalPublications: number;
15 | };
16 | };
17 |
18 | export type IlensFeed = ILensPublication[];
19 |
20 | export type ILensPublication = {
21 | id: any;
22 | metadata: {
23 | name: string;
24 | description: string;
25 | content: string;
26 | media: ILensMedia[];
27 | attributes: {
28 | displayType: string;
29 | traitType: string;
30 | value: string;
31 | }[];
32 | };
33 | createdAt: string;
34 | stats: {
35 | totalAmountOfMirrors: number;
36 | totalAmountOfCollects: number;
37 | totalAmountOfComments: number;
38 | totalUpvotes: number;
39 | totalDownvotes: number;
40 | };
41 | };
42 |
43 | export type ILensMedia = {
44 | original: {
45 | url: string;
46 | mimeType: string;
47 | };
48 | };
49 |
--------------------------------------------------------------------------------
/src/modules/Messaging/components/CardHeader.tsx:
--------------------------------------------------------------------------------
1 | interface ICardHeaderProps {
2 | peerAddress: string;
3 | }
4 | const CardHeader = ({ peerAddress }: ICardHeaderProps) => {
5 | return (
6 |
7 |
10 |
11 | {peerAddress && (
12 |
13 | To:
14 |
15 | {peerAddress}
16 |
17 |
18 | )}
19 |
20 |
21 | );
22 | };
23 |
24 | export default CardHeader;
25 |
--------------------------------------------------------------------------------
/src/modules/Messaging/components/ConnectButton.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import MessagingContext from '../context/messging';
3 |
4 | function ConnectButton() {
5 | const { handleRegisterToMessaging } = useContext(MessagingContext);
6 |
7 | return (
8 | handleRegisterToMessaging()}>
12 | Connect to XMTP
13 |
14 | );
15 | }
16 |
17 | export default ConnectButton;
18 |
--------------------------------------------------------------------------------
/src/modules/Messaging/components/ContactButton.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import MessagingContext from '../context/messging';
3 |
4 | function ContactButton({ userAddress, userHandle }: { userAddress: string; userHandle: string }) {
5 | const { handleMessageUser } = useContext(MessagingContext);
6 |
7 | return (
8 | {
11 | handleMessageUser(userAddress);
12 | }}>
13 | Contact {userHandle}
14 |
15 | );
16 | }
17 |
18 | export default ContactButton;
19 |
--------------------------------------------------------------------------------
/src/modules/Messaging/components/ConversationCard.tsx:
--------------------------------------------------------------------------------
1 | import { truncate } from '../utils/messaging';
2 | import useUserByAddress from '../../../hooks/useUserByAddress';
3 | import { useRouter } from 'next/router';
4 | import { formatDateConversationCard } from '../../../utils/dates';
5 | import Image from 'next/image';
6 | import { XmtpChatMessage } from '../utils/types';
7 |
8 | interface IConversationCardProps {
9 | peerAddress: string;
10 | selectedConversationPeerAddress: string;
11 | latestMessage?: XmtpChatMessage;
12 | }
13 |
14 | const ConversationCard = ({
15 | peerAddress,
16 | latestMessage,
17 | selectedConversationPeerAddress,
18 | }: IConversationCardProps) => {
19 | const user = useUserByAddress(peerAddress);
20 | const router = useRouter();
21 | const isConvSelected = peerAddress === selectedConversationPeerAddress;
22 |
23 | const handleSelectConversation = () => {
24 | router.push(`/messaging/${peerAddress}`);
25 | };
26 |
27 | return (
28 | user && (
29 | handleSelectConversation()}
31 | className={`flex py-4 px-2 justify-center items-center border-b-2 cursor-pointer ${
32 | isConvSelected ? 'bg-gray-200 ' : 'border-b-2'
33 | }`}>
34 |
35 |
42 |
43 |
44 | {user && user.handle &&
{user.handle} }
45 |
46 | {latestMessage && truncate(latestMessage.messageContent, 75)}
47 |
48 |
49 |
50 | {formatDateConversationCard(latestMessage?.timestamp as Date)}
51 |
52 |
53 |
54 |
55 | )
56 | );
57 | };
58 |
59 | export default ConversationCard;
60 |
--------------------------------------------------------------------------------
/src/modules/Messaging/components/ConversationList.tsx:
--------------------------------------------------------------------------------
1 | import { getLatestMessage } from '../utils/messaging';
2 | import ConversationCard from './ConversationCard';
3 | import Loading from '../../../components/Loading';
4 | import { XmtpChatMessage } from '../utils/types';
5 |
6 | interface IConversationListProps {
7 | conversationMessages: Map;
8 | selectedConversationPeerAddress: string;
9 | conversationsLoading: boolean;
10 | }
11 |
12 | const ConversationList = ({
13 | conversationMessages,
14 | selectedConversationPeerAddress,
15 | conversationsLoading,
16 | }: IConversationListProps) => {
17 | // Sort conversations by latest message timestamp
18 | const sortedConversations: Map = new Map(
19 | [...conversationMessages.entries()].sort((convA, convB) => {
20 | return getLatestMessage(convA[1])?.timestamp < getLatestMessage(convB[1])?.timestamp ? 1 : -1;
21 | }),
22 | );
23 |
24 | return (
25 | <>
26 | {conversationsLoading && (
27 |
28 |
29 |
30 | )}
31 | {!conversationsLoading &&
32 | Array.from(sortedConversations.keys()).map(peerAddress => {
33 | return (
34 |
44 | );
45 | })}
46 | >
47 | );
48 | };
49 |
50 | export default ConversationList;
51 |
--------------------------------------------------------------------------------
/src/modules/Messaging/components/MessageComposer.tsx:
--------------------------------------------------------------------------------
1 | import { Dispatch, SetStateAction } from 'react';
2 | import Loading from '../../../components/Loading';
3 |
4 | interface IMessageComposerProps {
5 | messageContent: string;
6 | setMessageContent: Dispatch>;
7 | sendNewMessage: () => void;
8 | sendingPending: boolean;
9 | peerUserExistsOnXMTP: boolean;
10 | peerUserExistsOnTalentLayer: boolean;
11 | }
12 |
13 | const MessageComposer = ({
14 | setMessageContent,
15 | messageContent,
16 | sendNewMessage,
17 | sendingPending,
18 | peerUserExistsOnXMTP,
19 | peerUserExistsOnTalentLayer,
20 | }: IMessageComposerProps) => {
21 | const renderSendButton = (peerUserExists: boolean, sendingPending: boolean) => {
22 | return (
23 | !sendingPending && (
24 |
28 | Send
29 |
30 | )
31 | );
32 | };
33 |
34 | return (
35 | <>
36 |
37 | setMessageContent(e.target.value)}
41 | placeholder='Write a message'
42 | disabled={!peerUserExistsOnXMTP || !peerUserExistsOnTalentLayer}
43 | value={messageContent}
44 | />
45 | {sendingPending && }
46 | {renderSendButton(peerUserExistsOnXMTP, sendingPending)}
47 |
48 | >
49 | );
50 | };
51 |
52 | export default MessageComposer;
53 |
--------------------------------------------------------------------------------
/src/modules/Messaging/components/MessageList.tsx:
--------------------------------------------------------------------------------
1 | import useStreamMessages from '../hooks/useStreamMessages';
2 | import MessageCard from './MessageCard';
3 | import { isDateOnSameDay } from '../utils/messaging';
4 | import Loading from '../../../components/Loading';
5 | import { useContext, useEffect, useRef } from 'react';
6 | import { XmtpContext } from '../context/XmtpContext';
7 | import { XmtpChatMessage } from '../utils/types';
8 |
9 | interface IMessageListProps {
10 | conversationMessages: XmtpChatMessage[];
11 | selectedConversationPeerAddress: string;
12 | peerUserId: string;
13 | userId: string;
14 | messagesLoading: boolean;
15 | sendingPending: boolean;
16 | setMessageSendingErrorMsg: React.Dispatch>;
17 | }
18 |
19 | const MessageList = ({
20 | conversationMessages,
21 | selectedConversationPeerAddress,
22 | peerUserId,
23 | userId,
24 | messagesLoading,
25 | sendingPending,
26 | setMessageSendingErrorMsg,
27 | }: IMessageListProps) => {
28 | const { providerState } = useContext(XmtpContext);
29 | //We only listen to the active selected conversation
30 | useStreamMessages(selectedConversationPeerAddress, userId, peerUserId, setMessageSendingErrorMsg);
31 | let lastMessageDate: Date | undefined;
32 | const bottomRef = useRef(null);
33 |
34 | useEffect(() => {
35 | bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
36 | }, [sendingPending, conversationMessages, bottomRef, providerState]);
37 |
38 | return (
39 |
40 | {messagesLoading &&
}
41 | {conversationMessages.map((msg, index) => {
42 | const messageCard = (
43 |
44 | {index === 0 && }
45 |
50 |
51 | );
52 | lastMessageDate = msg.timestamp as Date;
53 | return messageCard;
54 | })}
55 |
56 |
57 | );
58 | };
59 |
60 | const ConversationBeginningNotice = (): JSX.Element => (
61 |
62 |
63 | This is the beginning of the conversation
64 |
65 |
66 | );
67 |
68 | export default MessageList;
69 |
--------------------------------------------------------------------------------
/src/modules/Messaging/context/messging.tsx:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { useRouter } from 'next/router';
3 | import { createContext, ReactNode, useContext, useMemo } from 'react';
4 | import { useSigner } from 'wagmi';
5 | import TalentLayerContext from '../../../context/talentLayer';
6 | import { XmtpContext } from './XmtpContext';
7 |
8 | const MessagingContext = createContext<{
9 | userExists: () => boolean;
10 | handleRegisterToMessaging: () => Promise;
11 | handleMessageUser: (userAddress: string) => Promise;
12 | }>({
13 | userExists: () => false,
14 | handleRegisterToMessaging: () => Promise.resolve(),
15 | handleMessageUser: (userAddress: string) => Promise.resolve(),
16 | });
17 |
18 | const MessagingProvider = ({ children }: { children: ReactNode }) => {
19 | const { user } = useContext(TalentLayerContext);
20 | const { providerState } = useContext(XmtpContext);
21 | const { data: signer } = useSigner({
22 | chainId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID as string),
23 | });
24 | const router = useRouter();
25 |
26 | const userExists = (): boolean => {
27 | return providerState ? providerState.userExists : false;
28 | };
29 |
30 | const handleRegisterToMessaging = async (): Promise => {
31 | try {
32 | if (user?.address && providerState?.initClient && signer) {
33 | await providerState.initClient(signer);
34 | }
35 | } catch (e) {
36 | console.error('Error initializing XMTP client :', e);
37 | }
38 | };
39 |
40 | const handleMessageUser = async (userAddress: string): Promise => {
41 | if (signer && providerState) {
42 | //If initClient() is in the context, then we can assume that the user has not already logged in
43 | if (providerState.initClient) {
44 | try {
45 | await providerState.initClient(signer);
46 | } catch (e) {
47 | console.log('ServiceDetail - Error initializing XMTP client: ', e);
48 | return;
49 | }
50 | }
51 | const buyerAddress = ethers.utils.getAddress(userAddress);
52 | router.push(`/messaging/${buyerAddress}`);
53 | }
54 | };
55 |
56 | const value = useMemo(() => {
57 | return {
58 | userExists,
59 | handleRegisterToMessaging,
60 | handleMessageUser,
61 | };
62 | }, [userExists, handleRegisterToMessaging, handleMessageUser]);
63 |
64 | return {children} ;
65 | };
66 |
67 | export { MessagingProvider };
68 |
69 | export default MessagingContext;
70 |
--------------------------------------------------------------------------------
/src/modules/Messaging/hooks/useSendMessage.ts:
--------------------------------------------------------------------------------
1 | import { XmtpContext } from '../context/XmtpContext';
2 | import { useContext } from 'react';
3 | import { InvitationContext } from '@xmtp/xmtp-js/dist/types/src/Invitation';
4 | import useUserByAddress from '../../../hooks/useUserByAddress';
5 | import { buildConversationId } from '../utils/messaging';
6 | import { DecodedMessage } from '@xmtp/xmtp-js';
7 |
8 | const useSendMessage = (peerAddress: string, senderId: string | undefined) => {
9 | const { providerState } = useContext(XmtpContext);
10 | const peerUser = useUserByAddress(peerAddress);
11 | const { client } = providerState || {};
12 |
13 | const sendMessage = async (message: string): Promise => {
14 | if (!client || !peerAddress || !peerUser?.id || !senderId) {
15 | throw new Error('Message sending failed');
16 | }
17 |
18 | const conversationId = buildConversationId(senderId, peerUser.id);
19 |
20 | //Could add a context to define the linked job
21 | const context: InvitationContext = {
22 | conversationId: conversationId,
23 | metadata: { ['domain']: 'TalentLayer' },
24 | };
25 | const conversation = await client.conversations.newConversation(peerAddress, context);
26 |
27 | if (!conversation) throw new Error('Conversation not found');
28 | return await conversation.send(message);
29 | };
30 |
31 | return {
32 | sendMessage,
33 | };
34 | };
35 |
36 | export default useSendMessage;
37 |
--------------------------------------------------------------------------------
/src/modules/Messaging/hooks/useStreamConversations.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useContext } from 'react';
2 | import { XmtpContext } from '../context/XmtpContext';
3 | import { useSigner } from 'wagmi';
4 | import { Conversation, Stream } from '@xmtp/xmtp-js';
5 | import { buildChatMessage, CONVERSATION_PREFIX } from '../utils/messaging';
6 |
7 | const useStreamConversations = () => {
8 | const { data: signer } = useSigner({
9 | chainId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID as string),
10 | });
11 | const { providerState, setProviderState } = useContext(XmtpContext);
12 | const [stream, setStream] = useState | undefined>();
13 |
14 | useEffect(() => {
15 | if (!providerState?.conversations || !providerState?.client || !setProviderState) return;
16 |
17 | const streamConversations = async () => {
18 | const newStream = await providerState.client?.conversations.stream();
19 | if (!newStream) return;
20 | // /!\ in ex was set to 'stream' instead of 'newStream'
21 | setStream(newStream);
22 | for await (const conversation of newStream) {
23 | if (
24 | conversation.peerAddress !== (await signer?.getAddress()) &&
25 | conversation.context?.conversationId.startsWith(CONVERSATION_PREFIX)
26 | ) {
27 | //If a new conversation is detected, we get its messages
28 | const messages = await conversation.messages();
29 | const chatMessages = messages.map(msg => {
30 | return buildChatMessage(msg);
31 | });
32 | providerState.conversationMessages.set(conversation.peerAddress, chatMessages);
33 | providerState.conversations.set(conversation.peerAddress, conversation);
34 | setProviderState({
35 | ...providerState,
36 | conversationMessages: providerState.conversationMessages,
37 | conversations: providerState.conversations,
38 | });
39 | }
40 | }
41 | };
42 |
43 | streamConversations();
44 |
45 | return () => {
46 | const closeStream = async () => {
47 | if (!stream) return;
48 | await stream.return();
49 | };
50 | closeStream();
51 | };
52 | }, [providerState?.conversations]);
53 | };
54 |
55 | export default useStreamConversations;
56 |
--------------------------------------------------------------------------------
/src/modules/Messaging/utils/messaging.ts:
--------------------------------------------------------------------------------
1 | import { DecodedMessage } from '@xmtp/xmtp-js';
2 | import { ChatMessageStatus, XmtpChatMessage } from './types';
3 |
4 | export const shortAddress = (addr: string) =>
5 | addr.length > 10 && addr.startsWith('0x')
6 | ? `${addr.substring(0, 6)}...${addr.substring(addr.length - 4)}`
7 | : addr;
8 |
9 | export const truncate = (str: string, length: number) => {
10 | if (!str) {
11 | return str;
12 | }
13 | if (str.length > length) {
14 | return `${str.substring(0, length - 3)}...`;
15 | }
16 | return str;
17 | };
18 |
19 | export const getLatestMessage = (messages: XmtpChatMessage[]): XmtpChatMessage =>
20 | messages[messages.length - 1];
21 |
22 | export const CONVERSATION_PREFIX = 'talentLayer/dmV5';
23 | export const buildConversationId = (talentLayerId1: string, talentLayerId2: string) => {
24 | const profileIdAParsed = parseInt(talentLayerId1, 16);
25 | const profileIdBParsed = parseInt(talentLayerId2, 16);
26 |
27 | return profileIdAParsed < profileIdBParsed
28 | ? `${CONVERSATION_PREFIX}/${talentLayerId1}-${talentLayerId2}`
29 | : `${CONVERSATION_PREFIX}/${talentLayerId2}-${talentLayerId1}`;
30 | };
31 |
32 | export const formatDateTime = (d: Date | undefined): string =>
33 | d
34 | ? d.toLocaleTimeString(undefined, {
35 | hour12: true,
36 | hour: 'numeric',
37 | minute: '2-digit',
38 | })
39 | : '';
40 |
41 | export const isDateOnSameDay = (d1?: Date, d2?: Date): boolean => {
42 | return d1?.toDateString() === d2?.toDateString();
43 | };
44 |
45 | export const buildChatMessage = (xmtpMessage: DecodedMessage): XmtpChatMessage => {
46 | return {
47 | from: xmtpMessage.senderAddress,
48 | to: xmtpMessage.conversation.peerAddress ? xmtpMessage.conversation.peerAddress : '',
49 | messageContent: xmtpMessage.content,
50 | timestamp: xmtpMessage.sent,
51 | status: ChatMessageStatus.SENT,
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/src/modules/Messaging/utils/types.ts:
--------------------------------------------------------------------------------
1 | export enum ConversationDisplayType {
2 | CONVERSATION = 'conversations',
3 | REQUEST = 'requests',
4 | }
5 |
6 | export type XmtpChatMessage = {
7 | from: string;
8 | to: string;
9 | messageContent: string;
10 | timestamp: Date;
11 | status: ChatMessageStatus;
12 | };
13 |
14 | export enum ChatMessageStatus {
15 | PENDING = 'pending',
16 | SENT = 'sent',
17 | ERROR = 'error',
18 | }
19 |
--------------------------------------------------------------------------------
/src/modules/Poh/PohModule.tsx:
--------------------------------------------------------------------------------
1 | import UserPohProfile from './components/UserPohProfile';
2 | import usePohUser from './hooks/usePohUsers';
3 |
4 | interface IProps {
5 | address: string;
6 | }
7 |
8 | function PohModule({ address }: IProps) {
9 | const { pohUser } = usePohUser(address);
10 |
11 | if (!pohUser?.registered) {
12 | return null;
13 | }
14 |
15 | return (
16 |
17 | <>
18 |
19 |
20 |
21 | >
22 |
23 | );
24 | }
25 |
26 | export default PohModule;
27 |
--------------------------------------------------------------------------------
/src/modules/Poh/hooks/usePohUsers.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { getPohProfileInfo } from '../queries/pohProfileData';
3 | import { IPohUser } from '../utils/types';
4 |
5 | const usePohUser = (address: string): { pohUser: IPohUser | undefined } => {
6 | const [pohUser, setPohUser] = useState();
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await getPohProfileInfo(address);
12 |
13 | if (response?.data?.data?.submission) {
14 | setPohUser(response.data.data.submission);
15 | }
16 | } catch (err: any) {
17 | // eslint-disable-next-line no-console
18 | console.error(err);
19 | }
20 | };
21 | fetchData();
22 | }, [address]);
23 |
24 | return { pohUser };
25 | };
26 |
27 | export default usePohUser;
28 |
--------------------------------------------------------------------------------
/src/modules/Poh/queries/pohProfileData.ts:
--------------------------------------------------------------------------------
1 | import { processPohRequest } from '../utils/graphql';
2 |
3 | /*
4 | * @doc: https://api.thegraph.com/subgraphs/name/kleros/proof-of-humanity-mainnet || https://api.poh.dev/docs/static/index.html#/profiles/get_profiles
5 | */
6 | export const getPohProfileInfo = (userAddress: string): Promise => {
7 | const query = `
8 | {
9 | submission(id: "${userAddress}") {
10 | id
11 | creationTime
12 | registered
13 | }
14 | }
15 | `;
16 | return processPohRequest(query);
17 | };
18 |
--------------------------------------------------------------------------------
/src/modules/Poh/utils/graphql.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import axios from 'axios';
3 |
4 | export const processPohRequest = async (query: string): Promise => {
5 | try {
6 | return await axios.post(process.env.NEXT_PUBLIC_POH_SUBGRAPH_URL as string, { query });
7 | } catch (err) {
8 | console.error(err);
9 | return null;
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/modules/Poh/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type IPohUser = {
2 | id: string;
3 | creationTime: string;
4 | registered: boolean;
5 | };
6 |
--------------------------------------------------------------------------------
/src/modules/Sismo/components/SismoBadgeCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { ISismoBadge } from '../utils/types';
3 | import SismoHelpPopover from './SismoHelpPopover';
4 |
5 | function SismoBadgeCard({ sismoBadgeData }: { sismoBadgeData: ISismoBadge }) {
6 | return (
7 |
8 |
9 | {sismoBadgeData.name}
10 |
11 | Description: {sismoBadgeData.description}
12 |
13 |
14 |
21 |
22 | );
23 | }
24 |
25 | export default SismoBadgeCard;
26 |
--------------------------------------------------------------------------------
/src/modules/Sismo/components/SismoGroupCard.tsx:
--------------------------------------------------------------------------------
1 | import { ISismoGroup } from '../utils/types';
2 | import { useContext } from 'react';
3 | import TalentLayerContext from '../../../context/talentLayer';
4 | import SismoHelpPopover from './SismoHelpPopover';
5 | import Image from 'next/image';
6 |
7 | function SismoGroupCard({
8 | sismoGroupData,
9 | userAddrss,
10 | }: {
11 | sismoGroupData: ISismoGroup;
12 | userAddrss: string;
13 | }) {
14 | const { user } = useContext(TalentLayerContext);
15 | const isConnectedUser = () => {
16 | return user?.address === userAddrss;
17 | };
18 |
19 | return (
20 |
21 |
22 |
23 |
24 |
31 |
32 |
{sismoGroupData.name}
33 |
34 |
35 |
36 | How to get this Badge ?
37 |
38 |
39 | Description: {sismoGroupData.description}
40 |
41 |
42 | Specs: {sismoGroupData.specs}
43 |
44 |
45 |
46 |
47 |
48 | {isConnectedUser() && (
49 |
70 | )}
71 |
72 |
73 | );
74 | }
75 |
76 | export default SismoGroupCard;
77 |
--------------------------------------------------------------------------------
/src/modules/Sismo/components/SismoHelpPopover.tsx:
--------------------------------------------------------------------------------
1 | import { QuestionMarkCircle } from 'heroicons-react';
2 | import { useState } from 'react';
3 |
4 | function HelpPopover(props: { children: React.ReactNode }) {
5 | const [showHelp, setShowHelp] = useState(false);
6 |
7 | return (
8 |
9 |
10 | {
13 | setShowHelp(!showHelp);
14 | e.preventDefault();
15 | }}>
16 |
17 |
18 |
19 |
23 |
{props.children}
24 |
25 |
26 | );
27 | }
28 |
29 | export default HelpPopover;
30 |
--------------------------------------------------------------------------------
/src/modules/Sismo/components/UserBadges.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import TalentLayerContext from '../../../context/talentLayer';
3 | import { IUser } from '../../../types';
4 | import useSismoBadgesPerAddress from '../hooks/useSismoBadgesPerAddress';
5 | import { TALENTLAYER_GROUPS } from '../utils/sismoGroupsData';
6 | import { ISismoBadge, ISismoGroup } from '../utils/types';
7 | import SismoBadgeCard from './SismoBadgeCard';
8 | import SismoGroupCard from './SismoGroupCard';
9 |
10 | interface IProps {
11 | user: IUser;
12 | }
13 |
14 | function UserBadges({ user }: IProps) {
15 | const { user: currentUser } = useContext(TalentLayerContext);
16 | const sismoBadges = useSismoBadgesPerAddress(user.address);
17 |
18 | const groupsData: ISismoGroup[] = [...TALENTLAYER_GROUPS];
19 |
20 | // TODO: clean that - NOT clean to have conditional use
21 | // if (user.address === currentUser?.address) {
22 | // groupsData.map(group => {
23 | // group.userInGroup = useIsUserInSismoGroup(group.id, user.address);
24 | // });
25 | // }
26 |
27 | return (
28 | <>
29 | {sismoBadges && sismoBadges.length > 0 && (
30 | <>
31 |
32 | {user.address === currentUser?.address ? 'Your badges' : 'Badges'}:
33 |
34 |
35 | {sismoBadges.map((badge: ISismoBadge, i: number) => {
36 | return ;
37 | })}
38 |
39 | >
40 | )}
41 |
42 | {user.address === currentUser?.address && groupsData.length > 0 && (
43 | <>
44 |
45 | All zkPOW badges:
46 |
47 |
48 | {groupsData.map((groupData: ISismoGroup, i: number) => {
49 | return (
50 |
51 | );
52 | })}
53 |
54 | >
55 | )}
56 | >
57 | );
58 | }
59 |
60 | export default UserBadges;
61 |
--------------------------------------------------------------------------------
/src/modules/Sismo/hooks/useIsUserInSismoGroup.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getSismoGroupSnapshotUrl } from '../queries/sismo';
3 | import { callUrl } from '../utils/rest';
4 |
5 | const useIsUserInSismoGroup = (sismoGroupId: string, userAddress: string): boolean => {
6 | const [userInGroup, setUserInGroup] = useState(false);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await getSismoGroupSnapshotUrl(sismoGroupId);
12 | if (response?.data?.data?.groups?.length > 0) {
13 | const groupUsers = await callUrl(response.data.data.groups[0].latestSnapshot.dataUrl);
14 | const userInGroup = groupUsers.data[userAddress] == 1;
15 | setUserInGroup(userInGroup);
16 | }
17 | } catch (err: any) {
18 | // eslint-disable-next-line no-console
19 | console.error(err);
20 | }
21 | };
22 | fetchData();
23 | }, []);
24 |
25 | return userInGroup;
26 | };
27 |
28 | export default useIsUserInSismoGroup;
29 |
--------------------------------------------------------------------------------
/src/modules/Sismo/hooks/useSismoBadgesPerAddress.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { getSismoBadgesPerAddress } from '../queries/sismo';
3 | import { ISismoBadge } from '../utils/types';
4 |
5 | const useSismoBadgesPerAddress = (userAddress: string): ISismoBadge[] | null => {
6 | const [badgesData, setBadgesData] = useState(null);
7 |
8 | useEffect(() => {
9 | const fetchData = async () => {
10 | try {
11 | const response = await getSismoBadgesPerAddress(userAddress);
12 | const allBadges: ISismoBadge[] = [];
13 | if (response?.data?.data?.accounts[0]?.mintedBadges.length > 0) {
14 | const badges = response.data.data.accounts[0]?.mintedBadges;
15 | badges.forEach((badge: any) => {
16 | allBadges.push({
17 | name: badge.badge.name,
18 | image: badge.badge.image,
19 | description: badge.badge.description,
20 | });
21 | });
22 | setBadgesData(allBadges);
23 | }
24 | } catch (err: any) {
25 | // eslint-disable-next-line no-console
26 | console.error(err);
27 | }
28 | };
29 | fetchData();
30 | }, []);
31 |
32 | return badgesData;
33 | };
34 |
35 | export default useSismoBadgesPerAddress;
36 |
--------------------------------------------------------------------------------
/src/modules/Sismo/hooks/useSismoGroupData.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { ISismoGroup } from '../utils/types';
3 | import { getSismoGroupSnapshot } from '../queries/sismo';
4 | import { callUrl } from '../utils/rest';
5 |
6 | const useSismoGroupData = (sismoGroupId: string, userAddress: string): ISismoGroup | null => {
7 | const [groupData, setGroupData] = useState(null);
8 |
9 | useEffect(() => {
10 | const fetchData = async () => {
11 | try {
12 | const response = await getSismoGroupSnapshot(sismoGroupId);
13 | if (response?.data?.data?.groups?.length > 0) {
14 | const groupUsers = await callUrl(response.data.data.groups[0].latestSnapshot.dataUrl);
15 | const userInGroup = groupUsers.data[userAddress] == 1;
16 | setGroupData({
17 | description: response.data.data.groups[0].description,
18 | id: response.data.data.groups[0].id,
19 | name: response.data.data.groups[0].name,
20 | specs: response.data.data.groups[0].specs,
21 | image: '',
22 | link: '',
23 | userInGroup,
24 | });
25 | }
26 | } catch (err: any) {
27 | // eslint-disable-next-line no-console
28 | console.error(err);
29 | }
30 | };
31 | fetchData();
32 | }, []);
33 |
34 | return groupData;
35 | };
36 |
37 | export default useSismoGroupData;
38 |
--------------------------------------------------------------------------------
/src/modules/Sismo/queries/sismo.ts:
--------------------------------------------------------------------------------
1 | import { processSismoRequest } from '../utils/graphql';
2 |
3 | export const getSismoGroupSnapshot = async (groupId: string): Promise => {
4 | let condition = ', where: {';
5 | condition += groupId ? `, id: "${groupId}"` : '';
6 | condition += '}';
7 |
8 | const query = `
9 | {
10 | groups(${condition}) {
11 | description
12 | id
13 | latestSnapshot {
14 | dataUrl
15 | }
16 | name
17 | specs
18 | }
19 | }
20 | `;
21 |
22 | return processSismoRequest(query);
23 | };
24 | export const getSismoGroupSnapshotUrl = async (groupId: string): Promise => {
25 | let condition = ', where: {';
26 | condition += groupId ? `, id: "${groupId}"` : '';
27 | condition += '}';
28 |
29 | const query = `
30 | {
31 | groups(${condition}) {
32 | latestSnapshot {
33 | dataUrl
34 | }
35 | }
36 | }
37 | `;
38 |
39 | return processSismoRequest(query);
40 | };
41 |
42 | export const getSismoBadgesPerAddress = async (address: string): Promise => {
43 | let condition = ', where: {';
44 | condition += address ? `, id: "${address}"` : '';
45 | condition += '}';
46 |
47 | const query = `
48 | {
49 | accounts(${condition}) {
50 | mintedBadges {
51 | badge {
52 | name
53 | image
54 | description,
55 | }
56 | }
57 | }
58 | }
59 | `;
60 |
61 | return processSismoRequest(query);
62 | };
63 |
--------------------------------------------------------------------------------
/src/modules/Sismo/utils/graphql.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import axios from 'axios';
3 |
4 | export const processSismoRequest = async (query: string): Promise => {
5 | try {
6 | return await axios.post(process.env.NEXT_PUBLIC_SISMO_GRAPH_API as string, { query });
7 | } catch (err) {
8 | console.error(err);
9 | return null;
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/modules/Sismo/utils/rest.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export const callUrl = async (url: string): Promise => {
4 | try {
5 | return await axios.get(url);
6 | } catch (err) {
7 | console.error(err);
8 | return null;
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/modules/Sismo/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type ISismoGroup = {
2 | description: string;
3 | id: string;
4 | userInGroup: boolean;
5 | name: string;
6 | specs: string;
7 | image: string;
8 | link: string;
9 | };
10 |
11 | export type ISismoBadge = {
12 | name: string;
13 | image: string;
14 | description: string;
15 | };
16 |
--------------------------------------------------------------------------------
/src/modules/WorkX/hooks/use-workx-skills.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback, Dispatch, SetStateAction } from 'react';
2 | import { Skill } from '../utils/types';
3 | import axios from 'axios';
4 |
5 | export const MINIMUM_QUERY_LENGTH = 2;
6 |
7 | export const useWorkxSkills = (): {
8 | skills: Skill[];
9 | query: string;
10 | fetchData: (name: string) => void;
11 | setQuery: Dispatch>;
12 | } => {
13 | const [query, setQuery] = useState('');
14 | const [skills, setSkills] = useState([]);
15 |
16 | const fetchData = useCallback(async (query: string) => {
17 | try {
18 | if (query.length < MINIMUM_QUERY_LENGTH) return;
19 | const response = await axios.get('/api/get-skills', { params: { name: query } });
20 | if (response?.data?.skills?.length > 0) {
21 | setSkills(response.data.skills);
22 | return;
23 | }
24 |
25 | setSkills([]);
26 | } catch (err: any) {
27 | // eslint-disable-next-line no-console
28 | console.error(err);
29 | }
30 | }, []);
31 |
32 | useEffect(() => {
33 | fetchData(query);
34 | }, [fetchData]);
35 |
36 | return { skills: skills, fetchData, query, setQuery };
37 | };
38 |
--------------------------------------------------------------------------------
/src/modules/WorkX/queries/global.ts:
--------------------------------------------------------------------------------
1 | import { processWorkXRequest } from '../utils/graphql';
2 |
3 | export const getWorkXSkills = (name: string): Promise => {
4 | const query = `
5 | {
6 | indicatorFind(
7 | query:{
8 | name: "${name}"
9 | indicatorCategory: SKILL
10 | paging: {
11 | start: 0,
12 | limit: 10
13 | }
14 | }
15 | ) {
16 | name
17 | }
18 | }
19 | `;
20 | return processWorkXRequest(query);
21 | };
22 |
--------------------------------------------------------------------------------
/src/modules/WorkX/utils/graphql.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export const processWorkXRequest = async (query: string): Promise => {
4 | try {
5 | return await axios.post(process.env.NEXT_PUBLIC_WORKX_API_URL as string, { query });
6 | } catch (err) {
7 | console.error(err);
8 | return null;
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/src/modules/WorkX/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type Skill = {
2 | name: string;
3 | };
4 |
--------------------------------------------------------------------------------
/src/pages/About.tsx:
--------------------------------------------------------------------------------
1 | function About() {
2 | return (
3 |
4 |
5 | About TalentLayer
6 |
7 |
8 | );
9 | }
10 |
11 | export default About;
12 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { EthereumClient, modalConnectors } from '@web3modal/ethereum';
2 | import { Web3Modal } from '@web3modal/react';
3 | import { DefaultSeo } from 'next-seo';
4 | import { ThemeProvider } from 'next-themes';
5 | import type { AppProps } from 'next/app';
6 | import { GoogleAnalytics } from 'nextjs-google-analytics';
7 | import { ToastContainer } from 'react-toastify';
8 | import { Chain, WagmiConfig, configureChains, createClient } from 'wagmi';
9 | import { jsonRpcProvider } from 'wagmi/providers/jsonRpc';
10 | import { customChains } from '../chains';
11 | import { TalentLayerProvider } from '../context/talentLayer';
12 | import { MessagingProvider } from '../modules/Messaging/context/messging';
13 | import { XmtpContextProvider } from '../modules/Messaging/context/XmtpContext';
14 | import '../styles/globals.css';
15 | import 'react-toastify/dist/ReactToastify.css';
16 | import Layout from './Layout';
17 | import { useEffect } from 'react';
18 |
19 | const chains: Chain[] = [customChains.polygonMumbai];
20 |
21 | // Wagmi client
22 | const { provider } = configureChains(chains, [
23 | jsonRpcProvider({
24 | rpc: chain => {
25 | return { http: chain.rpcUrls.default };
26 | },
27 | }),
28 | ]);
29 | const wagmiClient = createClient({
30 | autoConnect: false,
31 | connectors: modalConnectors({ appName: 'web3Modal', chains }),
32 | provider,
33 | });
34 |
35 | // Web3Modal Ethereum Client
36 | const ethereumClient = new EthereumClient(wagmiClient, chains);
37 |
38 | function MyApp({ Component, pageProps }: AppProps) {
39 | useEffect(() => {
40 | wagmiClient.autoConnect();
41 | }, []);
42 |
43 | return (
44 | <>
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
64 |
65 | >
66 | );
67 | }
68 |
69 | export default MyApp;
70 |
--------------------------------------------------------------------------------
/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from 'next/document';
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/pages/api/delegate/create-service.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { Contract } from 'ethers';
3 | import { config } from '../../../config';
4 | import TalentLayerService from '../../../contracts/ABI/TalentLayerService.json';
5 | import { getServiceSignature } from '../../../utils/signature';
6 | import { getDelegationSigner, isPlatformAllowedToDelegate } from '../utils/delegate';
7 |
8 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
9 | const { userId, userAddress, cid } = req.body;
10 |
11 | // @dev : you can add here all the checks you need to confirm the delegation for a user
12 | await isPlatformAllowedToDelegate(userAddress, res);
13 |
14 | try {
15 | const signer = await getDelegationSigner(res);
16 | if (!signer) {
17 | return;
18 | }
19 |
20 | const signature = await getServiceSignature({ profileId: Number(userId), cid });
21 | const serviceRegistryContract = new Contract(
22 | config.contracts.serviceRegistry,
23 | TalentLayerService.abi,
24 | signer,
25 | );
26 |
27 | const transaction = await serviceRegistryContract.createService(
28 | userId,
29 | process.env.NEXT_PUBLIC_PLATFORM_ID,
30 | cid,
31 | signature,
32 | );
33 |
34 | res.status(200).json({ transaction: transaction });
35 | } catch (error) {
36 | console.log('errorDebug', error);
37 | res.status(500).json('tx failed');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/pages/api/delegate/create-update-proposal.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { Contract } from 'ethers';
3 | import { config } from '../../../config';
4 | import TalentLayerService from '../../../contracts/ABI/TalentLayerService.json';
5 | import { getProposalSignature } from '../../../utils/signature';
6 | import { getDelegationSigner, isPlatformAllowedToDelegate } from '../utils/delegate';
7 |
8 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
9 | const {
10 | userId,
11 | userAddress,
12 | serviceId,
13 | valuesRateToken,
14 | parsedRateAmountString,
15 | cid,
16 | convertExpirationDateString,
17 | existingProposalStatus,
18 | } = req.body;
19 |
20 | // @dev : you can add here all the checks you need to confirm the delegation for a user
21 | await isPlatformAllowedToDelegate(userAddress, res);
22 |
23 | try {
24 | const signer = await getDelegationSigner(res);
25 |
26 | if (!signer) {
27 | return;
28 | }
29 |
30 | const serviceRegistryContract = new Contract(
31 | config.contracts.serviceRegistry,
32 | TalentLayerService.abi,
33 | signer,
34 | );
35 |
36 | let transaction;
37 |
38 | if (existingProposalStatus) {
39 | transaction = await serviceRegistryContract.updateProposal(
40 | userId,
41 | serviceId,
42 | valuesRateToken,
43 | parsedRateAmountString,
44 | cid,
45 | convertExpirationDateString,
46 | );
47 | } else {
48 | const signature = await getProposalSignature({
49 | profileId: Number(userId),
50 | cid,
51 | serviceId: Number(serviceId),
52 | });
53 |
54 | transaction = await serviceRegistryContract.createProposal(
55 | userId,
56 | serviceId,
57 | valuesRateToken,
58 | parsedRateAmountString,
59 | process.env.NEXT_PUBLIC_PLATFORM_ID,
60 | cid,
61 | convertExpirationDateString,
62 | signature,
63 | );
64 | }
65 |
66 | res.status(200).json({ transaction: transaction });
67 | } catch (error) {
68 | console.log('errorDebug', error);
69 | res.status(500).json('tx failed');
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/pages/api/delegate/mint-id.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { Contract } from 'ethers';
3 | import { config } from '../../../config';
4 | import TalentLayerID from '../../../contracts/ABI/TalentLayerID.json';
5 | import { getDelegationSigner } from '../utils/delegate';
6 |
7 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
8 | const { handle, handlePrice, userAddress } = req.body;
9 |
10 | // @dev : you can add here all the checks you need to confirm the delegation for a user
11 |
12 | try {
13 | if (process.env.NEXT_PUBLIC_ACTIVE_DELEGATE_MINT !== 'true') {
14 | res.status(500).json('Delegation is not activated');
15 | return null;
16 | }
17 |
18 | const signer = await getDelegationSigner(res);
19 |
20 | if (!signer) {
21 | return;
22 | }
23 |
24 | const talentLayerID = new Contract(config.contracts.talentLayerId, TalentLayerID.abi, signer);
25 | const transaction = await talentLayerID.mintForAddress(
26 | userAddress,
27 | process.env.NEXT_PUBLIC_PLATFORM_ID,
28 | handle,
29 | {
30 | value: handlePrice,
31 | },
32 | );
33 |
34 | res.status(200).json({ transaction: transaction });
35 | } catch (error) {
36 | console.log('errorDebug', error);
37 | res.status(500).json({ error: error });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/pages/api/delegate/mint-review.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { Contract } from 'ethers';
3 | import { config } from '../../../config';
4 | import TalentLayerReview from '../../../contracts/ABI/TalentLayerReview.json';
5 | import { getDelegationSigner, isPlatformAllowedToDelegate } from '../utils/delegate';
6 |
7 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
8 | const { userId, userAddress, serviceId, uri, valuesRating } = req.body;
9 |
10 | // @dev : you can add here all the checks you need to confirm the delegation for a user
11 | await isPlatformAllowedToDelegate(userAddress, res);
12 |
13 | try {
14 | const signer = await getDelegationSigner(res);
15 |
16 | if (!signer) {
17 | return;
18 | }
19 |
20 | const talentLayerReview = new Contract(
21 | config.contracts.talentLayerReview,
22 | TalentLayerReview.abi,
23 | signer,
24 | );
25 | const transaction = await talentLayerReview.mint(userId, serviceId, uri, valuesRating);
26 |
27 | res.status(200).json({ transaction: transaction });
28 | } catch (error) {
29 | console.log('errorDebug', error);
30 | res.status(500).json('tx failed');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/api/delegate/release-reimburse.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { Contract } from 'ethers';
3 | import { config } from '../../../config';
4 | import TalentLayerEscrow from '../../../contracts/ABI/TalentLayerEscrow.json';
5 | import { getDelegationSigner, isPlatformAllowedToDelegate } from '../utils/delegate';
6 |
7 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
8 | const { userAddress, profileId, transactionId, amount, isBuyer } = req.body;
9 |
10 | // @dev : you can add here all the checks you need to confirm the delegation for a user
11 |
12 | await isPlatformAllowedToDelegate(userAddress, res);
13 |
14 | try {
15 | const signer = await getDelegationSigner(res);
16 |
17 | if (!signer) {
18 | return;
19 | }
20 |
21 | const talentLayerEscrow = new Contract(
22 | config.contracts.talentLayerEscrow,
23 | TalentLayerEscrow.abi,
24 | signer,
25 | );
26 |
27 | let transaction;
28 | if (isBuyer) {
29 | transaction = await talentLayerEscrow.release(profileId, transactionId, amount);
30 | } else {
31 | transaction = await talentLayerEscrow.reimburse(profileId, transactionId, amount);
32 | }
33 |
34 | res.status(200).json({ transaction: transaction });
35 | } catch (error) {
36 | console.log('errorDebug', error);
37 | res.status(500).json({ error: error });
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/pages/api/delegate/update-profile-data.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { Contract } from 'ethers';
3 | import { config } from '../../../config';
4 | import TalentLayerID from '../../../contracts/ABI/TalentLayerID.json';
5 | import { getDelegationSigner, isPlatformAllowedToDelegate } from '../utils/delegate';
6 |
7 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
8 | const { userId, userAddress, cid } = req.body;
9 |
10 | // @dev : you can add here all the checks you need to confirm the delegation for a user
11 |
12 | await isPlatformAllowedToDelegate(userAddress, res);
13 |
14 | try {
15 | const signer = await getDelegationSigner(res);
16 |
17 | if (!signer) {
18 | return;
19 | }
20 |
21 | const talentLayerID = new Contract(config.contracts.talentLayerId, TalentLayerID.abi, signer);
22 | const transaction = await talentLayerID.updateProfileData(userId, cid);
23 |
24 | res.status(200).json({ transaction: transaction });
25 | } catch (error) {
26 | console.log('errorDebug', error);
27 | res.status(500).json({ error: error });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/pages/api/delegate/update-service.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next';
2 | import { Contract } from 'ethers';
3 | import { config } from '../../../config';
4 | import TalentLayerService from '../../../contracts/ABI/TalentLayerService.json';
5 | import { getDelegationSigner, isPlatformAllowedToDelegate } from '../utils/delegate';
6 |
7 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
8 | const { userId, userAddress, serviceId, cid } = req.body;
9 |
10 | // @dev : you can add here all the checks you need to confirm the delegation for a user
11 | await isPlatformAllowedToDelegate(userAddress, res);
12 |
13 | try {
14 | const signer = await getDelegationSigner(res);
15 | if (!signer) {
16 | return;
17 | }
18 |
19 | const serviceRegistryContract = new Contract(
20 | config.contracts.serviceRegistry,
21 | TalentLayerService.abi,
22 | signer,
23 | );
24 |
25 | const transaction = await serviceRegistryContract.updateServiceData(userId, serviceId, cid);
26 |
27 | res.status(200).json({ transaction: transaction });
28 | } catch (error) {
29 | console.log('errorDebug', error);
30 | res.status(500).json('tx failed');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/pages/api/get-skills.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | import { getWorkXSkills } from '../../modules/WorkX/queries/global';
3 |
4 | export default async function getSkills(req: NextApiRequest, res: NextApiResponse) {
5 | const query = req.query;
6 | const { name } = query;
7 | const response = await getWorkXSkills(name as string);
8 | const skills = response?.data?.data?.indicatorFind;
9 | res.status(200).json({ skills });
10 | }
11 |
--------------------------------------------------------------------------------
/src/pages/api/services/filter.json:
--------------------------------------------------------------------------------
1 | {
2 | "keywords": ["blockchain", "solidity", "peer"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/pages/api/services/filtered.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | import { getServices } from '../../../queries/services';
3 | import { ServiceStatusEnum } from '../../../types';
4 |
5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
6 | const query = req.query;
7 |
8 | // @dev : here you can add optional additional filters, example in ./filter.json
9 | const keywordList: string[] = [];
10 |
11 | const serviceStatus = query.serviceStatus as ServiceStatusEnum;
12 | const buyerId = query.buyerId as string;
13 | const sellerId = query.sellerId as string;
14 | const numberPerPage = Number(query.numberPerPage);
15 | const offset = Number(query.offset);
16 | const searchQuery = query.searchQuery as string;
17 |
18 | try {
19 | const response = await getServices({
20 | serviceStatus,
21 | buyerId,
22 | sellerId,
23 | numberPerPage,
24 | offset,
25 | keywordList,
26 | searchQuery,
27 | });
28 |
29 | const filteredServices = response?.data?.data?.services;
30 |
31 | res.status(200).json({ services: filteredServices });
32 | } catch (error) {
33 | res.status(500).json({ error: error });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/api/services/request.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { ServiceStatusEnum } from '../../../types';
3 |
4 | export const getFilteredServicesByKeywords = async (
5 | serviceStatus?: ServiceStatusEnum,
6 | buyerId?: string,
7 | sellerId?: string,
8 | numberPerPage?: number,
9 | offset?: number,
10 | searchQuery?: string,
11 | ): Promise => {
12 | try {
13 | return await axios.get('/api/services/filtered', {
14 | params: {
15 | serviceStatus,
16 | buyerId,
17 | sellerId,
18 | numberPerPage,
19 | offset,
20 | searchQuery,
21 | },
22 | });
23 | } catch (err) {
24 | console.error(err);
25 | throw err;
26 | }
27 | };
28 |
--------------------------------------------------------------------------------
/src/pages/api/utils/delegate.ts:
--------------------------------------------------------------------------------
1 | import { NextApiResponse } from 'next';
2 | import { ethers, Wallet } from 'ethers';
3 | import { getUserByAddress } from '../../../queries/users';
4 |
5 | export async function isPlatformAllowedToDelegate(
6 | userAddress: string,
7 | res: NextApiResponse,
8 | ): Promise {
9 | const getUser = await getUserByAddress(userAddress);
10 | const delegateAddresses = getUser.data?.data?.users[0].delegates;
11 |
12 | if (
13 | delegateAddresses.indexOf(
14 | (process.env.NEXT_PUBLIC_DELEGATE_ADDRESS as string).toLowerCase(),
15 | ) === -1
16 | ) {
17 | res.status(500).json('Delegation is not activated');
18 | return false;
19 | }
20 |
21 | return true;
22 | }
23 |
24 | export async function getDelegationSigner(res: NextApiResponse): Promise {
25 | const provider = new ethers.providers.JsonRpcProvider(process.env.NEXT_PUBLIC_BACKEND_RPC_URL);
26 | const delegateSeedPhrase = process.env.NEXT_PRIVATE_DELEGATE_SEED_PHRASE;
27 |
28 | if (!delegateSeedPhrase) {
29 | res.status(500).json('Delegate seed phrase is not set');
30 | return null;
31 | }
32 |
33 | const signer = Wallet.fromMnemonic(delegateSeedPhrase).connect(provider);
34 |
35 | return signer;
36 | }
37 |
--------------------------------------------------------------------------------
/src/pages/dashboard/incomes.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import Back from '../../components/Back';
3 | import Loading from '../../components/Loading';
4 | import UserIncomes from '../../components/UserIncomes';
5 | import TalentLayerContext from '../../context/talentLayer';
6 |
7 | function Incomes() {
8 | const { user } = useContext(TalentLayerContext);
9 |
10 | if (!user) {
11 | return ;
12 | }
13 |
14 | return (
15 |
16 |
17 |
18 | Your incomes summary : {user?.handle}
19 |
20 |
23 |
24 | );
25 | }
26 |
27 | export default Incomes;
28 |
--------------------------------------------------------------------------------
/src/pages/dashboard/index.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import Steps from '../../components/Steps';
3 | import UserBadges from '../../modules/Sismo/components/UserBadges';
4 | import UserDetail from '../../components/UserDetail';
5 | import UserGains from '../../components/UserGains';
6 | import UserPayments from '../../components/UserPayments';
7 | import UserProposals from '../../components/UserProposals';
8 | import UserServices from '../../components/UserServices';
9 | import TalentLayerContext from '../../context/talentLayer';
10 |
11 | function Dashboard() {
12 | const { account, user } = useContext(TalentLayerContext);
13 |
14 | return (
15 |
16 |
17 | Your dashboard
18 |
19 |
20 |
21 |
22 | {account?.isConnected && user && (
23 |
24 |
25 |
26 | Your profile
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | )}
50 |
51 | );
52 | }
53 |
54 | export default Dashboard;
55 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import CreateId from '../components/Home/CreateId';
2 | import SearchService from '../components/Home/SearchService';
3 | import SearchTalent from '../components/Home/SearchTalent';
4 |
5 | function Home() {
6 | return (
7 | <>
8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
15 | export default Home;
16 |
--------------------------------------------------------------------------------
/src/pages/messaging/[address].tsx:
--------------------------------------------------------------------------------
1 | import Dashboard from '../../modules/Messaging/components/Dashboard';
2 |
3 | function MessagingAddress() {
4 | return ;
5 | }
6 |
7 | export default MessagingAddress;
8 |
--------------------------------------------------------------------------------
/src/pages/messaging/index.tsx:
--------------------------------------------------------------------------------
1 | import Dashboard from '../../modules/Messaging/components/Dashboard';
2 |
3 | function MessagingIndex() {
4 | return ;
5 | }
6 |
7 | export default MessagingIndex;
8 |
--------------------------------------------------------------------------------
/src/pages/profile/[id].tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import Back from '../../components/Back';
3 | import Loading from '../../components/Loading';
4 | import UserDetail from '../../components/UserDetail';
5 | import UserServices from '../../components/UserServices';
6 | import useUserById from '../../hooks/useUserById';
7 | import LensModule from '../../modules/Lens/LensModule';
8 | import UserBadges from '../../modules/Sismo/components/UserBadges';
9 |
10 | function Profile() {
11 | const router = useRouter();
12 | const { id } = router.query;
13 | const user = useUserById(id as string);
14 |
15 | if (!user) {
16 | return ;
17 | }
18 |
19 | return (
20 |
21 |
22 | {user && (
23 | <>
24 |
25 | Profile {user.handle}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | >
45 | )}
46 |
47 | );
48 | }
49 |
50 | export default Profile;
51 |
--------------------------------------------------------------------------------
/src/pages/profile/edit.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import ProfileForm from '../../components/Form/ProfileForm';
3 | import Steps from '../../components/Steps';
4 | import TalentLayerContext from '../../context/talentLayer';
5 |
6 | function EditProfile() {
7 | const { account, user } = useContext(TalentLayerContext);
8 |
9 | return (
10 |
11 |
12 | Edit your Profile
13 |
14 |
15 |
16 |
17 | {account?.isConnected && user &&
}
18 |
19 | );
20 | }
21 |
22 | export default EditProfile;
23 |
--------------------------------------------------------------------------------
/src/pages/services/[id].tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import Back from '../../components/Back';
3 | import Loading from '../../components/Loading';
4 | import ServiceDetail from '../../components/ServiceDetail';
5 | import useServiceById from '../../hooks/useServiceById';
6 |
7 | function Service() {
8 | const router = useRouter();
9 | const { id } = router.query;
10 | const service = useServiceById(id as string);
11 |
12 | return (
13 |
14 |
15 |
16 | Job #{id}
17 |
18 | {service ?
:
}
19 |
20 | );
21 | }
22 |
23 | export default Service;
24 |
--------------------------------------------------------------------------------
/src/pages/services/create.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import ServiceForm from '../../components/Form/ServiceForm';
3 | import Steps from '../../components/Steps';
4 | import TalentLayerContext from '../../context/talentLayer';
5 | import ConnectButton from '../../modules/Messaging/components/ConnectButton';
6 | import MessagingContext from '../../modules/Messaging/context/messging';
7 |
8 | function CreateService() {
9 | const { account, user } = useContext(TalentLayerContext);
10 | const { userExists } = useContext(MessagingContext);
11 |
12 | return (
13 |
14 |
15 | Post a job
16 |
17 |
18 |
19 |
20 | {!userExists() && account?.isConnected && user && (
21 |
22 |
23 | In order to create a service, you need to be registered to our decentralized messaging
24 | service Please sign in to our messaging service to verify your identity
25 |
26 |
27 |
28 | )}
29 |
30 | {account?.isConnected && user && userExists() &&
}
31 |
32 | );
33 | }
34 |
35 | export default CreateService;
36 |
--------------------------------------------------------------------------------
/src/pages/services/edit/[id].tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import Steps from '../../../components/Steps';
3 | import ServiceForm from '../../../components/Form/ServiceForm';
4 | import TalentLayerContext from '../../../context/talentLayer';
5 | import { useRouter } from 'next/router';
6 |
7 | function EditWService() {
8 | const { account, user } = useContext(TalentLayerContext);
9 | const router = useRouter();
10 | const { id } = router.query;
11 |
12 | return (
13 |
14 |
15 | Edit your Service
16 |
17 |
18 |
19 |
20 | {account?.isConnected && user &&
}
21 |
22 | );
23 | }
24 |
25 | export default EditWService;
26 |
--------------------------------------------------------------------------------
/src/pages/talents.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import SearchTalentButton from '../components/Form/SearchTalentButton';
3 | import Loading from '../components/Loading';
4 | import UserItem from '../components/UserItem';
5 | import useUsers from '../hooks/useUsers';
6 |
7 | function Talents() {
8 | const PAGE_SIZE = 36;
9 | const router = useRouter();
10 | const query = router.query;
11 | const searchQuery = query.search as string;
12 | const { users, hasMoreData, loading, loadMore } = useUsers(
13 | searchQuery?.toLocaleLowerCase(),
14 | PAGE_SIZE,
15 | );
16 |
17 | return (
18 |
19 |
20 | All Talents
21 |
22 |
23 | {searchQuery && users.length > 0 && (
24 |
25 | Search results for {searchQuery}
26 |
27 | )}
28 | {searchQuery && users.length === 0 && (
29 |
30 | No search results for {searchQuery}
31 |
32 | )}
33 |
34 |
35 |
36 |
37 |
38 |
39 | {users.map((user, i) => {
40 | return ;
41 | })}
42 |
43 |
44 | {users.length > 0 && hasMoreData && !loading && (
45 |
46 | loadMore()}>
53 | Load More
54 |
55 |
56 | )}
57 | {loading && (
58 |
59 |
60 |
61 | )}
62 | {!hasMoreData && (
63 |
64 |
No more Users...
65 |
66 | )}
67 |
68 | );
69 | }
70 |
71 | export default Talents;
72 |
--------------------------------------------------------------------------------
/src/queries/evidences.ts:
--------------------------------------------------------------------------------
1 | import { processRequest } from '../utils/graphql';
2 |
3 | export const getEvidencesTransactionId = (transactionId: string): Promise => {
4 | let condition = `where: {transaction_: {id: "${transactionId}"}`;
5 | condition += '}, orderBy: id, orderDirection: asc';
6 | const query = `
7 | {
8 | evidences(${condition}) {
9 | id
10 | cid
11 | party {
12 | handle
13 | id
14 | }
15 | description {
16 | name
17 | fileTypeExtension
18 | description
19 | fileHash
20 | }
21 | }
22 | }
23 | `;
24 | return processRequest(query);
25 | };
26 |
--------------------------------------------------------------------------------
/src/queries/fees.ts:
--------------------------------------------------------------------------------
1 | import { processRequest } from '../utils/graphql';
2 |
3 | export const getProtocolAndPlatformsFees = (
4 | originServicePlatformId: string,
5 | originValidatedProposalPlatformId: string,
6 | ): Promise => {
7 | const query = `
8 | {
9 | protocols {
10 | protocolEscrowFeeRate
11 | }
12 | servicePlatform: platform(id:${originServicePlatformId}){
13 | originServiceFeeRate
14 | }
15 | proposalPlatform: platform(id:${originValidatedProposalPlatformId}){
16 | originValidatedProposalFeeRate
17 | }
18 | }
19 | `;
20 |
21 | return processRequest(query);
22 | };
23 |
--------------------------------------------------------------------------------
/src/queries/global.ts:
--------------------------------------------------------------------------------
1 | import { processRequest } from '../utils/graphql';
2 | import { getUserByAddress } from './users';
3 |
4 | export const graphIsSynced = async (entity: string, cid: string): Promise => {
5 | return new Promise((resolve, reject) => {
6 | const interval = setInterval(async () => {
7 | const response = await checkEntityByUri(entity, cid);
8 | if (response?.data?.data?.[entity][0]) {
9 | clearInterval(interval);
10 | resolve(response?.data?.data?.[entity][0].id);
11 | }
12 | }, 5000);
13 | });
14 | };
15 |
16 | export const graphUserIsSynced = async (address: string): Promise => {
17 | return new Promise((resolve, reject) => {
18 | const interval = setInterval(async () => {
19 | const response = await getUserByAddress(address);
20 | if (response?.data?.data?.['users'][0]) {
21 | clearInterval(interval);
22 | resolve(response?.data?.data?.['users'][0].id);
23 | }
24 | }, 3000);
25 | });
26 | };
27 |
28 | export const checkEntityByUri = (entity: string, cid: string): Promise => {
29 | let query;
30 | if (entity.includes('Description')) {
31 | query = `
32 | {
33 | ${entity}(where: {${entity.replace('Descriptions', '')}_ : {cid: "${cid}"}}, first: 1) {
34 | id
35 | }
36 | } `;
37 | } else {
38 | query = `
39 | {
40 | ${entity}(where: {cid: "${cid}"}, first: 1) {
41 | id
42 | }
43 | } `;
44 | }
45 | return processRequest(query);
46 | };
47 |
48 | export const getAllowedTokenList = (): Promise => {
49 | const query = `
50 | {
51 | tokens(where: {allowed: true}) {
52 | address
53 | symbol
54 | name
55 | decimals
56 | minimumTransactionAmount
57 | }
58 | }
59 | `;
60 | return processRequest(query);
61 | };
62 |
63 | export const getAllowedToken = (address: string): Promise => {
64 | const query = `
65 | {
66 | tokens(where: {allowed: true, address:"${address}"}) {
67 | decimals
68 | minimumTransactionAmount
69 | }
70 | }
71 | `;
72 | return processRequest(query);
73 | };
74 |
--------------------------------------------------------------------------------
/src/queries/payments.ts:
--------------------------------------------------------------------------------
1 | import { processRequest } from '../utils/graphql';
2 |
3 | export const getPaymentsByService = (serviceId: string, paymentType?: string): Promise => {
4 | let condition = `where: {service: "${serviceId}"`;
5 | paymentType ? (condition += `, paymentType: "${paymentType}"`) : '';
6 | condition += '}, orderBy: id, orderDirection: asc';
7 | const query = `
8 | {
9 | payments(${condition}) {
10 | id
11 | amount
12 | rateToken {
13 | address
14 | decimals
15 | name
16 | symbol
17 | }
18 | paymentType
19 | transactionHash
20 | createdAt
21 | }
22 | }
23 | `;
24 | return processRequest(query);
25 | };
26 |
27 | export const getPaymentsForUser = (
28 | userId: string,
29 | numberPerPage?: number,
30 | offset?: number,
31 | startDate?: string,
32 | endDate?: string,
33 | ): Promise => {
34 | const pagination = numberPerPage ? 'first: ' + numberPerPage + ', skip: ' + offset : '';
35 |
36 | const startDataCondition = startDate ? `, createdAt_gte: "${startDate}"` : '';
37 | const endDateCondition = endDate ? `, createdAt_lte: "${endDate}"` : '';
38 |
39 | const query = `
40 | {
41 | payments(where: {
42 | service_: {seller: "${userId}"}
43 | ${startDataCondition}
44 | ${endDateCondition}
45 | },
46 | orderBy: createdAt orderDirection: desc ${pagination} ) {
47 | id,
48 | rateToken {
49 | address
50 | decimals
51 | name
52 | symbol
53 | }
54 | amount
55 | transactionHash
56 | paymentType
57 | createdAt
58 | service {
59 | id,
60 | cid
61 | }
62 | }
63 | }
64 | `;
65 | return processRequest(query);
66 | };
67 |
--------------------------------------------------------------------------------
/src/queries/reviews.ts:
--------------------------------------------------------------------------------
1 | import { processRequest } from '../utils/graphql';
2 |
3 | export const getReviewsByService = (serviceId: string): Promise => {
4 | const query = `
5 | {
6 | reviews(where: { service: "${serviceId}" }, orderBy: id, orderDirection: desc) {
7 | id
8 | rating
9 | createdAt
10 | service {
11 | id
12 | status
13 | }
14 | to {
15 | id
16 | handle
17 | }
18 | description{
19 | id
20 | content
21 | }
22 | }
23 | }
24 | `;
25 | return processRequest(query);
26 | };
27 |
--------------------------------------------------------------------------------
/src/queries/transactions.ts:
--------------------------------------------------------------------------------
1 | import { processRequest } from '../utils/graphql';
2 |
3 | export const getTransactionById = (id: string): Promise => {
4 | const query = `
5 | {
6 | transactions (where: {id: "${id}"}) {
7 | id
8 | sender {
9 | id
10 | }
11 | receiver {
12 | id
13 | handle
14 | address
15 | }
16 | token {
17 | decimals
18 | symbol
19 | }
20 | arbitrationFeeTimeout
21 | amount
22 | disputeId
23 | senderFee
24 | receiverFee
25 | lastInteraction
26 | senderFeePaidAt
27 | receiverFeePaidAt
28 | arbitrator
29 | status
30 | ruling
31 | evidences {
32 | cid
33 | party {
34 | address
35 | createdAt
36 | handle
37 | id
38 | }
39 | }
40 | }
41 | }
42 | `;
43 | return processRequest(query);
44 | };
45 |
--------------------------------------------------------------------------------
/src/queries/users.ts:
--------------------------------------------------------------------------------
1 | import { processRequest } from '../utils/graphql';
2 |
3 | export const getUsers = (
4 | numberPerPage?: number,
5 | offset?: number,
6 | searchQuery?: string,
7 | ): Promise => {
8 | const pagination = numberPerPage ? 'first: ' + numberPerPage + ', skip: ' + offset : '';
9 | let condition = ', where: {';
10 | condition += searchQuery ? `, handle_contains_nocase: "${searchQuery}"` : '';
11 | condition += '}';
12 |
13 | const query = `
14 | {
15 | users(orderBy: rating, orderDirection: desc ${pagination} ${condition}) {
16 | id
17 | address
18 | handle
19 | userStats {
20 | numReceivedReviews
21 | }
22 | rating
23 | }
24 | }
25 | `;
26 | return processRequest(query);
27 | };
28 |
29 | export const getUserById = (id: string): Promise => {
30 | const query = `
31 | {
32 | user(id: "${id}") {
33 | id
34 | address
35 | handle
36 | rating
37 | delegates
38 | userStats {
39 | numReceivedReviews
40 | }
41 | updatedAt
42 | createdAt
43 | description {
44 | about
45 | role
46 | name
47 | country
48 | headline
49 | id
50 | image_url
51 | video_url
52 | title
53 | timezone
54 | skills_raw
55 | }
56 | }
57 | }
58 | `;
59 | return processRequest(query);
60 | };
61 |
62 | export const getUserByAddress = (address: string): Promise => {
63 | const query = `
64 | {
65 | users(where: {address: "${address.toLocaleLowerCase()}"}, first: 1) {
66 | id
67 | address
68 | handle
69 | rating
70 | delegates
71 | userStats {
72 | numReceivedReviews
73 | }
74 | updatedAt
75 | createdAt
76 | description {
77 | about
78 | role
79 | name
80 | country
81 | headline
82 | id
83 | image_url
84 | video_url
85 | title
86 | timezone
87 | skills_raw
88 | }
89 | }
90 | }
91 | `;
92 | return processRequest(query);
93 | };
94 |
95 | export const getUserTotalGains = (id: string): Promise => {
96 | const query = `
97 | {
98 | user(id: "${id}") {
99 | totalGains{
100 | id
101 | totalGain
102 | token {
103 | id
104 | name
105 | symbol
106 | decimals
107 | }
108 | }
109 | }
110 | }
111 | `;
112 | return processRequest(query);
113 | };
114 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/src/utils/conversion.ts:
--------------------------------------------------------------------------------
1 | import { ethers } from 'ethers';
2 | import { config } from '../config';
3 | import { IToken } from '../types';
4 |
5 | export const renderTokenAmount = (token: IToken, value: string): string => {
6 | const formattedValue = ethers.utils.formatUnits(value, token.decimals);
7 | return `${formattedValue} ${token.symbol}`;
8 | };
9 |
10 | // TODO: query tokens list from graph
11 | export const renderTokenAmountFromConfig = (tokenAddress: string, value: string): string | null => {
12 | if (config.tokens[tokenAddress] === undefined || !value) {
13 | return null;
14 | }
15 | const symbol = config.tokens[tokenAddress].symbol;
16 | const formattedValue = ethers.utils.formatUnits(value, config.tokens[tokenAddress].decimals);
17 | return `${formattedValue} ${symbol}`;
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/dates.ts:
--------------------------------------------------------------------------------
1 | export const formatDate = (timestamp: number | undefined) => {
2 | if (!timestamp) return '';
3 | return new Date(timestamp).toLocaleDateString('en-US', {
4 | month: 'numeric',
5 | day: 'numeric',
6 | });
7 | };
8 |
9 | export const formatTimestampDivider = (timestamp: number | undefined) => {
10 | if (!timestamp) return '';
11 | return new Date(timestamp)?.toLocaleString('en-US', {
12 | year: 'numeric',
13 | month: 'long',
14 | day: 'numeric',
15 | });
16 | };
17 |
18 | export const formatStringDate = (timestamp: string) => {
19 | return new Date(timestamp).toLocaleDateString('en-US', {
20 | year: 'numeric',
21 | month: 'long',
22 | day: 'numeric',
23 | hour: 'numeric',
24 | minute: 'numeric',
25 | });
26 | };
27 |
28 | export const formatDateDivider = (d?: Date) =>
29 | d?.toLocaleString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
30 |
31 | export const formatTimestampDateConversationCard = (timestamp: number | undefined) => {
32 | if (!timestamp) return '';
33 | return new Date(timestamp)?.toLocaleString('en-US', {
34 | year: 'numeric',
35 | month: 'numeric',
36 | day: 'numeric',
37 | });
38 | };
39 |
40 | export const formatDateConversationCard = (date: Date | undefined) => {
41 | if (!date) return '';
42 | return date.toLocaleString('en-US', {
43 | year: 'numeric',
44 | month: 'numeric',
45 | day: 'numeric',
46 | });
47 | };
48 |
49 | export const formatStringCompleteDate = (timestamp: number) => {
50 | return new Date(timestamp * 1000).toLocaleString('en-US', {
51 | year: 'numeric',
52 | month: 'long',
53 | day: 'numeric',
54 | hour: 'numeric',
55 | minute: 'numeric',
56 | second: 'numeric',
57 | hour12: false,
58 | });
59 | };
60 |
--------------------------------------------------------------------------------
/src/utils/graphql.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import axios from 'axios';
3 | import { config } from '../config';
4 |
5 | export const processRequest = async (query: string): Promise => {
6 | try {
7 | return await axios.post(config.subgraphUrl, { query });
8 | } catch (err) {
9 | console.error(err);
10 | return null;
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export const truncateAddress = (address: string, length = 5) => {
2 | return `${address.substring(0, length)}...${address.substring(
3 | address.length - length,
4 | address.length,
5 | )}`;
6 | };
7 |
8 | export const isValidHttpsUrl = (path: string) => {
9 | let url;
10 |
11 | try {
12 | url = new URL(path);
13 | } catch (_) {
14 | return false;
15 | }
16 |
17 | return url.protocol === 'https:';
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/ipfs.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { create, IPFSHTTPClient } from 'ipfs-http-client';
3 |
4 | export const postToIPFS = async (data: any): Promise => {
5 | let ipfs: IPFSHTTPClient | undefined;
6 | let cid = '';
7 | try {
8 | const authorization =
9 | 'Basic ' +
10 | btoa(process.env.NEXT_PUBLIC_INFURA_ID + ':' + process.env.NEXT_PUBLIC_INFURA_SECRET);
11 | ipfs = create({
12 | url: 'https://infura-ipfs.io:5001/api/v0',
13 | headers: {
14 | authorization,
15 | },
16 | });
17 | const result = await (ipfs as IPFSHTTPClient).add(data);
18 | cid = `${result.path}`;
19 | } catch (error) {
20 | console.error('IPFS error ', error);
21 | }
22 | return cid;
23 | };
24 |
25 | export const IpfsIsSynced = async (cid: string): Promise => {
26 | return new Promise((resolve, reject) => {
27 | const interval = setInterval(async () => {
28 | const response = await fetch(cid);
29 | if (response.status === 200) {
30 | clearInterval(interval);
31 | resolve(true);
32 | }
33 | }, 5000);
34 | });
35 | };
36 |
37 | export const readFromIpfs = async (cid: string): Promise => {
38 | try {
39 | const response = await fetch(process.env.NEXT_PUBLIC_IPFS_BASE_URL + cid);
40 | return await response.json();
41 | } catch (error) {
42 | console.error('IPFS error ', error);
43 | }
44 | };
45 |
46 | export const readFileFromIpfs = async (cid: string): Promise => {
47 | try {
48 | const response = await fetch(process.env.NEXT_PUBLIC_IPFS_BASE_URL + cid);
49 | return await response.arrayBuffer();
50 | } catch (error) {
51 | console.error('IPFS error ', error);
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/src/utils/signature.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export const getSignature = async (method: string, args: Record) => {
4 | if (!process.env.NEXT_PUBLIC_SIGNATURE_API_URL) {
5 | return '0x';
6 | }
7 | const res = await axios.post(process.env.NEXT_PUBLIC_SIGNATURE_API_URL as string, {
8 | method,
9 | args,
10 | });
11 |
12 | return JSON.parse(res.data.result);
13 | };
14 |
15 | interface CreateServiceArgs {
16 | profileId: number;
17 | cid: string;
18 | }
19 |
20 | export const getServiceSignature = async (args: CreateServiceArgs) => {
21 | return getSignature('createService', args);
22 | };
23 |
24 | interface CreateProposalArgs {
25 | profileId: number;
26 | serviceId: number;
27 | cid: string;
28 | }
29 |
30 | export const getProposalSignature = async (args: CreateProposalArgs) => {
31 | return getSignature('createProposal', args);
32 | };
33 |
--------------------------------------------------------------------------------
/src/utils/web3.ts:
--------------------------------------------------------------------------------
1 | import { Contract } from '@ethersproject/contracts';
2 | import { ExternalProvider, JsonRpcFetchFunc, Web3Provider } from '@ethersproject/providers';
3 | import { BigNumber, ethers, FixedNumber } from 'ethers';
4 | import { ITokenFormattedValues } from '../types';
5 |
6 | export default function getLibrary(provider: ExternalProvider | JsonRpcFetchFunc): Web3Provider {
7 | const library = new Web3Provider(provider);
8 | library.pollingInterval = 8000;
9 | return library;
10 | }
11 |
12 | const getDecimal = async (erc20Token: Contract): Promise => {
13 | if (!sessionStorage[erc20Token.address]) {
14 | const tokenDecimals = await erc20Token.decimals();
15 | sessionStorage[erc20Token.address] = tokenDecimals;
16 | return tokenDecimals;
17 | }
18 | return JSON.parse(sessionStorage[erc20Token.address]);
19 | };
20 |
21 | export const parseRateAmount = async (
22 | rateAmount: string,
23 | rateToken: string,
24 | decimals?: number,
25 | ): Promise => {
26 | if (rateToken === ethers.constants.AddressZero) {
27 | return ethers.utils.parseEther(rateAmount);
28 | }
29 | return ethers.utils.parseUnits(rateAmount, decimals);
30 | };
31 |
32 | export const formatRateAmount = (
33 | rateAmount: string,
34 | rateToken: string,
35 | tokenDecimals: number,
36 | ): ITokenFormattedValues => {
37 | if (rateToken === ethers.constants.AddressZero) {
38 | const valueInEther = ethers.utils.formatEther(rateAmount);
39 | const roundedValue = FixedNumber.from(valueInEther).round(2).toString();
40 | const exactValue = FixedNumber.from(valueInEther).toString();
41 | return {
42 | roundedValue,
43 | exactValue,
44 | };
45 | }
46 | const valueInToken = ethers.utils.formatUnits(rateAmount, tokenDecimals);
47 | const roundedValue = FixedNumber.from(valueInToken).round(2).toString();
48 | const exactValue = FixedNumber.from(valueInToken).toString();
49 | return {
50 | roundedValue,
51 | exactValue,
52 | };
53 | };
54 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [require('@tailwindcss/forms'), require('@tailwindcss/line-clamp')],
8 | };
9 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": [
6 | "DOM",
7 | "DOM.Iterable",
8 | "ESNext"
9 | ],
10 | "allowJs": false,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "module": "ESNext",
17 | "moduleResolution": "Node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "preserve",
22 | "incremental": true
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
25 | "exclude": [
26 | "node_modules"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {"source": "/(.*)", "destination": "/"}
4 | ]
5 | }
--------------------------------------------------------------------------------