├── .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 | 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 |
handleSubmit(e)}> 26 |
27 |
28 | 29 | 36 | 41 | 42 | 43 | setSearchQuery(e.target.value)} 48 | value={searchQuery} 49 | /> 50 |
51 |
52 | 57 |
58 |
59 |
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 |
handleSubmit(e)}> 26 |
27 |
28 | 29 | 36 | 41 | 42 | 43 | setSearchQuery(e.target.value)} 48 | value={searchQuery} 49 | /> 50 |
51 | 52 |
53 | 58 |
59 |
60 |
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 | MATIC 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 | 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 | 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 | 17 | 18 |
22 |
23 |
24 |
25 |

Create a review

26 | 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 | 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 | 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 | default avatar 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 | default avatar 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 | 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 |
    39 |
    40 |
    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 |
    28 | 33 | 38 | 39 |
    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 | default avatar 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 |
    27 | 32 | 37 | 38 |
    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 | 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 | 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 |
    8 | 9 |
    10 |
    11 | 12 | logo 13 | 14 |
    15 | 22 |
    23 |
    24 |
    25 |
    26 |
    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 |
    8 |

    Conversations

    9 |
    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 | 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 | 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 | 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 |
    50 | 58 | Mint Badge 59 | 60 | {sismoGroupData.userInGroup && ( 61 | 68 | )} 69 |
    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 | 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 |
    21 |
    {user?.id && }
    22 |
    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 | 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 | } --------------------------------------------------------------------------------