├── .github
├── FUNDING.yml
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
├── src
├── app
│ ├── config
│ │ └── index.ts
│ ├── favicon.ico
│ ├── categories
│ │ ├── [categoryId]
│ │ │ ├── layout.tsx
│ │ │ ├── project-ranking
│ │ │ │ ├── layout.tsx
│ │ │ │ └── done
│ │ │ │ │ └── page.tsx
│ │ │ ├── filter-guide
│ │ │ │ └── page.tsx
│ │ │ ├── pairwise-ranking
│ │ │ │ ├── done
│ │ │ │ │ └── page.tsx
│ │ │ │ └── ranking-done
│ │ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── components
│ │ │ ├── CategoryRewardBanner.tsx
│ │ │ ├── CategoryProjectItem.tsx
│ │ │ ├── CategoryRankingItem.tsx
│ │ │ ├── CategoryToggleButton.tsx
│ │ │ ├── CategoryCard.tsx
│ │ │ ├── CategoryRankingBasicListItem.tsx
│ │ │ ├── DrawerContent.tsx
│ │ │ ├── CategoryRankingNotSelectedListItem.tsx
│ │ │ ├── CategoryEditProjectItem.tsx
│ │ │ ├── CategoryBadge.tsx
│ │ │ ├── Countdown.tsx
│ │ │ ├── CategoryRankingListItem.tsx
│ │ │ ├── CategoriesProjectDrawerContent.tsx
│ │ │ ├── CategoryProjectRankingCard.tsx
│ │ │ ├── CategoryPairwiseCard.tsx
│ │ │ ├── CategoryPairwiseCardWithMetrics.tsx
│ │ │ ├── CategoryItem.tsx
│ │ │ └── CategoryCardView.tsx
│ │ ├── types.ts
│ │ └── page.tsx
│ ├── helpers
│ │ ├── cn.ts
│ │ └── text-helpers.ts
│ ├── category-ranking
│ │ ├── layout.tsx
│ │ ├── components
│ │ │ └── CategoryPairwiseModal.tsx
│ │ ├── done
│ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── constants
│ │ ├── WalletIcons.ts
│ │ ├── BadgesData.ts
│ │ └── Routes.ts
│ ├── badges
│ │ ├── layout.tsx
│ │ ├── components
│ │ │ ├── AdjacentBadges.tsx
│ │ │ └── BadgeCard.tsx
│ │ └── page.tsx
│ ├── components
│ │ ├── CircleNumber.tsx
│ │ ├── LoadingSpinner.tsx
│ │ ├── ProgressBar.tsx
│ │ ├── TopNavigation.tsx
│ │ ├── Button.tsx
│ │ ├── MinimumIncludedProjectsModal.tsx
│ │ ├── Modal.tsx
│ │ ├── ConnectDrawers.tsx
│ │ ├── TopRouteIndicator.tsx
│ │ ├── VoteSubmitted.tsx
│ │ ├── LogoutModal.tsx
│ │ ├── Header.tsx
│ │ ├── Drawer.tsx
│ │ └── SubmittingVoteSpinner.tsx
│ ├── features
│ │ ├── user
│ │ │ ├── getOtp.ts
│ │ │ ├── getIsUserLoggedIn.ts
│ │ │ └── updateOtp.ts
│ │ ├── categories
│ │ │ ├── getCategories.ts
│ │ │ ├── getCategoryPairs.ts
│ │ │ ├── updatePairwiseFinish.ts
│ │ │ ├── getProjectsByCategoryId.ts
│ │ │ ├── getCategoryById.ts
│ │ │ ├── updateCategoryVote.ts
│ │ │ ├── getCategoryRankings.ts
│ │ │ ├── getPairwisePairs.ts
│ │ │ ├── updateCategoryMarkFiltered.ts
│ │ │ ├── updateProjectInclusion.ts
│ │ │ ├── getProjectsRankingByCategoryId.ts
│ │ │ ├── updateProjectInclusionBulk.ts
│ │ │ ├── updateSortingByCategoryId.ts
│ │ │ └── updateProjectVote.ts
│ │ └── badges
│ │ │ └── getBadges.ts
│ ├── login
│ │ ├── layout.tsx
│ │ └── components
│ │ │ ├── ErrorBox.tsx
│ │ │ ├── BackHeader.tsx
│ │ │ ├── InfoBox.tsx
│ │ │ ├── SuccessBox.tsx
│ │ │ ├── bouncing-dots
│ │ │ └── DotsLoader.tsx
│ │ │ ├── success-screens
│ │ │ ├── SigninSuccess.tsx
│ │ │ └── SignupSuccess.tsx
│ │ │ ├── SignInEmail2.tsx
│ │ │ └── OtpInput.tsx
│ ├── connect
│ │ ├── components
│ │ │ ├── ConnectErrorBox.tsx
│ │ │ ├── ConnectHeader.tsx
│ │ │ ├── ConnectSplashMessage.tsx
│ │ │ ├── ConnectOtpInput.tsx
│ │ │ ├── ConnectFooter.tsx
│ │ │ └── ConnectButton.tsx
│ │ ├── layout.tsx
│ │ ├── otp
│ │ │ ├── no-badge
│ │ │ │ └── page.tsx
│ │ │ └── success
│ │ │ │ └── page.tsx
│ │ └── page.tsx
│ ├── reset
│ │ └── page.tsx
│ ├── providers
│ │ ├── PostHogProvider.tsx
│ │ ├── TanstackProvider.tsx
│ │ ├── WagmiAppProvider.tsx
│ │ └── ConnectProvider.tsx
│ ├── welcome
│ │ ├── page.tsx
│ │ └── layout.tsx
│ ├── page.tsx
│ ├── intro
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── globals.css
│ ├── hooks
│ │ └── useCopyToClipboard.ts
│ ├── api
│ │ └── rephrase
│ │ │ └── route.ts
│ └── layout.tsx
├── utils
│ ├── types.ts
│ ├── numbers.ts
│ ├── badgeUtils.ts
│ ├── AuthGuard.tsx
│ ├── eas.ts
│ ├── attest-utils.ts
│ └── auth.ts
└── lib
│ ├── axios.ts
│ ├── third-web
│ ├── provider.tsx
│ ├── constants.ts
│ ├── methods.ts
│ └── AutoConnect.tsx
│ └── react-query.ts
├── public
├── favicon.ico
├── images
│ ├── icons
│ │ ├── types.ts
│ │ ├── IconCancel.tsx
│ │ ├── IconCheck.tsx
│ │ ├── IconArrowLeft.tsx
│ │ ├── IconAlertCircle.tsx
│ │ ├── IconRefresh.tsx
│ │ ├── ErrorBoxX.tsx
│ │ ├── SuccessBoxIcon.tsx
│ │ ├── iconOTP.tsx
│ │ ├── IconMove.tsx
│ │ ├── Edit2.tsx
│ │ ├── IconTrash.tsx
│ │ ├── IconWallet.tsx
│ │ ├── IconX.tsx
│ │ ├── ListIcon.tsx
│ │ ├── IconLogout.tsx
│ │ ├── IconCopy.tsx
│ │ ├── IconEye.tsx
│ │ ├── IconParagraph.tsx
│ │ ├── WarningBoxIcon.tsx
│ │ ├── IconWarning.tsx
│ │ ├── IconGithub.tsx
│ │ ├── IconBug.tsx
│ │ └── CardIcon.tsx
│ ├── logo.png
│ ├── apple.png
│ ├── google.png
│ ├── mail-01.png
│ ├── badges
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ └── 4.png
│ ├── confetti.gif
│ ├── sandClock.gif
│ ├── tokens
│ │ └── op.png
│ ├── characters
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ ├── 5.png
│ │ ├── 6.png
│ │ ├── 7.png
│ │ ├── 8.png
│ │ ├── 9.png
│ │ ├── 10.png
│ │ ├── 11.png
│ │ ├── 12.png
│ │ ├── 13.png
│ │ ├── 14.png
│ │ ├── 15.png
│ │ ├── 16.png
│ │ ├── 17.png
│ │ ├── 18.png
│ │ ├── 19.png
│ │ ├── 20.png
│ │ ├── 21.png
│ │ ├── 22.png
│ │ ├── 23.png
│ │ ├── 24.png
│ │ ├── 25.png
│ │ ├── 26.png
│ │ ├── 27.png
│ │ ├── 28.png
│ │ ├── 29.png
│ │ ├── 30.png
│ │ ├── 31.png
│ │ ├── 32.png
│ │ ├── welcome-character.png
│ │ └── ranking-done-character.png
│ ├── error-box-x.png
│ ├── filter-guide
│ │ ├── 1.png
│ │ └── 2.png
│ ├── impact-profit.png
│ ├── collect-loading.png
│ ├── logos
│ │ └── logo-text.png
│ ├── sign-in-success.png
│ ├── wallets
│ │ ├── cbw-logo.png
│ │ ├── metamask-logo.png
│ │ └── walletconnect-logo.png
│ └── defaults
│ │ ├── category
│ │ ├── category-1.png
│ │ ├── category-2.png
│ │ ├── category-3.png
│ │ ├── category-4.png
│ │ ├── category-5.png
│ │ ├── category icon 1.png
│ │ ├── category icon 2.png
│ │ ├── category icon 3.png
│ │ ├── category icon 4.png
│ │ └── category icon 5.png
│ │ └── project
│ │ ├── Project Card -_ Default.png
│ │ ├── Project Cover -_ Default.png
│ │ ├── Project Icon Large -_ Default.png
│ │ └── Project Icon Small -_ Default.png
└── preview-image.png
├── postcss.config.js
├── funding.json
├── .prettierrc
├── .eslintrc.json
├── .gitignore
├── next.config.mjs
├── tsconfig.json
├── tailwind.config.ts
├── README.md
└── package.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ['https://giveth.io/donate/pairwise']
2 |
--------------------------------------------------------------------------------
/src/app/config/index.ts:
--------------------------------------------------------------------------------
1 | export const API_URL = process.env.NEXT_PUBLIC_BASE_URL;
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/icons/types.ts:
--------------------------------------------------------------------------------
1 | interface IIconProps {
2 | color?: string;
3 | size?: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/apple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/apple.png
--------------------------------------------------------------------------------
/public/images/google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/google.png
--------------------------------------------------------------------------------
/public/images/mail-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/mail-01.png
--------------------------------------------------------------------------------
/public/preview-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/preview-image.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/images/badges/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/badges/1.png
--------------------------------------------------------------------------------
/public/images/badges/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/badges/2.png
--------------------------------------------------------------------------------
/public/images/badges/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/badges/3.png
--------------------------------------------------------------------------------
/public/images/badges/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/badges/4.png
--------------------------------------------------------------------------------
/public/images/confetti.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/confetti.gif
--------------------------------------------------------------------------------
/public/images/sandClock.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/sandClock.gif
--------------------------------------------------------------------------------
/public/images/tokens/op.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/tokens/op.png
--------------------------------------------------------------------------------
/public/images/characters/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/2.png
--------------------------------------------------------------------------------
/public/images/characters/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/3.png
--------------------------------------------------------------------------------
/public/images/characters/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/4.png
--------------------------------------------------------------------------------
/public/images/characters/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/5.png
--------------------------------------------------------------------------------
/public/images/characters/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/6.png
--------------------------------------------------------------------------------
/public/images/characters/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/7.png
--------------------------------------------------------------------------------
/public/images/characters/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/8.png
--------------------------------------------------------------------------------
/public/images/characters/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/9.png
--------------------------------------------------------------------------------
/public/images/error-box-x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/error-box-x.png
--------------------------------------------------------------------------------
/public/images/characters/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/10.png
--------------------------------------------------------------------------------
/public/images/characters/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/11.png
--------------------------------------------------------------------------------
/public/images/characters/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/12.png
--------------------------------------------------------------------------------
/public/images/characters/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/13.png
--------------------------------------------------------------------------------
/public/images/characters/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/14.png
--------------------------------------------------------------------------------
/public/images/characters/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/15.png
--------------------------------------------------------------------------------
/public/images/characters/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/16.png
--------------------------------------------------------------------------------
/public/images/characters/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/17.png
--------------------------------------------------------------------------------
/public/images/characters/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/18.png
--------------------------------------------------------------------------------
/public/images/characters/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/19.png
--------------------------------------------------------------------------------
/public/images/characters/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/20.png
--------------------------------------------------------------------------------
/public/images/characters/21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/21.png
--------------------------------------------------------------------------------
/public/images/characters/22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/22.png
--------------------------------------------------------------------------------
/public/images/characters/23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/23.png
--------------------------------------------------------------------------------
/public/images/characters/24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/24.png
--------------------------------------------------------------------------------
/public/images/characters/25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/25.png
--------------------------------------------------------------------------------
/public/images/characters/26.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/26.png
--------------------------------------------------------------------------------
/public/images/characters/27.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/27.png
--------------------------------------------------------------------------------
/public/images/characters/28.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/28.png
--------------------------------------------------------------------------------
/public/images/characters/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/29.png
--------------------------------------------------------------------------------
/public/images/characters/30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/30.png
--------------------------------------------------------------------------------
/public/images/characters/31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/31.png
--------------------------------------------------------------------------------
/public/images/characters/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/32.png
--------------------------------------------------------------------------------
/public/images/filter-guide/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/filter-guide/1.png
--------------------------------------------------------------------------------
/public/images/filter-guide/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/filter-guide/2.png
--------------------------------------------------------------------------------
/public/images/impact-profit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/impact-profit.png
--------------------------------------------------------------------------------
/public/images/collect-loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/collect-loading.png
--------------------------------------------------------------------------------
/public/images/logos/logo-text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/logos/logo-text.png
--------------------------------------------------------------------------------
/public/images/sign-in-success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/sign-in-success.png
--------------------------------------------------------------------------------
/public/images/wallets/cbw-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/wallets/cbw-logo.png
--------------------------------------------------------------------------------
/funding.json:
--------------------------------------------------------------------------------
1 | {
2 | "opRetro": {
3 | "projectId": "0x98877a3c5f3d5eee496386ae93a23b17f0f51b70b3041b3c8226f98fbeca09ec"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/images/wallets/metamask-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/wallets/metamask-logo.png
--------------------------------------------------------------------------------
/public/images/wallets/walletconnect-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/wallets/walletconnect-logo.png
--------------------------------------------------------------------------------
/public/images/characters/welcome-character.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/welcome-character.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category-1.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category-2.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category-3.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category-4.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category-5.png
--------------------------------------------------------------------------------
/public/images/characters/ranking-done-character.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/characters/ranking-done-character.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category icon 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category icon 1.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category icon 2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category icon 2.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category icon 3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category icon 3.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category icon 4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category icon 4.png
--------------------------------------------------------------------------------
/public/images/defaults/category/category icon 5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/category/category icon 5.png
--------------------------------------------------------------------------------
/public/images/defaults/project/Project Card -_ Default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/project/Project Card -_ Default.png
--------------------------------------------------------------------------------
/public/images/defaults/project/Project Cover -_ Default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/project/Project Cover -_ Default.png
--------------------------------------------------------------------------------
/public/images/defaults/project/Project Icon Large -_ Default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/project/Project Icon Large -_ Default.png
--------------------------------------------------------------------------------
/public/images/defaults/project/Project Icon Small -_ Default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GeneralMagicio/pairwise-rf4/HEAD/public/images/defaults/project/Project Icon Small -_ Default.png
--------------------------------------------------------------------------------
/src/utils/types.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: number;
3 | address: string;
4 | isBadgeholder: boolean;
5 | }
6 |
7 | export enum MinimumModalState {
8 | Shown,
9 | False,
10 | True,
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/categories/[categoryId]/layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | const CategoryLayout = ({ children }: { children: ReactNode }) => {
4 | return
{children}
;
5 | };
6 |
7 | export default CategoryLayout;
8 |
--------------------------------------------------------------------------------
/src/app/helpers/cn.ts:
--------------------------------------------------------------------------------
1 | import clsx from 'clsx';
2 | import { ClassValue } from 'clsx';
3 | import { twMerge } from 'tailwind-merge';
4 |
5 | export function cn(...classes: ClassValue[]) {
6 | return twMerge(clsx(classes.filter(Boolean)));
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/category-ranking/layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | const Layout = ({ children }: { children: ReactNode }) => {
4 | return {children}
;
5 | };
6 |
7 | export default Layout;
8 |
--------------------------------------------------------------------------------
/src/app/categories/[categoryId]/project-ranking/layout.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 |
3 | const ProjectRankingLayout = ({ children }: { children: ReactNode }) => {
4 | return {children}
;
5 | };
6 |
7 | export default ProjectRankingLayout;
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "useTabs": true,
4 | "tabWidth": 4,
5 | "semi": true,
6 | "jsxSingleQuote": true,
7 | "trailingComma": "all",
8 | "arrowParens": "avoid",
9 | "endOfLine": "auto",
10 | "plugins": ["prettier-plugin-tailwindcss"]
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/constants/WalletIcons.ts:
--------------------------------------------------------------------------------
1 | export const walletsLogos: {
2 | [key: string]: string;
3 | } = {
4 | walletConnect: '/images/wallets/walletconnect-logo.png',
5 | metaMask: '/images/wallets/metamask-logo.png',
6 | coinbaseWalletSDK: '/images/wallets/cbw-logo.png',
7 | };
8 |
--------------------------------------------------------------------------------
/src/app/badges/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { ReactNode } from 'react';
4 |
5 | const BadgesLayout = ({ children }: { children: ReactNode }) => {
6 | return {children}
;
7 | };
8 |
9 | export default BadgesLayout;
10 |
--------------------------------------------------------------------------------
/src/app/constants/BadgesData.ts:
--------------------------------------------------------------------------------
1 | export const badgesImages = [
2 | {
3 | src: '/images/badges/1.png',
4 | alt: 'badge 1',
5 | },
6 | {
7 | src: '/images/badges/2.png',
8 | alt: 'badge 2',
9 | },
10 | {
11 | src: '/images/badges/3.png',
12 | alt: 'badge 3',
13 | },
14 | {
15 | src: '/images/badges/4.png',
16 | alt: 'badge 4',
17 | },
18 | ];
19 |
--------------------------------------------------------------------------------
/src/app/helpers/text-helpers.ts:
--------------------------------------------------------------------------------
1 | export const truncate = (input: string, maxLength: number = 90) =>
2 | input.length > maxLength ? `${input.substring(0, maxLength)}...` : input;
3 |
4 | export const formatAddress = (address: string = '') => {
5 | const firstEight = address.substring(0, 8);
6 | const lastSix = address.substring(address.length - 6);
7 | return `${firstEight}...${lastSix}`;
8 | };
9 |
--------------------------------------------------------------------------------
/src/app/components/CircleNumber.tsx:
--------------------------------------------------------------------------------
1 | interface CircleNumberProps {
2 | number: number;
3 | }
4 |
5 | const CircleNumber: React.FC = ({ number }) => {
6 | return (
7 |
8 | {number}
9 |
10 | );
11 | };
12 |
13 | export default CircleNumber;
14 |
--------------------------------------------------------------------------------
/src/app/features/user/getOtp.ts:
--------------------------------------------------------------------------------
1 | // /auth/otp
2 |
3 | import { axios } from '@/lib/axios';
4 | import { useQuery } from '@tanstack/react-query';
5 | import { AxiosResponse } from 'axios';
6 |
7 | export const getOtp = async (): Promise => {
8 | return axios.get('auth/otp');
9 | };
10 |
11 | export const useGetOtp = () => {
12 | return useQuery({
13 | queryKey: ['otp'],
14 | queryFn: getOtp,
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "next/core-web-vitals", "prettier"],
3 | "plugins": ["react", "@typescript-eslint", "unused-imports"],
4 | "rules": {
5 | "unused-imports/no-unused-imports": "error",
6 | "unused-imports/no-unused-vars": [
7 | "warn",
8 | {
9 | "vars": "all",
10 | "varsIgnorePattern": "^_",
11 | "args": "after-used",
12 | "argsIgnorePattern": "^_"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/login/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Head from 'next/head';
4 | import React, { ReactNode } from 'react';
5 |
6 | const LoginLayout = ({ children }: { children: ReactNode }) => {
7 | return (
8 |
9 |
10 |
Login Page
11 |
12 | {children}
13 |
14 | );
15 | };
16 |
17 | export default LoginLayout;
18 |
--------------------------------------------------------------------------------
/src/app/constants/Routes.ts:
--------------------------------------------------------------------------------
1 | export const Routes = {
2 | Home: '/',
3 | Welcome: '/welcome',
4 | Intro: '/intro',
5 | Categories: '/categories',
6 | Badges: '/badges',
7 | Connect: '/connect',
8 | ConnectOtp: '/connect/otp',
9 | ConnectSuccess: '/connect/otp/success',
10 | LinkX: 'https://twitter.com/Pairwisevote',
11 | LinkGithub: 'https://github.com/GeneralMagicio/pairwise-RPGF4',
12 | LinkParagraph: 'https://paragraph.xyz/@pairwise',
13 | };
14 |
--------------------------------------------------------------------------------
/src/app/login/components/ErrorBox.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBoxX } from 'public/images/icons/ErrorBoxX';
2 | import { FC } from 'react';
3 |
4 | interface Props {
5 | message: string;
6 | }
7 |
8 | export const ErrorBox: FC = ({ message }) => {
9 | return (
10 |
11 | {message}
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/public/images/icons/IconCancel.tsx:
--------------------------------------------------------------------------------
1 | const IconCancel = () => {
2 | return (
3 |
10 |
17 |
18 | );
19 | };
20 |
21 | export default IconCancel;
22 |
--------------------------------------------------------------------------------
/src/app/login/components/BackHeader.tsx:
--------------------------------------------------------------------------------
1 | import IconArrowLeft from 'public/images/icons/IconArrowLeft';
2 | import { FC } from 'react';
3 |
4 | interface Props {
5 | onClick: () => void;
6 | }
7 |
8 | export const BackHeader: FC = ({ onClick }) => {
9 | return (
10 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/app/features/user/getIsUserLoggedIn.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { useQuery } from '@tanstack/react-query';
3 | import { AxiosResponse } from 'axios';
4 |
5 | export const getIsUserLoggedIn = async (): Promise => {
6 | return axios.get('auth/isLoggedIn');
7 | };
8 |
9 | export const useIsUserLoggedIn = () => {
10 | return useQuery({
11 | queryKey: ['isLoggedIn'],
12 | queryFn: getIsUserLoggedIn,
13 | });
14 | };
15 |
--------------------------------------------------------------------------------
/src/app/features/user/updateOtp.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { useMutation } from '@tanstack/react-query';
3 |
4 | type ProjectVoteData = {
5 | data: {
6 | otp: string;
7 | };
8 | };
9 |
10 | export const updateOtp = ({ data }: ProjectVoteData) => {
11 | return axios.post('auth/otp/validate', data);
12 | };
13 |
14 | export const useUpdateOtp = () => {
15 | return useMutation({
16 | mutationFn: updateOtp,
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/src/app/login/components/InfoBox.tsx:
--------------------------------------------------------------------------------
1 | import { WarningBoxIcon } from 'public/images/icons/WarningBoxIcon';
2 | import { FC } from 'react';
3 |
4 | interface Props {
5 | message: string;
6 | }
7 |
8 | export const InfoBox: FC = ({ message }) => {
9 | return (
10 |
11 | {message}
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/public/images/icons/IconCheck.tsx:
--------------------------------------------------------------------------------
1 | const IconCheck = ({ color = 'white', size = '24' }: IIconProps) => {
2 | return (
3 |
10 |
17 |
18 | );
19 | };
20 |
21 | export default IconCheck;
22 |
--------------------------------------------------------------------------------
/src/app/login/components/SuccessBox.tsx:
--------------------------------------------------------------------------------
1 | import { SuccessBoxIcon } from 'public/images/icons/SuccessBoxIcon';
2 | import { FC } from 'react';
3 |
4 | interface Props {
5 | message: string;
6 | }
7 |
8 | export const SuccessBox: FC = ({ message }) => {
9 | return (
10 |
14 | {message}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/app/login/components/bouncing-dots/DotsLoader.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 |
3 | export const DotsLoader = () => {
4 | const [dots, setDots] = useState(3);
5 |
6 | useEffect(() => {
7 | setTimeout(() => {
8 | setDots(Math.max((dots + 1) % 4, 1));
9 | }, 250);
10 | }, [dots]);
11 |
12 | return (
13 |
14 | {Array.from(Array(dots)).map((dot, index) => (
15 |
.
16 | ))}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/public/images/icons/IconArrowLeft.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconArrowLeft: React.FC = () => {
4 | return (
5 |
12 |
19 |
20 | );
21 | };
22 |
23 | export default IconArrowLeft;
24 |
--------------------------------------------------------------------------------
/src/app/connect/components/ConnectErrorBox.tsx:
--------------------------------------------------------------------------------
1 | import IconWarning from 'public/images/icons/IconWarning';
2 | import { FC } from 'react';
3 |
4 | interface Props {
5 | message: string;
6 | }
7 |
8 | export const ConnectErrorBox: FC = ({ message }) => {
9 | return (
10 |
11 |
12 |
13 |
14 | {message}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/app/features/categories/getCategories.ts:
--------------------------------------------------------------------------------
1 | import { ICategory } from '@/app/categories/types';
2 | import { axios } from '@/lib/axios';
3 | import { useQuery } from '@tanstack/react-query';
4 | import { AxiosResponse } from 'axios';
5 |
6 | export const getCategories = async (): Promise> => {
7 | return axios.get('flow/collections');
8 | };
9 |
10 | export const useCategories = () => {
11 | return useQuery({
12 | queryKey: ['categories'],
13 | queryFn: getCategories,
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/app/components/LoadingSpinner.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | const LoadingSpinner = () => {
4 | return (
5 |
8 | );
9 | };
10 |
11 | export const ButtonLoadingSpinner = () => {
12 | return (
13 |
14 | );
15 | };
16 |
17 | export default LoadingSpinner;
18 |
--------------------------------------------------------------------------------
/src/app/connect/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { ReactNode } from 'react';
4 |
5 | import ConnectHeader from './components/ConnectHeader';
6 | import ConnectFooter from './components/ConnectFooter';
7 |
8 | const ConnectLayout = ({ children }: { children: ReactNode }) => {
9 | return (
10 |
11 |
12 |
{children}
13 |
14 |
15 | );
16 | };
17 |
18 | export default ConnectLayout;
19 |
--------------------------------------------------------------------------------
/src/app/reset/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { axios } from '@/lib/axios';
4 | import React from 'react';
5 | import Button from '../components/Button';
6 |
7 | const ResetPage = () => {
8 | const onReset = async () => {
9 | await axios.get('/flow/temp-reset-inclusions');
10 | };
11 | return (
12 |
13 |
14 | Reset User Data
15 |
16 |
17 | );
18 | };
19 |
20 | export default ResetPage;
21 |
--------------------------------------------------------------------------------
/src/app/providers/PostHogProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ReactNode } from 'react';
4 | import posthog from 'posthog-js';
5 | import { PostHogProvider } from 'posthog-js/react';
6 |
7 | posthog.init(process.env.NEXT_PUBLIC_POST_HOG_API!, {
8 | api_host:
9 | process.env.NEXT_PUBLIC_POST_HOG_HOST || 'https://us.i.posthog.com',
10 | autocapture: false,
11 | });
12 |
13 | export default function PHProvider({ children }: { children: ReactNode }) {
14 | return {children} ;
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/axios.ts:
--------------------------------------------------------------------------------
1 | import { API_URL } from '@/app/config';
2 | import Axios, { InternalAxiosRequestConfig } from 'axios';
3 |
4 | function authRequestInterceptor(config: InternalAxiosRequestConfig) {
5 | config.headers = config.headers || {};
6 | config.headers.Accept = 'application/json';
7 | const token = localStorage.getItem('auth');
8 | if (token) config.headers.auth = token;
9 | return config;
10 | }
11 |
12 | export const axios = Axios.create({
13 | baseURL: API_URL,
14 | });
15 |
16 | axios.interceptors.request.use(authRequestInterceptor);
17 |
--------------------------------------------------------------------------------
/src/app/features/categories/getCategoryPairs.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { useQuery } from '@tanstack/react-query';
3 | import { IPairwisePairsResponse } from './getPairwisePairs';
4 |
5 | export const getCategoryPairs = async (): Promise<
6 | IPairwisePairsResponse['pairs'][0]
7 | > => {
8 | const res = await axios.get(`flow/pairs`);
9 | return res.data;
10 | };
11 |
12 | export const useGetCategoryPairs = () => {
13 | return useQuery({
14 | queryKey: ['category-pairs'],
15 | queryFn: () => getCategoryPairs(),
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/src/app/features/categories/updatePairwiseFinish.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { useMutation } from '@tanstack/react-query';
3 |
4 | type ProjectVoteData = {
5 | data: {
6 | cid: number;
7 | };
8 | };
9 |
10 | export const updatePairwiseFinish = ({ data }: ProjectVoteData) => {
11 | return axios.post('flow/finish', data);
12 | };
13 |
14 | export const useUpdatePairwiseFinish = () => {
15 | // const queryClient = useQueryClient();
16 |
17 | return useMutation({
18 | mutationFn: updatePairwiseFinish,
19 | // onSuccess:
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/src/app/login/components/success-screens/SigninSuccess.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | export const SigninSuccess = () => {
4 | return (
5 |
6 |
12 |
13 | Account verified successfully
14 |
15 | Hodl! Logging in...
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/app/categories/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { ReactNode } from 'react';
4 | import Header from '../components/Header'; // Adjust the path as necessary
5 | import { useParams } from 'next/navigation';
6 |
7 | const CategoriesLayout = ({ children }: { children: ReactNode }) => {
8 | const params = useParams();
9 |
10 | console.log('params', params);
11 |
12 | return (
13 |
14 | {!params.categoryId && }
15 | {children}
16 |
17 | );
18 | };
19 |
20 | export default CategoriesLayout;
21 |
--------------------------------------------------------------------------------
/src/app/connect/components/ConnectHeader.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Image from 'next/image';
4 | import ConnectButton from './ConnectButton';
5 |
6 | const ConnectHeader = () => {
7 | return (
8 |
19 | );
20 | };
21 |
22 | export default ConnectHeader;
23 |
--------------------------------------------------------------------------------
/src/app/welcome/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import Image from 'next/image';
3 | import React from 'react';
4 | import posthog from 'posthog-js';
5 |
6 | const WelcomePage = () => {
7 | posthog.capture('Just Landed');
8 | return (
9 |
10 |
16 |
Welcome to Pairwise
17 |
18 | );
19 | };
20 |
21 | export default WelcomePage;
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 | MyNotes.md
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | # local env files
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | .env*
40 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: 'https',
7 | hostname: 'content.optimism.io',
8 | port: '',
9 | pathname: '/profile/v0/profile-image/10/**',
10 | },
11 | {
12 | protocol: 'https',
13 | hostname: 'i.imgur.com',
14 | port: '',
15 | pathname: '/**',
16 | },
17 | {
18 | protocol: 'https',
19 | hostname: 'storage.googleapis.com',
20 | port: '',
21 | pathname: '/**',
22 | },
23 | ],
24 | },
25 | };
26 |
27 | export default nextConfig;
28 |
--------------------------------------------------------------------------------
/public/images/icons/IconAlertCircle.tsx:
--------------------------------------------------------------------------------
1 | const IconAlertCircle = ({ color = '#404454' }: IIconProps) => {
2 | return (
3 |
10 |
17 |
18 | );
19 | };
20 |
21 | export default IconAlertCircle;
22 |
--------------------------------------------------------------------------------
/src/app/components/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | interface IProgressBarProps {
2 | progress: number; // progress should be a value between 0 to 100
3 | isMinGreater: boolean;
4 | }
5 |
6 | const ProgressBar: React.FC = ({
7 | progress,
8 | isMinGreater,
9 | }) => {
10 | return (
11 |
19 | );
20 | };
21 |
22 | export default ProgressBar;
23 |
--------------------------------------------------------------------------------
/src/app/components/TopNavigation.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import IconArrowLeft from 'public/images/icons/IconArrowLeft';
3 |
4 | interface ITopNavigationProps {
5 | link?: string;
6 | text?: string;
7 | }
8 |
9 | const TopNavigation = ({ link = '/', text }: ITopNavigationProps) => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
{text}
17 |
18 |
19 | );
20 | };
21 |
22 | export default TopNavigation;
23 |
--------------------------------------------------------------------------------
/src/app/features/categories/getProjectsByCategoryId.ts:
--------------------------------------------------------------------------------
1 | import { IProject } from '@/app/categories/types';
2 | import { axios } from '@/lib/axios';
3 | import { useQuery } from '@tanstack/react-query';
4 | import { AxiosResponse } from 'axios';
5 |
6 | export const getProjectsByCategoryId = async (
7 | id: number,
8 | ): Promise> => {
9 | return axios.get(`flow/projects?cid=${id}`);
10 | };
11 |
12 | export const useProjectsByCategoryId = (id: number) => {
13 | return useQuery({
14 | queryKey: ['projects', id],
15 | queryFn: () => getProjectsByCategoryId(id),
16 | staleTime: Infinity,
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/numbers.ts:
--------------------------------------------------------------------------------
1 | import { Metric } from './getMetrics';
2 |
3 | export function formatMetricsNumber(num: Metric['value']) {
4 | if (num === undefined || num === null) {
5 | return undefined;
6 | }
7 | if (typeof num !== 'number') {
8 | return num;
9 | }
10 |
11 | if (num >= 1000000) {
12 | return (num / 1000000).toFixed(1) + 'M';
13 | } else if (num >= 1000) {
14 | return (num / 1000).toFixed(1) + 'K';
15 | } else {
16 | const decimalPlaces = (num.toString().split('.')[1] || []).length;
17 | if (decimalPlaces > 3) {
18 | return num.toFixed(3);
19 | } else {
20 | return num.toString();
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useActiveWallet } from 'thirdweb/react';
4 | import LoadingSpinner from './components/LoadingSpinner';
5 | import { useEffect } from 'react';
6 | import { useRouter } from 'next/navigation';
7 |
8 | export default function Home() {
9 | const router = useRouter();
10 | const wallet = useActiveWallet();
11 |
12 | useEffect(() => {
13 | router.push('/categories');
14 | }, [wallet, router]);
15 |
16 | if (wallet) {
17 | return ;
18 | }
19 |
20 | return (
21 |
22 | Main Page
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/badgeUtils.ts:
--------------------------------------------------------------------------------
1 | import { BadgeData } from '@/app/badges/components/BadgeCard';
2 | import { BadgeCardEntryType } from '@/app/badges/page';
3 |
4 | export const getBadgeAmount = (
5 | key: BadgeCardEntryType['0'],
6 | badges: BadgeData,
7 | ) => {
8 | return key === 'holderPoints'
9 | ? badges.holderAmount
10 | : key === 'delegatePoints'
11 | ? badges.delegateAmount
12 | : undefined;
13 | };
14 |
15 | export const getBadgeMedal = (
16 | key: BadgeCardEntryType['0'],
17 | badges: BadgeData,
18 | ) => {
19 | return key === 'holderPoints'
20 | ? badges.holderType
21 | : key === 'delegatePoints'
22 | ? badges.delegateType
23 | : undefined;
24 | };
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "baseUrl": ".",
16 | "paths": {
17 | "@/*": ["./src/*"]
18 | },
19 | "incremental": true,
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ]
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/public/images/icons/IconRefresh.tsx:
--------------------------------------------------------------------------------
1 | const IconRefresh = () => {
2 | return (
3 |
10 |
17 |
18 | );
19 | };
20 |
21 | export default IconRefresh;
22 |
--------------------------------------------------------------------------------
/src/app/login/components/success-screens/SignupSuccess.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | export const SignupSuccess = () => {
4 | return (
5 |
6 |
12 |
13 | Pairwise Passport Verified!
14 |
15 |
16 | Your journey to direct the Superchain through RetroPGF
17 | begins.
18 |
19 | Hodl! Logging in...
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/src/app/features/categories/getCategoryById.ts:
--------------------------------------------------------------------------------
1 | import { CollectionProgressStatus, ICategory } from '@/app/categories/types';
2 | import { axios } from '@/lib/axios';
3 | import { useQuery } from '@tanstack/react-query';
4 | import { AxiosResponse } from 'axios';
5 |
6 | interface ICategoryResponse {
7 | collection: ICategory;
8 | progress: CollectionProgressStatus;
9 | }
10 |
11 | export const getCategoryById = async (
12 | id: number,
13 | ): Promise> => {
14 | return axios.get(`collection/${id}`);
15 | };
16 |
17 | export const useCategoryById = (id: number) => {
18 | return useQuery({
19 | queryKey: ['category', id],
20 | queryFn: () => getCategoryById(id),
21 | });
22 | };
23 |
--------------------------------------------------------------------------------
/src/app/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import { ComponentProps } from 'react';
2 | import { cn } from '../helpers/cn';
3 | import { ButtonLoadingSpinner } from './LoadingSpinner';
4 |
5 | interface ButtonType extends ComponentProps<'button'> {
6 | isLoading?: boolean;
7 | }
8 |
9 | const Button = ({
10 | children,
11 | isLoading,
12 | className = '',
13 | ...props
14 | }: ButtonType) => {
15 | return (
16 |
23 | {children}
24 | {isLoading ? : null}
25 |
26 | );
27 | };
28 |
29 | export default Button;
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: '[Suggestion] '
5 | labels: Feature Request
6 | assignees: MoeNick
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/src/lib/third-web/provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { ReactNode } from 'react';
4 | import { createThirdwebClient } from 'thirdweb';
5 | import { ThirdwebProvider } from 'thirdweb/react';
6 | import { activeChain, clientId, factoryAddress } from './constants';
7 | import { AuthProvider } from './AutoConnect';
8 |
9 | export const smartWalletConfig = {
10 | factoryAddress: factoryAddress,
11 | chain: activeChain,
12 | gasless: true,
13 | };
14 |
15 | export const client = createThirdwebClient({ clientId });
16 |
17 | export const Thirdweb5Provider = ({ children }: { children: ReactNode }) => {
18 | return (
19 |
20 | {children}
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/public/images/icons/ErrorBoxX.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const ErrorBoxX: React.FC = () => {
4 | return (
5 |
12 |
16 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/app/features/categories/updateCategoryVote.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 |
4 | type CategoryVoteData = {
5 | data: {
6 | collection1Id: number;
7 | collection2Id: number;
8 | pickedId: number;
9 | };
10 | };
11 |
12 | export const updateCategoryVote = ({ data }: CategoryVoteData) => {
13 | return axios.post('flow/collections/vote', data);
14 | };
15 |
16 | export const useUpdateCategoryVote = () => {
17 | const queryClient = useQueryClient();
18 |
19 | return useMutation({
20 | mutationFn: updateCategoryVote,
21 | onSuccess: ({ data }) => {
22 | queryClient.refetchQueries({
23 | queryKey: ['category-pairs'],
24 | });
25 | },
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/public/images/icons/SuccessBoxIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const SuccessBoxIcon: React.FC = () => {
4 | return (
5 |
12 |
16 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/app/components/MinimumIncludedProjectsModal.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@/app/components/Button';
2 | import Modal from '@/app/components/Modal';
3 | import React from 'react';
4 |
5 | interface Props {
6 | isOpen: boolean;
7 | close: () => void;
8 | minimum: number;
9 | }
10 |
11 | export const MinimumIncludedProjectsModal: React.FC = ({
12 | isOpen,
13 | close,
14 | minimum,
15 | }) => {
16 | return (
17 |
18 |
19 |
{`You must keep at least ${minimum} projects to proceed.`}
20 |
21 | Go back
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/app/features/categories/getCategoryRankings.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { useQuery } from '@tanstack/react-query';
3 | import { IProjectsRankingResponse } from './getProjectsRankingByCategoryId';
4 | import { ICategory } from '@/app/categories/types';
5 |
6 | interface ICategoryRankingResponse
7 | extends Omit {
8 | ranking: ICategory[];
9 | }
10 |
11 | export const getCategoryRankings =
12 | async (): Promise => {
13 | const res = await axios.get(`flow/ranking
14 | `);
15 |
16 | return res.data;
17 | };
18 |
19 | export const useCategoryRankings = () => {
20 | return useQuery({
21 | queryKey: ['category-ranking'],
22 | queryFn: () => getCategoryRankings(),
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/src/app/features/categories/getPairwisePairs.ts:
--------------------------------------------------------------------------------
1 | import { IProject } from '@/app/categories/types';
2 | import { axios } from '@/lib/axios';
3 | import { useQuery } from '@tanstack/react-query';
4 | import { AxiosResponse } from 'axios';
5 |
6 | export interface IPairwisePairsResponse {
7 | pairs: IProject[][];
8 | totalPairs: number;
9 | votedPairs: number;
10 | name: string;
11 | threshold: number;
12 | }
13 |
14 | export const getPairwisePairs = async (
15 | cid: number,
16 | ): Promise> => {
17 | return axios.get(`flow/pairs?cid=${cid}`);
18 | };
19 |
20 | export const useGetPairwisePairs = (cid: number) => {
21 | return useQuery({
22 | queryKey: ['pairwise-pairs', cid],
23 | queryFn: () => getPairwisePairs(cid),
24 | });
25 | };
26 |
--------------------------------------------------------------------------------
/src/lib/third-web/constants.ts:
--------------------------------------------------------------------------------
1 | import { optimism, optimismSepolia } from 'thirdweb/chains';
2 |
3 | const getActiveChain = (chain?: string) => {
4 | switch (chain) {
5 | case 'optimism':
6 | return optimism;
7 | case 'optimism-sepolia':
8 | return optimismSepolia;
9 | default:
10 | return optimismSepolia;
11 | }
12 | };
13 |
14 | export const activeChain = getActiveChain(
15 | process.env.NEXT_PUBLIC_THIRDWEB_ACTIVE_CHAIN,
16 | );
17 | export const factoryAddress =
18 | process.env.NEXT_PUBLIC_THIRDWEB_FACTORY_ADDRESS ||
19 | '0xE424DC62723a40FCE052c5300699C28A3bD7cc01';
20 | export const clientId =
21 | process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID ||
22 | 'ab996cc033833508e203e80eecca234f';
23 | export const LAST_CONNECT_PERSONAL_WALLET_ID =
24 | 'last-connect-personal-wallet-id';
25 |
--------------------------------------------------------------------------------
/src/app/features/categories/updateCategoryMarkFiltered.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 |
4 | type CategoryMarkFilteredData = {
5 | data: {
6 | cid: number; //Category ID
7 | };
8 | };
9 |
10 | export const updateCategoryMarkFiltered = ({
11 | data,
12 | }: CategoryMarkFilteredData) => {
13 | return axios.post('/flow/mark-filtered', data);
14 | };
15 |
16 | export const useUpdateCategoryMarkFiltered = ({
17 | categoryId,
18 | }: {
19 | categoryId: number;
20 | }) => {
21 | const queryClient = useQueryClient();
22 |
23 | return useMutation({
24 | mutationFn: updateCategoryMarkFiltered,
25 | onSuccess: () => {
26 | queryClient.refetchQueries({
27 | queryKey: ['category', categoryId],
28 | });
29 | },
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/public/images/icons/iconOTP.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const OtpIcon = () => {
4 | return (
5 |
12 |
19 |
20 | );
21 | };
22 |
23 | export default OtpIcon;
24 |
--------------------------------------------------------------------------------
/src/app/welcome/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { ReactNode } from 'react';
4 | import Button from '../components/Button';
5 | import { useRouter } from 'next/navigation';
6 | import { Routes } from '../constants/Routes';
7 |
8 | const WelcomeLayout = ({ children }: { children: ReactNode }) => {
9 | const router = useRouter();
10 | return (
11 |
12 |
13 | {children}
14 |
15 |
16 |
17 | router.push(Routes.Intro)}
20 | >
21 | Next
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default WelcomeLayout;
29 |
--------------------------------------------------------------------------------
/src/app/features/categories/updateProjectInclusion.ts:
--------------------------------------------------------------------------------
1 | import { InclusionState } from '@/app/categories/types';
2 | import { axios } from '@/lib/axios';
3 | import { useMutation, useQueryClient } from '@tanstack/react-query';
4 |
5 | type ProjectInclusionData = {
6 | data: {
7 | state: InclusionState;
8 | id: number; //project id
9 | };
10 | };
11 |
12 | export const updateProjectInclusion = ({ data }: ProjectInclusionData) => {
13 | return axios.post('/flow/projects/set-inclusion', data);
14 | };
15 |
16 | export const useUpdateProjectInclusion = ({
17 | categoryId,
18 | }: {
19 | categoryId: number;
20 | }) => {
21 | const queryClient = useQueryClient();
22 |
23 | return useMutation({
24 | mutationFn: updateProjectInclusion,
25 | onSuccess: () => {
26 | queryClient.refetchQueries({
27 | queryKey: ['projects', categoryId],
28 | });
29 | },
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/src/app/intro/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Button from '@/app/components/Button';
4 | import { useRouter } from 'next/navigation';
5 | import React, { ReactNode } from 'react';
6 | import { Routes } from '../constants/Routes';
7 |
8 | const IntroLayout = ({ children }: { children: ReactNode }) => {
9 | const router = useRouter();
10 | return (
11 |
12 |
13 | {children}
14 |
15 |
16 |
17 | router.push(Routes.Categories)}
19 | className='w-full bg-primary'
20 | >
21 | Let’s go!
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default IntroLayout;
29 |
--------------------------------------------------------------------------------
/public/images/icons/IconMove.tsx:
--------------------------------------------------------------------------------
1 | const IconMove = () => {
2 | return (
3 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default IconMove;
29 |
--------------------------------------------------------------------------------
/public/images/icons/Edit2.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Edit2: React.FC = () => {
4 | return (
5 |
12 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/app/connect/otp/no-badge/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | const ConnectNoBadgePage = () => {
4 | return (
5 |
6 |
7 |
14 |
15 |
16 |
17 | No Badges Found!
18 |
19 |
20 | Looks like you don’t have any badges to Collect Voting
21 | Power.
22 |
23 |
24 | Try connecting with a different wallet instead?
25 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default ConnectNoBadgePage;
32 |
--------------------------------------------------------------------------------
/src/app/providers/TanstackProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
5 | import { ReactNode, useEffect } from 'react';
6 |
7 | const TanstackProvider = ({ children }: { children: ReactNode }) => {
8 | const queryClient = new QueryClient();
9 |
10 | useEffect(() => {
11 | const handleFocus = () => {
12 | queryClient.invalidateQueries({ queryKey: ['badges'] });
13 | };
14 |
15 | window.addEventListener('focus', handleFocus);
16 |
17 | return () => {
18 | window.removeEventListener('focus', handleFocus);
19 | };
20 | }, []);
21 |
22 | return (
23 |
24 | {children}
25 |
26 |
27 | );
28 | };
29 |
30 | export default TanstackProvider;
31 |
--------------------------------------------------------------------------------
/src/app/features/categories/getProjectsRankingByCategoryId.ts:
--------------------------------------------------------------------------------
1 | import { IProject } from '@/app/categories/types';
2 | import { axios } from '@/lib/axios';
3 | import { useQuery } from '@tanstack/react-query';
4 | import { AxiosResponse } from 'axios';
5 |
6 | export interface IProjectsRankingResponse {
7 | ranking: IProject[];
8 | hasRanking: boolean;
9 | isFinished: boolean;
10 | progress: string;
11 | name: string;
12 | share: number;
13 | id: number;
14 | }
15 |
16 | export const getProjectsRankingByCategoryId = async (
17 | cid: number,
18 | ): Promise> => {
19 | return axios.get(`flow/ranking?cid=${cid}
20 | `);
21 | };
22 |
23 | export const useProjectsRankingByCategoryId = (cid: number) => {
24 | return useQuery({
25 | queryKey: ['projects-ranking', cid],
26 | queryFn: () => getProjectsRankingByCategoryId(cid),
27 | staleTime: Infinity,
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | const config: Config = {
4 | content: [
5 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
7 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 | 'gradient-conic':
14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 | },
16 | maxWidth: {
17 | mobile: '500px', // Adding the custom size variable 'mobile'
18 | },
19 | colors: {
20 | primary: '#FF0420',
21 | ph: '#636779',
22 | fg_disabled: '#98A2B3',
23 | bg_disabled: '#F2F4F7',
24 | success: '#75E0A7',
25 | },
26 | screens: {
27 | xxs: '250px',
28 | xs: '376px',
29 | },
30 | },
31 | },
32 | plugins: [],
33 | };
34 | export default config;
35 |
--------------------------------------------------------------------------------
/src/app/providers/WagmiAppProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { ReactNode } from 'react';
4 | import { WagmiProvider } from 'wagmi';
5 | import { http, createConfig } from 'wagmi';
6 | import { mainnet, sepolia } from 'wagmi/chains';
7 | import { coinbaseWallet, walletConnect } from 'wagmi/connectors';
8 |
9 | export const projectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_ID!;
10 |
11 | const config = createConfig({
12 | chains: [mainnet, sepolia],
13 | connectors: [
14 | walletConnect({
15 | projectId,
16 | }),
17 | coinbaseWallet({
18 | appName: 'Pairwise',
19 | appLogoUrl: '/images/logo.png',
20 | }),
21 | ],
22 | transports: {
23 | [mainnet.id]: http(),
24 | [sepolia.id]: http(),
25 | },
26 | });
27 |
28 | const WagmiAppProvider = ({ children }: { children: ReactNode }) => {
29 | return {children} ;
30 | };
31 |
32 | export default WagmiAppProvider;
33 |
--------------------------------------------------------------------------------
/public/images/icons/IconTrash.tsx:
--------------------------------------------------------------------------------
1 | const IconTrash = ({ color = 'white' }: IIconProps) => {
2 | return (
3 |
10 |
17 |
18 | );
19 | };
20 |
21 | export default IconTrash;
22 |
--------------------------------------------------------------------------------
/public/images/icons/IconWallet.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconWallet = () => {
4 | return (
5 |
12 |
19 |
20 | );
21 | };
22 |
23 | export default IconWallet;
24 |
--------------------------------------------------------------------------------
/src/app/features/categories/updateProjectInclusionBulk.ts:
--------------------------------------------------------------------------------
1 | import { InclusionState } from '@/app/categories/types';
2 | import { axios } from '@/lib/axios';
3 | import { useMutation, useQueryClient } from '@tanstack/react-query';
4 |
5 | type ProjectInclusionBulkData = {
6 | data: {
7 | collectionId: number;
8 | state: InclusionState;
9 | ids: number[]; //project id
10 | };
11 | };
12 |
13 | export const updateProjectInclusionBulk = ({
14 | data,
15 | }: ProjectInclusionBulkData) => {
16 | return axios.post('/flow/projects/set-inclusion-bulk', data);
17 | };
18 |
19 | export const useUpdateProjectInclusionBulk = ({
20 | categoryId,
21 | }: {
22 | categoryId: number;
23 | }) => {
24 | const queryClient = useQueryClient();
25 |
26 | return useMutation({
27 | mutationFn: updateProjectInclusionBulk,
28 | onSuccess: () => {
29 | queryClient.refetchQueries({
30 | queryKey: ['projects', categoryId],
31 | });
32 | },
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/src/lib/react-query.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DefaultOptions,
3 | QueryClient,
4 | UseQueryOptions,
5 | UseMutationOptions,
6 | } from '@tanstack/react-query';
7 | import { AxiosError } from 'axios';
8 |
9 | import { PromiseValue } from 'type-fest';
10 |
11 | const queryConfig: DefaultOptions = {
12 | queries: {
13 | refetchOnWindowFocus: false,
14 | retry: false,
15 | },
16 | };
17 |
18 | export const queryClient = new QueryClient({ defaultOptions: queryConfig });
19 |
20 | export type ExtractFnReturnType any> =
21 | PromiseValue>;
22 |
23 | export type QueryConfig any> = Omit<
24 | UseQueryOptions>,
25 | 'queryKey' | 'queryFn'
26 | >;
27 |
28 | export type MutationConfig any> =
29 | UseMutationOptions<
30 | ExtractFnReturnType,
31 | AxiosError,
32 | Parameters[0]
33 | >;
34 |
--------------------------------------------------------------------------------
/public/images/icons/IconX.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconX = () => {
4 | return (
5 |
12 |
13 |
19 |
20 |
21 |
22 |
28 |
29 |
30 |
31 | );
32 | };
33 |
34 | export default IconX;
35 |
--------------------------------------------------------------------------------
/src/app/features/categories/updateSortingByCategoryId.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 |
4 | type ProjectVoteData = {
5 | data: {
6 | collectionId: number;
7 | projectIds: number[];
8 | };
9 | };
10 |
11 | export const updateSortingByCategoryId = ({ data }: ProjectVoteData) => {
12 | return axios.post('flow/dnd', data);
13 | };
14 |
15 | export const useUpdateSortingByCategoryId = ({
16 | categoryId,
17 | }: {
18 | categoryId: number;
19 | }) => {
20 | const queryClient = useQueryClient();
21 |
22 | return useMutation({
23 | mutationFn: updateSortingByCategoryId,
24 | onSuccess: () => {
25 | // Flatten the array of query keys
26 | const queryKeys = [
27 | ['category', categoryId],
28 | ['projects-ranking', categoryId],
29 | ['projects', categoryId],
30 | ];
31 |
32 | queryKeys.forEach(queryKey => {
33 | queryClient.refetchQueries({ queryKey });
34 | });
35 | },
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryRewardBanner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const CategoryRewaredBanner: React.FC = () => {
4 | return (
5 |
6 |
7 |
8 |
9 | You've got a chance to claim OP! Click "Check
10 | Eligibility" to see if you're eligible.
11 | Don't forget to review your badge group and check
12 | which raffles you can enter.
13 |
14 |
15 |
16 |
17 | Check Elegibility
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default CategoryRewaredBanner;
26 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryProjectItem.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { IProject } from '../types';
3 |
4 | interface ICategoriesProjectItemProps {
5 | project: IProject;
6 | }
7 |
8 | const CategoryProjectItem = ({ project }: ICategoriesProjectItemProps) => {
9 | return (
10 |
11 |
12 | {project.image ? (
13 |
20 | ) : (
21 |
22 |
23 | {project.name}
24 |
25 |
26 | )}
27 |
{project.name}
28 |
29 |
30 | );
31 | };
32 |
33 | export default CategoryProjectItem;
34 |
--------------------------------------------------------------------------------
/src/app/category-ranking/components/CategoryPairwiseModal.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@/app/components/Button';
2 | import Modal from '@/app/components/Modal';
3 | import React from 'react';
4 |
5 | interface Props {
6 | isOpen: boolean;
7 | close: () => void;
8 | handleSubmit: () => void;
9 | }
10 |
11 | export const CategoryPairwiseModal: React.FC = ({
12 | isOpen,
13 | close,
14 | handleSubmit,
15 | }) => {
16 | return (
17 |
18 |
19 |
20 | Category Voting
21 |
22 |
23 | Congratulations! You've successfully voted in at least
24 | two categories. Now, it's time to vote for the
25 | categories that overall had more impact!
26 |
27 |
31 | Start Category Voting
32 |
33 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: '[BUG] '
5 | labels: bug
6 | assignees: MoeNick
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 |
28 | - OS: [e.g. iOS]
29 | - Browser [e.g. chrome, safari]
30 | - Version [e.g. 22]
31 |
32 | **Smartphone (please complete the following information):**
33 |
34 | - Device: [e.g. iPhone6]
35 | - OS: [e.g. iOS8.1]
36 | - Browser [e.g. stock browser, safari]
37 | - Version [e.g. 22]
38 |
39 | **Additional context**
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/public/images/icons/ListIcon.tsx:
--------------------------------------------------------------------------------
1 | const ListIcon = () => {
2 | return (
3 |
10 |
17 |
18 | );
19 | };
20 |
21 | export default ListIcon;
22 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | color: rgb(var(--foreground-rgb));
7 | background: linear-gradient(
8 | to bottom,
9 | transparent,
10 | rgb(var(--background-end-rgb))
11 | )
12 | rgb(var(--background-start-rgb));
13 | }
14 |
15 | @layer utilities {
16 | .text-balance {
17 | text-wrap: balance;
18 | }
19 | }
20 |
21 | @layer components {
22 | .centered-mobile-max-width {
23 | @apply mx-auto max-w-mobile;
24 | }
25 | }
26 |
27 | input[type='number']::-webkit-inner-spin-button,
28 | input[type='number']::-webkit-outer-spin-button {
29 | -webkit-appearance: none;
30 | margin: 0;
31 | }
32 |
33 | input[type='number'] {
34 | -moz-appearance: textfield; /* Firefox */
35 | }
36 |
37 | /* Hide scrollbar for Chrome, Safari and Opera */
38 | *::-webkit-scrollbar {
39 | display: none;
40 | }
41 |
42 | /* Hide scrollbar for Internet Explorer, Edge, and Firefox */
43 | * {
44 | -ms-overflow-style: none; /* IE and Edge */
45 | scrollbar-width: none; /* Firefox */
46 | }
47 |
--------------------------------------------------------------------------------
/public/images/icons/IconLogout.tsx:
--------------------------------------------------------------------------------
1 | const IconLogout = () => {
2 | return (
3 |
10 |
17 |
18 | );
19 | };
20 |
21 | export default IconLogout;
22 |
--------------------------------------------------------------------------------
/src/app/hooks/useCopyToClipboard.ts:
--------------------------------------------------------------------------------
1 | import { useState, useCallback } from 'react';
2 |
3 | // Defines the return type of the hook: a tuple containing a string or null, and a function
4 | type UseCopyToClipboard = [
5 | copiedText: string | null,
6 | copy: (text: string) => Promise,
7 | ];
8 |
9 | const useCopyToClipboard = (): UseCopyToClipboard => {
10 | const [copiedText, setCopiedText] = useState(null);
11 |
12 | const copy = useCallback(async (text: string): Promise => {
13 | if (navigator.clipboard) {
14 | // Check if the clipboard API is available
15 | try {
16 | await navigator.clipboard.writeText(text);
17 | setCopiedText(text); // Update the state to the last copied text
18 | return true;
19 | } catch (error) {
20 | console.error('Failed to copy: ', error);
21 | setCopiedText(null);
22 | return false;
23 | }
24 | } else {
25 | console.warn('Clipboard not available');
26 | return false;
27 | }
28 | }, []);
29 |
30 | return [copiedText, copy];
31 | };
32 |
33 | export default useCopyToClipboard;
34 |
--------------------------------------------------------------------------------
/src/app/components/Modal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { FC, ReactNode, useEffect } from 'react';
4 | import ReactDOM from 'react-dom';
5 |
6 | interface ModalProps {
7 | isOpen: boolean;
8 | onClose: () => void;
9 | children: ReactNode;
10 | }
11 |
12 | const Modal: FC = ({ isOpen, onClose, children }) => {
13 | const modalNode = document.getElementById('modal-root');
14 |
15 | useEffect(() => {
16 | // Optional: Handle escape key press to close modal
17 | const handleKeyUp = (e: KeyboardEvent) => {
18 | if (e.key === 'Escape') {
19 | onClose();
20 | }
21 | };
22 | window.addEventListener('keyup', handleKeyUp);
23 | return () => window.removeEventListener('keyup', handleKeyUp);
24 | }, [onClose]);
25 |
26 | if (!isOpen || !modalNode) return null;
27 |
28 | return ReactDOM.createPortal(
29 |
30 |
31 | {children}
32 |
33 |
,
34 | modalNode,
35 | );
36 | };
37 |
38 | export default Modal;
39 |
--------------------------------------------------------------------------------
/src/lib/third-web/methods.ts:
--------------------------------------------------------------------------------
1 | import { Account, inAppWallet, smartWallet } from 'thirdweb/wallets';
2 | import { LAST_CONNECT_PERSONAL_WALLET_ID } from './constants';
3 | import { client, smartWalletConfig } from './provider';
4 |
5 | export const createEmailEoa = async (
6 | email: string,
7 | verificationCode: string,
8 | ) => {
9 | const wallet = inAppWallet();
10 | await wallet.connect({
11 | client,
12 | strategy: 'email',
13 | email,
14 | verificationCode,
15 | });
16 | localStorage.setItem(LAST_CONNECT_PERSONAL_WALLET_ID, wallet.id);
17 | return wallet;
18 | };
19 |
20 | export const createSocialEoa = async (strategy: 'google' | 'apple') => {
21 | const socialEOA = inAppWallet();
22 | await socialEOA.connect({
23 | client,
24 | strategy,
25 | });
26 | localStorage.setItem(LAST_CONNECT_PERSONAL_WALLET_ID, socialEOA.id);
27 | return socialEOA;
28 | };
29 |
30 | export const createSmartWalletFromEOA = async (eoa: Account) => {
31 | const wallet = smartWallet(smartWalletConfig);
32 | await wallet.connect({
33 | personalAccount: eoa,
34 | client,
35 | });
36 |
37 | return wallet;
38 | };
39 |
--------------------------------------------------------------------------------
/src/app/intro/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CircleNumber from '../components/CircleNumber';
3 |
4 | const IntroPage = () => {
5 | return (
6 |
7 |
Easy as 1-2-3
8 |
9 |
10 |
11 |
12 | Select a category to start ranking projects
13 |
14 |
15 |
16 |
17 |
18 | Discover projects and filter through them easily
19 |
20 |
21 |
22 |
23 |
24 | Finally, rank them to submit your vote!
25 |
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default IntroPage;
33 |
--------------------------------------------------------------------------------
/public/images/icons/IconCopy.tsx:
--------------------------------------------------------------------------------
1 | const IconCopy = () => {
2 | return (
3 |
10 |
17 |
18 | );
19 | };
20 |
21 | export default IconCopy;
22 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryRankingItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Image from 'next/image'; // Make sure to install 'next/image'
4 | import { ICategory } from '../types';
5 | import CategoryBadge from './CategoryBadge';
6 | import { truncate } from '@/app/helpers/text-helpers';
7 |
8 | interface ICategoryProps {
9 | category: ICategory;
10 | }
11 |
12 | const CategoryRankingItem = ({ category }: ICategoryProps) => {
13 | return (
14 |
15 |
26 |
27 |
{category.name}
28 |
29 | {truncate(category.impactDescription, 55)}
30 |
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default CategoryRankingItem;
38 |
--------------------------------------------------------------------------------
/src/app/connect/components/ConnectSplashMessage.tsx:
--------------------------------------------------------------------------------
1 | import { isMobile } from 'react-device-detect';
2 | import { useEffect, useState } from 'react';
3 |
4 | const ConnectSplashMessage = () => {
5 | const [open, setOpen] = useState(false);
6 |
7 | useEffect(() => {
8 | setOpen(isMobile);
9 | }, []);
10 |
11 | return (
12 |
13 | {open && (
14 |
15 |
16 |
17 |
18 | Go to the device with your OP account and
19 | connect it pseudonymously to this device
20 |
21 |
22 | setOpen(false)}
25 | >
26 | OK
27 |
28 |
29 |
30 |
31 |
32 | )}
33 |
34 | );
35 | };
36 |
37 | export default ConnectSplashMessage;
38 |
--------------------------------------------------------------------------------
/src/app/api/rephrase/route.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { NextResponse } from 'next/server';
3 |
4 | export async function GET(request: Request) {
5 | const { searchParams } = new URL(request.url);
6 | const comment = searchParams.get('comment');
7 | try {
8 | const response = await axios.post(
9 | 'https://api.openai.com/v1/chat/completions',
10 | {
11 | model: 'gpt-4o-mini',
12 | messages: [
13 | {
14 | role: 'system',
15 | content:
16 | 'You will be provided with a statement, and your task is to rephrase it.',
17 | },
18 | {
19 | role: 'user',
20 | content: comment,
21 | },
22 | ],
23 | temperature: 1,
24 | max_tokens: 64,
25 | top_p: 1,
26 | },
27 | {
28 | headers: {
29 | 'Content-Type': 'application/json',
30 | Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
31 | },
32 | },
33 | );
34 |
35 | const rephrasedText = response.data.choices[0].message.content;
36 | return NextResponse.json({ rephrasedText });
37 | } catch (error) {
38 | console.error('Error rephrasing text:', error);
39 | return NextResponse.json({ error });
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/app/categories/types.ts:
--------------------------------------------------------------------------------
1 | export interface ICategory {
2 | id: number;
3 | name: string;
4 | poll_id: number;
5 | url: string;
6 | impactDescription: string;
7 | contributionDescription: null | string;
8 | RPGF4Id: null | number;
9 | parentId: null | number;
10 | image: string | null;
11 | metadataUrl: null | string;
12 | created_at: string;
13 | type: string;
14 | progress: CollectionProgressStatus;
15 | }
16 |
17 | export enum InclusionState {
18 | Included = 'included',
19 | Excluded = 'excluded',
20 | Pending = 'pending',
21 | }
22 |
23 | export interface IProject {
24 | id: number;
25 | name: string;
26 | poll_id: number;
27 | url: string;
28 | impactDescription: string;
29 | shortDescription: string | null;
30 | contributionDescription: string | null;
31 | RPGF4Id: string;
32 | parentId: number | null;
33 | image: string | null;
34 | metadataUrl: string | null;
35 | created_at: string;
36 | type: 'collection' | 'project';
37 | inclusionState: InclusionState;
38 | }
39 |
40 | export type CollectionProgressStatus =
41 | | 'Attested'
42 | | 'Finished'
43 | | 'WIP - Threshold'
44 | | 'WIP'
45 | | 'Filtered'
46 | | 'Filtering'
47 | | 'Pending';
48 |
--------------------------------------------------------------------------------
/public/images/icons/IconEye.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconEye = () => {
4 | return (
5 |
12 |
19 |
26 |
27 | );
28 | };
29 |
30 | export default IconEye;
31 |
--------------------------------------------------------------------------------
/src/app/components/ConnectDrawers.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useConnect } from '../providers/ConnectProvider';
5 | import CollectVotingPowerContent from './CollectVotingPowerContent';
6 | import ConnectWalletContent from './ConnectWalletContent';
7 | import Drawer from './Drawer';
8 |
9 | const ConnectDrawers = () => {
10 | const {
11 | handleConnect,
12 | isClaimDrawerOpen,
13 | setIsClaimDrawerOpen,
14 | isConnectDrawerOpen,
15 | setIsConnectDrawerOpen,
16 | } = useConnect();
17 | return (
18 |
19 |
23 | setIsConnectDrawerOpen(false)}
26 | />
27 |
36 |
37 |
38 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default ConnectDrawers;
47 |
--------------------------------------------------------------------------------
/src/app/components/TopRouteIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Routes } from '@/app/constants/Routes';
3 | import Link from 'next/link';
4 | import IconArrowLeft from 'public/images/icons/IconArrowLeft';
5 | import IconCancel from 'public/images/icons/IconCancel';
6 | import { useParams } from 'next/navigation';
7 |
8 | interface ITopRouteIndicatorProps {
9 | name?: string;
10 | icon: 'cross' | 'arrow';
11 | }
12 |
13 | const iconMap = {
14 | cross: ,
15 | arrow: ,
16 | };
17 |
18 | const TopRouteIndicator = ({ name = '', icon }: ITopRouteIndicatorProps) => {
19 | const { categoryId } = useParams();
20 | return (
21 |
22 |
23 | {icon === 'arrow' ? (
24 |
28 | {iconMap[icon]}
29 |
30 | ) : (
31 | ''
32 | )}
33 |
{name}
34 |
35 | {icon === 'cross' ? iconMap[icon] : ''}
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default TopRouteIndicator;
43 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryToggleButton.tsx:
--------------------------------------------------------------------------------
1 | import CardIcon from 'public/images/icons/CardIcon';
2 | import ListIcon from 'public/images/icons/ListIcon';
3 | import React, { useState } from 'react';
4 | interface CategoryToggleButtonProps {
5 | toggleView: (isCardView: boolean) => void;
6 | }
7 |
8 | const CategoryToggleButton: React.FC = ({
9 | toggleView,
10 | }) => {
11 | // const [isToggled, setIsToggled] = useState(true);
12 | const [isToggled, setIsToggled] = useState(() => {
13 | const savedState = localStorage.getItem('isCardView');
14 | return savedState !== null ? JSON.parse(savedState) : false;
15 | });
16 |
17 | const toggleButton = () => {
18 | setIsToggled(!isToggled);
19 | toggleView(!isToggled);
20 | };
21 | return (
22 |
23 |
27 |
32 | {isToggled ? : }
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default CategoryToggleButton;
40 |
--------------------------------------------------------------------------------
/src/app/connect/otp/success/page.tsx:
--------------------------------------------------------------------------------
1 | import { badgesImages } from '@/app/constants/BadgesData';
2 | import Image from 'next/image';
3 |
4 | const ConnectOTPSuccessPage = () => {
5 | return (
6 |
7 |
8 |
15 |
16 |
17 |
18 | You successfully collected your voting power
19 |
20 |
21 | {badgesImages.map((image, index) => (
22 |
0 ? '-ml-9' : 'ml-0'} rounded-full p-2`}
25 | >
26 |
27 |
33 |
34 |
35 | ))}
36 |
37 |
38 | Pseudonymously
39 |
40 | {/* zk-proof messaging removed */}
41 |
42 |
43 | );
44 | };
45 |
46 | export default ConnectOTPSuccessPage;
47 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryCard.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ICategoryProps } from './CategoryItem';
3 | import Image from 'next/image';
4 | import CategoryBadge from './CategoryBadge';
5 | import { truncate } from '@/app/helpers/text-helpers';
6 |
7 | const CategoryCard = ({ category, progress, imageNumber }: ICategoryProps) => {
8 | const imgNumber = imageNumber || (category.id % 5) + 1;
9 | const imgSrc = `/images/defaults/category/category-${imgNumber}.png`;
10 |
11 | return (
12 |
13 |
14 |
15 |
21 |
22 | {category.name}
23 |
24 |
25 |
26 |
27 |
{' '}
28 |
{category.name}
29 |
30 | {truncate(category.impactDescription, 50)}
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default CategoryCard;
38 |
--------------------------------------------------------------------------------
/src/app/category-ranking/done/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import Button from '@/app/components/Button';
5 | import { Routes } from '@/app/constants/Routes';
6 | import { useRouter } from 'next/navigation';
7 | import IconCheck from 'public/images/icons/IconCheck';
8 |
9 | const CategoryRankingDone = () => {
10 | const router = useRouter();
11 |
12 | return (
13 |
14 |
15 |
16 |
17 |
{' '}
18 |
Hooray!!
19 |
20 | You have successfully submitted your vote.
21 |
22 |
23 | Now you can go back and vote on another
24 | category.
25 |
26 |
27 |
28 | router.push(`${Routes.Categories}`)}
30 | className='w-full bg-primary'
31 | >
32 | Done
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default CategoryRankingDone;
40 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryRankingBasicListItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { IProject } from '../types';
4 | import Image from 'next/image';
5 | import { truncate } from '@/app/helpers/text-helpers';
6 |
7 | interface ICategoryRankingBasicListItemProps {
8 | project: IProject;
9 | order: number;
10 | }
11 |
12 | const CategoryRankingBasicListItem = ({
13 | project,
14 | order,
15 | }: ICategoryRankingBasicListItemProps) => {
16 | return (
17 |
18 |
19 |
#{order}
20 |
21 | {project.image ? (
22 |
29 | ) : (
30 |
31 |
32 | {project.name}
33 |
34 |
35 | )}
36 |
37 | {truncate(project.name, 25)}
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default CategoryRankingBasicListItem;
46 |
--------------------------------------------------------------------------------
/src/app/features/categories/updateProjectVote.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { useMutation, useQueryClient } from '@tanstack/react-query';
3 |
4 | type ProjectVoteData = {
5 | data: {
6 | project1Id: number;
7 | project2Id: number;
8 | pickedId: number | null;
9 | };
10 | };
11 |
12 | export const updateProjectVote = ({ data }: ProjectVoteData) => {
13 | return axios.post('flow/projects/vote', data);
14 | };
15 |
16 | export const updateProjectUndo = (cid: Number) => {
17 | return axios.post('flow/pairs/back', { collectionId: cid });
18 | };
19 |
20 | export const useUpdateProjectVote = ({
21 | categoryId,
22 | }: {
23 | categoryId: number;
24 | }) => {
25 | const queryClient = useQueryClient();
26 |
27 | return useMutation({
28 | mutationFn: updateProjectVote,
29 | onSuccess: ({ data }) => {
30 | console.log('OnSuccess', data);
31 | queryClient.refetchQueries({
32 | queryKey: ['pairwise-pairs', categoryId],
33 | });
34 | },
35 | });
36 | };
37 |
38 | export const useUpdateProjectUndo = ({
39 | categoryId,
40 | }: {
41 | categoryId: number;
42 | }) => {
43 | const queryClient = useQueryClient();
44 |
45 | return useMutation({
46 | mutationFn: updateProjectUndo,
47 | onSuccess: ({ data }) => {
48 | console.log('OnSuccess', data);
49 | queryClient.refetchQueries({
50 | queryKey: ['pairwise-pairs', categoryId],
51 | });
52 | },
53 | });
54 | };
55 |
--------------------------------------------------------------------------------
/src/app/categories/components/DrawerContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IProject } from '../types';
3 |
4 | interface DrawerContentProps {
5 | project: IProject;
6 | }
7 | export const DrawerContent: React.FC = ({ project }) => {
8 | return (
9 |
10 |
22 |
28 |
29 |
30 |
31 | {project.name}
32 |
33 |
{project.shortDescription}
34 |
35 |
36 |
37 |
38 | Impact statement
39 |
40 |
{project.impactDescription}
41 |
42 |
43 |
44 |
45 | Contributions
46 |
47 |
{project.contributionDescription}
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/app/connect/components/ConnectOtpInput.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/app/helpers/cn';
4 | import React, { FC } from 'react';
5 | import OTPInput from 'react-otp-input';
6 | import { ConnectErrorBox } from './ConnectErrorBox';
7 |
8 | export enum OtpState {
9 | InProgress,
10 | Ready,
11 | Invalid,
12 | Valid,
13 | }
14 | interface Props {
15 | onSubmit: () => void;
16 | otp: string;
17 | setOtp: (otp: string) => void;
18 | state: OtpState;
19 | setState: (state: OtpState) => void;
20 | setError: (error: string | false) => void;
21 | error?: string | false;
22 | }
23 |
24 | const OtpLength = 6;
25 |
26 | const ConnectOTPInput: FC = ({ otp, setOtp, state, error }) => {
27 | const handleOTPChange = (otp: string) => {
28 | setOtp(otp);
29 | };
30 |
31 | return (
32 |
33 | }
45 | shouldAutoFocus
46 | />
47 | {error && }
48 |
49 | );
50 | };
51 |
52 | export default ConnectOTPInput;
53 |
--------------------------------------------------------------------------------
/public/images/icons/IconParagraph.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconParagraph = () => {
4 | return (
5 |
12 |
16 |
17 | );
18 | };
19 |
20 | export default IconParagraph;
21 |
--------------------------------------------------------------------------------
/public/images/icons/WarningBoxIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const WarningBoxIcon: React.FC = () => {
4 | return (
5 |
12 |
16 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/public/images/icons/IconWarning.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconWarning = () => {
4 | return (
5 |
12 |
16 |
23 |
24 | );
25 | };
26 |
27 | export default IconWarning;
28 |
--------------------------------------------------------------------------------
/src/utils/AuthGuard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import LoadingSpinner from '@/app/components/LoadingSpinner';
4 | import { useAuth } from '@/lib/third-web/AutoConnect';
5 | import { usePathname, useRouter } from 'next/navigation';
6 | import React, { PropsWithChildren, useEffect, useState } from 'react';
7 | import { useActiveWallet } from 'thirdweb/react';
8 |
9 | const PublicRoutes = [
10 | '/login',
11 | '/connect',
12 | '/connect/otp',
13 | '/connect/otp/success',
14 | '/connect/otp/no-badge',
15 | ];
16 |
17 | export const AuthGuard: React.FC = ({ children }) => {
18 | const wallet = useActiveWallet();
19 | const currentRoute = usePathname();
20 | const { loggedToPw, isAutoConnecting } = useAuth();
21 | const { push } = useRouter();
22 |
23 | const [moveForward, setMoveForward] = useState(false);
24 |
25 | const isPublicRoute = PublicRoutes.includes(currentRoute);
26 |
27 | useEffect(() => {
28 | if (
29 | !wallet &&
30 | isAutoConnecting === false &&
31 | !isPublicRoute &&
32 | currentRoute !== '/login'
33 | )
34 | push('/login');
35 | }, [wallet, isAutoConnecting, push, isPublicRoute, currentRoute]);
36 |
37 | useEffect(() => {
38 | const token = localStorage.getItem('auth');
39 | const temp = wallet && token !== null;
40 | setMoveForward(temp || false);
41 | }, [moveForward, wallet, loggedToPw]);
42 |
43 | if (isPublicRoute) return <>{children}>;
44 | if (!moveForward)
45 | return (
46 | <>
47 |
48 | >
49 | );
50 |
51 | return <>{children}>;
52 | };
53 |
--------------------------------------------------------------------------------
/src/app/connect/components/ConnectFooter.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Routes } from '@/app/constants/Routes';
4 | import Link from 'next/link';
5 | import { useRouter } from 'next/navigation';
6 | import IconGithub from 'public/images/icons/IconGithub';
7 | import IconGM from 'public/images/icons/IconGM';
8 | import IconParagraph from 'public/images/icons/IconParagraph';
9 | import IconX from 'public/images/icons/IconX';
10 | import { PwLogo } from 'public/images/icons/PwLogo';
11 | import React from 'react';
12 |
13 | const ConnectFooter = () => {
14 | const router = useRouter();
15 | return (
16 |
42 | );
43 | };
44 |
45 | export default ConnectFooter;
46 |
--------------------------------------------------------------------------------
/src/app/badges/components/AdjacentBadges.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { BadgeData } from './BadgeCard';
3 |
4 | interface Props extends BadgeData {
5 | size: number;
6 | }
7 |
8 | export const AdjacentBadges: React.FC = ({
9 | badgeholderPoints,
10 | delegatePoints,
11 | holderPoints,
12 | recipientsPoints,
13 | size,
14 | }) => {
15 | const getBadgeImages = () => {
16 | const badgesImages = [];
17 | if (holderPoints && holderPoints > 0)
18 | badgesImages.push({
19 | src: '/images/badges/1.png',
20 | alt: 'holder badge',
21 | });
22 | if (delegatePoints && delegatePoints > 0)
23 | badgesImages.push({
24 | src: '/images/badges/2.png',
25 | alt: 'delegate badge',
26 | });
27 | if (badgeholderPoints === 1)
28 | badgesImages.push({
29 | src: '/images/badges/3.png',
30 | alt: 'badge-holder badge',
31 | });
32 | if (recipientsPoints === 1)
33 | badgesImages.push({
34 | src: '/images/badges/4.png',
35 | alt: 'recipient badge',
36 | });
37 |
38 | return badgesImages;
39 | };
40 | return (
41 |
42 | {getBadgeImages().map((image, index) => (
43 |
0 ? (size > 25 ? '-ml-10' : '-ml-7') : 'ml-0'} rounded-full p-2`}
46 | >
47 |
48 |
54 |
55 |
56 | ))}
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/public/images/icons/IconGithub.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const IconGithub = () => {
4 | return (
5 |
12 |
18 |
19 | );
20 | };
21 |
22 | export default IconGithub;
23 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryRankingNotSelectedListItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { IProject } from '../types';
4 | import Image from 'next/image';
5 |
6 | import { truncate } from '@/app/helpers/text-helpers';
7 |
8 | interface ICategoryRankingNotSelectedListItemProps {
9 | project: IProject;
10 | handleAddFromNotSelected: (projectId: number) => void;
11 | }
12 |
13 | const CategoryRankingNotSelectedListItem = ({
14 | project,
15 | handleAddFromNotSelected,
16 | }: ICategoryRankingNotSelectedListItemProps) => {
17 | return (
18 |
19 |
20 |
21 | {project.image ? (
22 |
29 | ) : (
30 |
31 |
32 | {project.name}
33 |
34 |
35 | )}
36 |
37 | {truncate(project.name, 30)}
38 |
39 |
40 |
41 |
handleAddFromNotSelected(project.id)}
44 | >
45 | +
46 |
47 |
48 | );
49 | };
50 |
51 | export default CategoryRankingNotSelectedListItem;
52 |
--------------------------------------------------------------------------------
/src/app/categories/[categoryId]/filter-guide/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Button from '@/app/components/Button';
4 | import { Routes } from '@/app/constants/Routes';
5 | import Image from 'next/image';
6 | import { useParams, useRouter } from 'next/navigation';
7 | import { useEffect } from 'react';
8 |
9 | const FilterGuidePage = () => {
10 | const router = useRouter();
11 | const { categoryId } = useParams();
12 |
13 | useEffect(() => {
14 | window.scrollTo(0, 0);
15 | }, []);
16 |
17 | return (
18 |
19 |
20 |
Project filtering
21 |
22 | Start by selecting projects you want to keep or dismiss from
23 | this category.
24 |
25 |
26 |
32 |
38 |
39 |
40 |
41 |
43 | router.push(
44 | `${Routes.Categories}/${categoryId}/project-ranking`,
45 | )
46 | }
47 | className='w-full bg-primary'
48 | >
49 | Got it!
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default FilterGuidePage;
57 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pairwise-rpgf4-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "eslint": "eslint . --ext .ts --ext .tsx",
11 | "eslint:fix": "eslint . --ext .ts --ext .tsx --fix",
12 | "format": "prettier --write ."
13 | },
14 | "dependencies": {
15 |
16 | "@ethereum-attestation-service/eas-sdk": "^2.1.4",
17 |
18 | "@tanstack/react-query": "^5.29.0",
19 | "@tanstack/react-query-devtools": "^5.29.0",
20 | "axios": "^1.6.8",
21 | "class-variance-authority": "^0.7.0",
22 | "clsx": "^2.1.0",
23 | "eslint-plugin-unused-imports": "^4.0.0",
24 | "framer-motion": "^11.1.7",
25 | "next": "14.1.4",
26 | "openai": "^4.52.7",
27 | "posthog-js": "^1.139.1",
28 | "react": "^18",
29 | "react-device-detect": "^2.2.3",
30 | "react-dom": "^18",
31 | "react-otp-input": "^3.1.1",
32 | "tailwind-merge": "^2.2.2",
33 | "thirdweb": "^5.3.1",
34 | "viem": "2.x",
35 | "wagmi": "^2.5.19"
36 | },
37 | "devDependencies": {
38 | "@next/eslint-plugin-next": "^14.1.4",
39 | "@types/node": "^20",
40 | "@types/react": "^18",
41 | "@types/react-dom": "^18",
42 | "@typescript-eslint/eslint-plugin": "^7.6.0",
43 | "autoprefixer": "^10.0.1",
44 | "eslint": "^8.57.0",
45 | "eslint-config-next": "14.1.4",
46 | "eslint-config-prettier": "^9.1.0",
47 | "eslint-plugin-prettier": "^5.1.3",
48 | "eslint-plugin-react": "^7.34.1",
49 | "postcss": "^8",
50 | "prettier": "^3.2.5",
51 | "prettier-plugin-tailwindcss": "^0.5.13",
52 | "tailwindcss": "^3.3.0",
53 | "typescript": "^5"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/app/components/VoteSubmitted.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import Button from '@/app/components/Button';
5 | import { Routes } from '@/app/constants/Routes';
6 | import { useRouter } from 'next/navigation';
7 | import IconCheck from 'public/images/icons/IconCheck';
8 | import backgroundGif from 'public/images/confetti.gif'; // Import your GIF file
9 | interface IVoteType {
10 | categoryId?: number;
11 | }
12 | const VoteSubmitted = ({ categoryId }: IVoteType) => {
13 | const router = useRouter();
14 |
15 | return (
16 |
22 |
23 |
24 |
25 |
{' '}
26 |
Vote submitted
27 |
28 | Wooo!!! Now you can go back and vote on another category
29 |
30 |
31 |
32 | {
34 | {
35 | categoryId
36 | ? router.push(
37 | `${Routes.Categories}/${categoryId}/pairwise-ranking/done`,
38 | )
39 | : router.push(`/category-ranking/done`);
40 | }
41 | }}
42 | className='w-full bg-primary'
43 | >
44 | Continue
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default VoteSubmitted;
52 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryEditProjectItem.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { IProject } from '../types';
3 | import IconTrash from 'public/images/icons/IconTrash';
4 |
5 | export enum SelectionState {
6 | SELECTED = 'selected',
7 | NOT_SELECTED = 'notSelected',
8 | }
9 |
10 | interface ICategoriesEditProjectItemProps {
11 | project: IProject;
12 | selectionState: SelectionState;
13 | handleEditProject: (projectId: number) => void;
14 | }
15 |
16 | const CategoryEditProjectItem = ({
17 | project,
18 | selectionState,
19 | handleEditProject,
20 | }: ICategoriesEditProjectItemProps) => {
21 | return (
22 |
23 |
24 |
25 | {project.image ? (
26 |
33 | ) : (
34 |
35 |
36 | {project.name}
37 |
38 |
39 | )}
40 |
{project.name}
41 |
42 |
handleEditProject(project.id)}
45 | >
46 | {selectionState === SelectionState.SELECTED ? (
47 |
48 | ) : (
49 |
+
50 | )}
51 |
52 |
53 |
54 | );
55 | };
56 |
57 | export default CategoryEditProjectItem;
58 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryBadge.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { CollectionProgressStatus } from '../types';
3 | import IconCheck from 'public/images/icons/IconCheck';
4 |
5 | const CategoryBadge = ({
6 | progress = 'Pending',
7 | }: {
8 | progress?: CollectionProgressStatus;
9 | }) => {
10 | switch (progress) {
11 | case 'Pending':
12 | return (
13 |
14 | Not ranked
15 |
16 | );
17 | case 'Filtering':
18 | return (
19 |
20 | Filtering
21 |
22 | );
23 | case 'Filtered':
24 | return (
25 |
26 | Filtered
27 |
28 | );
29 | case 'WIP - Threshold':
30 | case 'WIP':
31 | case 'Finished':
32 | return (
33 |
34 | Ranking
35 |
36 | );
37 | case 'Attested':
38 | return (
39 |
43 | );
44 |
45 | default:
46 | return (
47 |
48 | Not ranked
49 |
50 | );
51 | }
52 | };
53 |
54 | export default CategoryBadge;
55 |
--------------------------------------------------------------------------------
/src/app/categories/[categoryId]/pairwise-ranking/done/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import Button from '@/app/components/Button';
5 | import { Routes } from '@/app/constants/Routes';
6 | import { useParams, useRouter } from 'next/navigation';
7 | import IconCheck from 'public/images/icons/IconCheck';
8 |
9 | const CategoryRankingDone = () => {
10 | const router = useRouter();
11 | const { categoryId } = useParams();
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
{' '}
19 |
Vote submitted
20 |
21 | You have voted in this category.If you change your mind,you
22 | can update your vote by selecting “Edit Vote”
23 | button below.
24 |
25 |
26 |
27 |
29 | router.push(
30 | `${Routes.Categories}/${categoryId}/pairwise-ranking/ranking-list/edit`,
31 | )
32 | }
33 | className='mb-5 w-full border border-[#E0E2EB] bg-[#FBFCFE] font-semibold leading-5 text-black'
34 | >
35 | Edit Vote
36 |
37 | router.push(`${Routes.Categories}`)}
39 | className='w-full bg-primary'
40 | >
41 | Done
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default CategoryRankingDone;
49 |
--------------------------------------------------------------------------------
/src/app/badges/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import TopNavigation from '../components/TopNavigation';
5 | import { Routes } from '../constants/Routes';
6 | import BadgeCard, { BadgeData, badgeTypeMapping } from './components/BadgeCard';
7 | import LoadingSpinner from '../components/LoadingSpinner';
8 | import { useGetBadges } from '../features/badges/getBadges';
9 | import { getBadgeAmount, getBadgeMedal } from '@/utils/badgeUtils';
10 |
11 | export type BadgeCardEntryType = [
12 | key: keyof typeof badgeTypeMapping,
13 | value: number,
14 | ];
15 |
16 | const BadgesPage = () => {
17 | const { data: badges, isLoading } = useGetBadges();
18 |
19 | if (isLoading) return ;
20 |
21 | const badgeCards = ({
22 | delegateAmount,
23 | holderAmount,
24 | holderType,
25 | delegateType,
26 | ...rest
27 | }: BadgeData) => {
28 | return { ...rest };
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
Your Badges
36 |
37 | {badges ? (
38 | Object.entries(badgeCards(badges)).map(([el1, el2]) => {
39 | const [key, value] = [
40 | el1,
41 | el2,
42 | ] as BadgeCardEntryType;
43 | return (
44 |
51 | );
52 | })
53 | ) : (
54 |
No badges found for You
55 | )}
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default BadgesPage;
63 |
--------------------------------------------------------------------------------
/src/app/features/badges/getBadges.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2 | import { axios } from '@/lib/axios';
3 | import { BadgeData } from '@/app/badges/components/BadgeCard';
4 | // Removed semaphore Identity
5 |
6 | const continueGuest = async () => {
7 | await axios.post('/user/continue-guest');
8 | };
9 |
10 | /**
11 | *
12 | * @returns badges stored in the Pairwise database for a given smart wallet addresss
13 | */
14 | export const useContinueGuest = () => {
15 | const queryClient = useQueryClient();
16 |
17 | return useMutation({
18 | mutationFn: continueGuest,
19 | onSuccess: () => {
20 | queryClient.refetchQueries({
21 | queryKey: ['badges'],
22 | });
23 | },
24 | });
25 | };
26 |
27 | const getBadges = async () => {
28 | const { data } = await axios.get('/user/badges');
29 | return data;
30 | };
31 |
32 | /**
33 | *
34 | * @returns badges stored in the Pairwise database for a given smart wallet addresss
35 | */
36 | export const useGetBadges = () => {
37 | return useQuery({
38 | queryKey: ['badges'],
39 | queryFn: getBadges,
40 | refetchOnWindowFocus: 'always',
41 | });
42 | };
43 |
44 | // Identity API removed
45 |
46 | const getPublicBadges = async (address: string) => {
47 | const { data } = await axios.get('/user/public/badges', {
48 | params: {
49 | address,
50 | },
51 | });
52 |
53 | return data;
54 | };
55 |
56 | /**
57 | *
58 | * @param address wallet address
59 | * @returns badges associated with an address (not read from Pairwise backend servers)
60 | */
61 | export const useGetPublicBadges = (address: string) => {
62 | return useQuery({
63 | queryKey: ['publicBadges', address],
64 | queryFn: () => getPublicBadges(address),
65 | });
66 | };
67 |
--------------------------------------------------------------------------------
/src/app/categories/[categoryId]/pairwise-ranking/ranking-done/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import CategoryCard from '@/app/categories/components/CategoryCard';
4 | import Button from '@/app/components/Button';
5 | import LoadingSpinner from '@/app/components/LoadingSpinner';
6 | import { Routes } from '@/app/constants/Routes';
7 | import { useCategoryById } from '@/app/features/categories/getCategoryById';
8 | import { useParams, useRouter } from 'next/navigation';
9 |
10 | const RankingDonePage = () => {
11 | const router = useRouter();
12 | const { categoryId } = useParams();
13 | const selectedCategoryId =
14 | typeof categoryId === 'string' ? categoryId : categoryId[0];
15 |
16 | const { data, isLoading } = useCategoryById(+selectedCategoryId);
17 |
18 | if (isLoading) {
19 | return ;
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 |
Ranking done!
27 |
28 | Preview your project ranking and submit your vote
29 |
30 |
31 |
35 |
36 |
37 |
38 |
39 |
41 | router.push(
42 | `${Routes.Categories}/${selectedCategoryId}/pairwise-ranking/ranking-list`,
43 | )
44 | }
45 | className='w-full bg-primary'
46 | >
47 | Preview Ranking
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default RankingDonePage;
55 |
--------------------------------------------------------------------------------
/src/app/login/components/SignInEmail2.tsx:
--------------------------------------------------------------------------------
1 | // signin.tsx
2 | import { FC } from 'react';
3 | import { ErrorBox } from './ErrorBox';
4 |
5 | interface SignInForm {
6 | email: string;
7 | onSubmit: () => void;
8 | setEmail: (email: string) => void;
9 | emailError: boolean;
10 | }
11 |
12 | export const isEmailValid = (email: string) => {
13 | const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
14 | return regex.test(email);
15 | };
16 |
17 | export const SignInEmail2: FC = ({
18 | onSubmit,
19 | email,
20 | setEmail,
21 | emailError,
22 | }) => {
23 | const handleChange = (event: React.ChangeEvent) => {
24 | setEmail(event.target.value);
25 | };
26 |
27 | return (
28 |
29 |
30 | Sign in with Email
31 |
32 |
51 | {emailError && (
52 |
53 | {' '}
54 | {' '}
55 |
56 | )}
57 |
63 | Sign in
64 |
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Inter } from 'next/font/google';
2 | import './globals.css';
3 | import TanstackProvider from './providers/TanstackProvider';
4 | import WagmiAppProvider from './providers/WagmiAppProvider';
5 | import PHProvider from './providers/PostHogProvider';
6 | import './globals.css';
7 | import './globals.css';
8 | import { Thirdweb5Provider } from '@/lib/third-web/provider';
9 | import { AuthGuard } from '@/utils/AuthGuard';
10 | import { ConnectProvider } from './providers/ConnectProvider';
11 | import ConnectDrawers from './components/ConnectDrawers';
12 | import Head from 'next/head';
13 |
14 | const inter = Inter({ subsets: ['latin'] });
15 |
16 | export default function RootLayout({
17 | children,
18 | }: Readonly<{
19 | children: React.ReactNode;
20 | }>) {
21 | return (
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {children}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/providers/ConnectProvider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { createContext, ReactNode, useContext, useState } from 'react';
4 |
5 | // Define the shape of the context data
6 | interface ConnectContextType {
7 | isConnectDrawerOpen: boolean;
8 | setIsConnectDrawerOpen: (isOpen: boolean) => void;
9 | isClaimDrawerOpen: boolean;
10 | setIsClaimDrawerOpen: (isOpen: boolean) => void;
11 | isLogOutDrawerOpen: boolean;
12 | setIsLogOutDrawerOpen: (isOpen: boolean) => void;
13 | handleConnect: () => void;
14 | handleDisconnect: () => void;
15 | }
16 |
17 | // Create the context
18 | const ConnectContext = createContext(undefined);
19 |
20 | // Create a provider component
21 | export const ConnectProvider = ({ children }: { children: ReactNode }) => {
22 | const [isConnectDrawerOpen, setIsConnectDrawerOpen] = useState(false);
23 | const [isClaimDrawerOpen, setIsClaimDrawerOpen] = useState(false);
24 | const [isLogOutDrawerOpen, setIsLogOutDrawerOpen] = useState(false);
25 |
26 | const handleConnect = () => {
27 | setIsConnectDrawerOpen(false);
28 | setIsClaimDrawerOpen(true);
29 | setIsLogOutDrawerOpen(false);
30 | };
31 |
32 | const handleDisconnect = () => {
33 | setIsClaimDrawerOpen(false);
34 | setIsConnectDrawerOpen(true);
35 | };
36 |
37 | return (
38 |
50 | {children}
51 |
52 | );
53 | };
54 |
55 | // Hook to use the context
56 | export const useConnect = () => {
57 | const context = useContext(ConnectContext);
58 | if (context === undefined) {
59 | throw new Error('useConnect must be used within a ConnectProvider');
60 | }
61 | return context;
62 | };
63 |
--------------------------------------------------------------------------------
/src/utils/eas.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { optimismSepolia } from 'thirdweb/chains';
3 | import { type Address } from 'viem';
4 | import { ethers6Adapter } from 'thirdweb/adapters/ethers6';
5 | import { useActiveWallet } from 'thirdweb/react';
6 | import { client } from '@/lib/third-web/provider';
7 | import { activeChain } from '@/lib/third-web/constants';
8 |
9 | export type EASConfig = {
10 | EASDeployment: Address;
11 | SchemaRegistry: Address;
12 | };
13 |
14 | type Signer = Awaited>;
15 |
16 | export function useSigner() {
17 | const wallet = useActiveWallet();
18 |
19 | const [signer, setSigner] = useState();
20 |
21 | useEffect(() => {
22 | async function getSigner() {
23 | if (!wallet) return;
24 |
25 | const account = wallet.getAccount();
26 | if (!account) return;
27 |
28 | const ethersSigner = await ethers6Adapter.signer.toEthers({
29 | client,
30 | chain: activeChain,
31 | account,
32 | });
33 |
34 | setSigner(ethersSigner);
35 | }
36 |
37 | getSigner();
38 | }, [wallet]);
39 | return signer;
40 | }
41 |
42 | interface Config extends EASConfig {
43 | explorer: string;
44 | gqlUrl: string;
45 | }
46 |
47 | export const EASNetworks: Record = {
48 | // Optimism
49 | 10: {
50 | EASDeployment: '0x4200000000000000000000000000000000000021',
51 | SchemaRegistry: '0x4200000000000000000000000000000000000020',
52 | explorer: 'https://optimism.easscan.org',
53 | gqlUrl: 'https://optimism.easscan.org/graphql',
54 | },
55 | // Optimism Sepolia
56 | [optimismSepolia.id]: {
57 | EASDeployment: '0x4200000000000000000000000000000000000021',
58 | SchemaRegistry: '0x4200000000000000000000000000000000000020',
59 | explorer: `https://optimism-sepolia.blockscout.com`,
60 | gqlUrl: 'https://optimism-sepolia.easscan.org/graphql',
61 | },
62 | };
63 |
64 | export const SCHEMA_UID =
65 | process.env.NEXT_PUBLIC_EAS_SCHEMA_UID ||
66 | '0x8c12749f56c911dbc13a6a6685b6964c3ea03023f246137e9c53ba97974e4b75';
67 |
--------------------------------------------------------------------------------
/src/app/components/LogoutModal.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDisconnect } from 'wagmi';
3 | import IconBug from 'public/images/icons/IconBug';
4 | import IconLogout from 'public/images/icons/IconLogout';
5 | interface LogoutModal {}
6 | // Logut Modal
7 | const LogoutModal: React.FC = () => {
8 | const { disconnectAsync } = useDisconnect();
9 |
10 | const handleLogOut = async () => {
11 | await disconnectAsync();
12 | localStorage.clear();
13 | window.location.reload();
14 | };
15 |
16 | return (
17 |
18 |
23 |
24 |
25 |
26 |
27 | Report a Bug
28 |
29 |
30 |
31 |
handleLogOut()}
33 | className='flex h-11 flex-[1_0_0] items-center justify-center gap-[var(--spacing-sm,6px)] rounded-lg border border-[var(--Border-Border-Secondary,#FF99A1)] bg-[var(--Component-colors-Components-Buttons-Secondary-button-secondary-bg,#FFF)] px-[var(--spacing-xl,16px)] py-2.5 shadow-[0px_1px_3px_0px_rgba(16,24,40,0.10),0px_1px_2px_0px_rgba(16,24,40,0.06)]'
34 | >
35 |
36 |
37 | {' '}
38 | Logout
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default LogoutModal;
46 |
--------------------------------------------------------------------------------
/src/app/categories/components/Countdown.tsx:
--------------------------------------------------------------------------------
1 | // function getTimeDifference(): {
2 | // days: number;
3 | // hours: number;
4 | // mins: number;
5 | // secs: number;
6 | // } {
7 | // const now = new Date();
8 |
9 | // // Target date and time (July 16th at 20:00 CET)
10 | // const targetDate = new Date(
11 | // Date.UTC(
12 | // now.getFullYear(),
13 | // 6,
14 | // 17,
15 | // 13 /* 15:55 CET adjusted to UTC */,
16 | // 55,
17 | // 0,
18 | // ),
19 | // );
20 |
21 | // const diffMs = targetDate.getTime() - now.getTime();
22 |
23 | // const dayMs = 1000 * 60 * 60 * 24;
24 | // const hourMs = dayMs / 24;
25 | // const minMs = hourMs / 60;
26 |
27 | // const days = Math.floor(diffMs / dayMs);
28 | // const hours = Math.floor((diffMs % dayMs) / hourMs);
29 | // const mins = Math.floor((diffMs % hourMs) / minMs);
30 | // const secs = Math.floor((diffMs % minMs) / 1000);
31 |
32 | // return {
33 | // days: Math.max(days, 0),
34 | // hours: Math.max(hours, 0),
35 | // mins: Math.max(mins, 0),
36 | // secs: Math.max(secs, 0),
37 | // };
38 | // }
39 |
40 | // interface Time {
41 | // days: number;
42 | // hours: number;
43 | // mins: number;
44 | // secs: number;
45 | // }
46 |
47 | export const Countdown: React.FC = () => {
48 | // const [time, setTime] = useState(getTimeDifference());
49 |
50 | // useEffect(() => {
51 | // const interval = setInterval(
52 | // () => setTime(getTimeDifference()),
53 | // 10 * 1000,
54 | // );
55 |
56 | // return () => clearInterval(interval);
57 | // }, []);
58 |
59 | // const { days, hours, mins } = time;
60 |
61 | // return (
62 | //
63 | //
Time left for voting
64 | //
65 | // {`${days}d: ${hours}h: ${mins}min`}
66 | //
67 | //
68 | // );
69 | return (
70 |
71 |
Official Voting Has Ended.
72 |
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/src/app/components/Header.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect, useState } from 'react';
4 | import Image from 'next/image'; // Make sure to install 'next/image'
5 | import { useRouter } from 'next/navigation';
6 | import { AdjacentBadges } from '../badges/components/AdjacentBadges';
7 | import { useGetBadges } from '../features/badges/getBadges';
8 | import { useConnect } from '../providers/ConnectProvider';
9 | import { isLoggedIn } from '@/utils/auth';
10 | import { ButtonLoadingSpinner } from './LoadingSpinner';
11 | const Header = () => {
12 | const [opImage, setOpImage] = useState(() => {
13 | const storedValue = localStorage.getItem('OPcharacter');
14 | return storedValue !== null ? Number(storedValue) : 0;
15 | });
16 |
17 | useEffect(() => {
18 | const updateOpImage = async () => {
19 | if (opImage === 0) {
20 | const userId = await isLoggedIn();
21 | const opImageNumber = (Number(userId) % 30) + 2;
22 | setOpImage(opImageNumber);
23 | localStorage.setItem('OPcharacter', opImageNumber.toString());
24 | }
25 | };
26 |
27 | updateOpImage();
28 | }, [opImage]);
29 |
30 | const router = useRouter();
31 | const { data: badges } = useGetBadges();
32 |
33 | const { setIsConnectDrawerOpen } = useConnect();
34 |
35 |
36 | const hasConnected = Boolean(badges);
37 |
38 | return (
39 |
65 | );
66 | };
67 |
68 | export default Header;
69 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryRankingListItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import IconMove from 'public/images/icons/IconMove';
4 | import { IProject } from '../types';
5 | import Image from 'next/image';
6 | import { Reorder, useDragControls } from 'framer-motion';
7 | import { truncate } from '@/app/helpers/text-helpers';
8 | import IconTrash from 'public/images/icons/IconTrash';
9 |
10 | interface ICategoryRankingListItemProps {
11 | project: IProject;
12 | order: number;
13 | handleRemoveFromSelected: (projectId: number) => void;
14 | }
15 |
16 | const CategoryRankingListItem = ({
17 | project,
18 | order,
19 | handleRemoveFromSelected,
20 | }: ICategoryRankingListItemProps) => {
21 | const controls = useDragControls();
22 |
23 | return (
24 |
29 |
30 |
31 |
controls.start(e)}
35 | >
36 |
37 |
38 |
#{order}
39 |
40 | {project.image ? (
41 |
48 | ) : (
49 |
50 |
51 | {project.name}
52 |
53 |
54 | )}
55 |
56 | {truncate(project.name, 25)}
57 |
58 |
59 |
60 |
handleRemoveFromSelected(project.id)}>
61 |
62 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default CategoryRankingListItem;
69 |
--------------------------------------------------------------------------------
/public/images/icons/IconBug.tsx:
--------------------------------------------------------------------------------
1 | const IconBug = () => {
2 | return (
3 |
10 |
16 |
22 |
28 |
34 |
40 |
46 |
52 |
58 |
64 |
70 |
76 |
77 | );
78 | };
79 |
80 | export default IconBug;
81 |
--------------------------------------------------------------------------------
/src/app/categories/[categoryId]/project-ranking/done/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Button from '@/app/components/Button';
4 | import LoadingSpinner from '@/app/components/LoadingSpinner';
5 | import { Routes } from '@/app/constants/Routes';
6 | import { useCategoryById } from '@/app/features/categories/getCategoryById';
7 | import Image from 'next/image';
8 | import { useParams, useRouter } from 'next/navigation';
9 |
10 | const ProjectRankingDonePage = () => {
11 | const router = useRouter();
12 | const { categoryId } = useParams();
13 |
14 | const selectedCategoryId =
15 | typeof categoryId === 'string' ? categoryId : categoryId[0];
16 |
17 | const { data, isLoading: isCategoryLoading } =
18 | useCategoryById(+selectedCategoryId);
19 | const selectedCategoryProgress = data?.data?.progress;
20 | console.log('Selected Category', selectedCategoryProgress);
21 |
22 | if (isCategoryLoading) {
23 | return ;
24 | }
25 |
26 | return (
27 |
28 |
29 |
35 |
36 | Fantastic job filtering
37 |
38 |
39 | {' '}
40 | Now let's find out which projects come out on top!
41 |
42 |
43 |
44 | {
46 | // if (
47 | // selectedCategoryProgress === 'Filtering' &&
48 | // currentIndex === -1
49 | // ) {
50 | // updateCategoryMarkFiltered.mutate({
51 | // data: {
52 | // cid: +selectedCategoryId,
53 | // },
54 | // });
55 | // }
56 | router.push(
57 | `${Routes.Categories}/${categoryId}/pairwise-ranking`,
58 | );
59 | }}
60 | // disabled={updateCategoryMarkFiltered.isPending}
61 | className='w-full bg-primary'
62 | >
63 | Start Ranking
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default ProjectRankingDonePage;
71 |
--------------------------------------------------------------------------------
/src/utils/attest-utils.ts:
--------------------------------------------------------------------------------
1 | import { axios as axiosInstance } from '@/lib/axios';
2 | import { ICategory, IProject } from '@/app/categories/types';
3 |
4 | export const pinFileToIPFS = async (list: object) => {
5 | try {
6 | const res = await axiosInstance.post('/flow/pinJSONToIPFS', {
7 | json: list,
8 | });
9 | return res.data;
10 | } catch (error) {
11 | console.log(error);
12 | }
13 | };
14 |
15 | export const convertRankingToAttestationFormat = async (
16 | ranking: IProject[] | ICategory[],
17 | collectionName: string,
18 | collectionDescription: string,
19 | ) => {
20 | const obj = {
21 | listDescription: `${collectionDescription}`,
22 | impactEvaluationLink: 'https://pairwise.vote',
23 | impactCategory: ['PAIRWISE'],
24 | impactEvaluationDescription: `This list has been carefully curated and ranked by Pairwise among projects related to ${collectionName}.`,
25 | listContent: ranking.map(item => ({
26 | RPGF3_Application_UID: item.RPGF4Id,
27 | })),
28 | };
29 |
30 | const listName = collectionName;
31 | const listMetadataPtrType = 1;
32 |
33 | const url = await pinFileToIPFS(obj);
34 |
35 | return {
36 | listName,
37 | listMetadataPtrType,
38 | listMetadataPtr: `https://giveth.mypinata.cloud/ipfs/${url}`,
39 | };
40 | };
41 |
42 | export const getPrevAttestationIds = async (
43 | address: string,
44 | schemaId: string,
45 | gqlUrl: string,
46 | collectionName: string,
47 | ): Promise => {
48 | const query = `
49 | query PrevAttestationsQuery($where: AttestationWhereInput) {
50 | groupByAttestation(
51 | where: $where,
52 | by: [id, decodedDataJson]
53 | ) {
54 | id
55 | decodedDataJson
56 | }
57 | }
58 | `;
59 |
60 | const res = await axiosInstance.post(gqlUrl, {
61 | query: query,
62 | operationName: 'PrevAttestationsQuery',
63 | variables: {
64 | where: {
65 | revocable: { equals: true },
66 | revoked: { equals: false },
67 | schemaId: {
68 | equals: schemaId,
69 | },
70 | attester: { equals: address },
71 | },
72 | by: null,
73 | },
74 | });
75 |
76 | const temp = res.data.data.groupByAttestation.map((item: any) => ({
77 | ...item,
78 | data: JSON.parse(item.decodedDataJson),
79 | }));
80 |
81 | return temp
82 | .filter((item: any) => item.data[0].value.value === collectionName)
83 | .map((item: any) => item.id);
84 | };
85 |
--------------------------------------------------------------------------------
/src/app/components/Drawer.tsx:
--------------------------------------------------------------------------------
1 | import { motion, AnimatePresence } from 'framer-motion';
2 | import React, { ReactNode, useEffect } from 'react';
3 |
4 | interface DrawerProps {
5 | isOpen: boolean;
6 | setIsOpen: (open: boolean) => void;
7 | children: ReactNode;
8 | }
9 |
10 | const Drawer: React.FC = ({ isOpen, setIsOpen, children }) => {
11 | // Manage body scroll when drawer is open
12 | useEffect(() => {
13 | const body = document.body;
14 | const scrollY = window.scrollY; // Capture scroll position
15 | if (isOpen) {
16 | body.style.position = 'fixed';
17 | body.style.top = `-${scrollY}px`;
18 | body.style.width = '100%';
19 | body.style.overflow = 'hidden';
20 | } else {
21 | const scrollY = body.style.top;
22 | body.style.position = '';
23 | body.style.top = '';
24 | body.style.overflow = '';
25 | window.scrollTo(0, parseInt(scrollY || '0') * -1);
26 | }
27 | }, [isOpen]);
28 |
29 | // Animation variants for Framer Motion
30 | const backdropVariants = {
31 | open: { opacity: 1 },
32 | closed: { opacity: 0 },
33 | };
34 |
35 | const drawerVariants = {
36 | open: { y: 0 },
37 | closed: { y: '100%' },
38 | };
39 |
40 | return (
41 |
42 | {isOpen && (
43 | {
46 | e.stopPropagation();
47 | setIsOpen(false);
48 | }}
49 | initial='closed'
50 | animate='open'
51 | exit='closed'
52 | variants={backdropVariants}
53 | transition={{ duration: 0.3 }} // Synchronize backdrop transition
54 | >
55 | e.stopPropagation()}
68 | >
69 |
70 |
71 | {children}
72 |
73 |
74 |
75 |
76 | )}
77 |
78 | );
79 | };
80 |
81 | export default Drawer;
82 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoriesProjectDrawerContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IProject } from '../types';
3 | import Image from 'next/image';
4 | import { cn } from '@/app/helpers/cn';
5 |
6 | interface ICategoriesProjectDrawerContentProps {
7 | project: IProject;
8 | }
9 |
10 | const CategoriesProjectDrawerContent = ({
11 | project,
12 | }: ICategoriesProjectDrawerContentProps) => {
13 | return (
14 |
15 |
16 |
17 | {project.image ? (
18 |
25 | ) : (
26 |
27 |
28 | {project.name}
29 |
30 |
31 | )}
32 |
33 |
39 |
46 | {project.image ? (
47 |
54 | ) : (
55 |
56 |
57 | {project.name}
58 |
59 |
60 | )}
61 |
62 |
63 |
64 |
65 |
66 |
{project?.name}
67 |
68 | {project?.contributionDescription}
69 |
70 |
{project?.impactDescription}
71 |
72 |
73 | );
74 | };
75 |
76 | export default CategoriesProjectDrawerContent;
77 |
--------------------------------------------------------------------------------
/src/app/components/SubmittingVoteSpinner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from 'next/image';
3 |
4 | const SubmittingVoteSpinner = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
18 |
19 |
20 |
21 |
29 |
30 |
31 |
32 |
40 |
41 |
42 |
43 |
51 |
57 |
58 |
59 |
60 |
66 |
67 |
68 |
69 | Submitting Vote
70 |
71 |
72 | Please wait while the vote is being submitted. This
73 | may take a while.
74 |
75 |
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | export default SubmittingVoteSpinner;
83 |
--------------------------------------------------------------------------------
/src/utils/auth.ts:
--------------------------------------------------------------------------------
1 | import { axios } from '@/lib/axios';
2 | import { Account } from 'thirdweb/wallets';
3 |
4 | axios.interceptors.response.use(
5 | function (response) {
6 | return response;
7 | },
8 | function (error) {
9 | if (error.response && error.response.status === 401) {
10 | logoutFromPwBackend();
11 | }
12 | return Promise.reject(error);
13 | },
14 | );
15 |
16 | export const isLoggedIn = async () => {
17 | if (!localStorage.getItem('auth')) return false;
18 | try {
19 | const { data } = await axios.get('/auth/isloggedin');
20 | return data;
21 | } catch (err) {
22 | return false;
23 | }
24 | };
25 |
26 | // const fetchNonce = async () => {
27 | // try {
28 | // const { data } = await axios.get('/auth/nonce')
29 | // return data
30 | // } catch (err) {
31 | // console.error(err)
32 | // }
33 | // }
34 |
35 | // function generateRandomString(length: number): string {
36 | // const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
37 | // let result = '';
38 | // for (let i = 0; i < length; i++) {
39 | // result += characters.charAt(Math.floor(Math.random() * characters.length));
40 | // }
41 | // return result;
42 | // }
43 |
44 | export let alreadyInProgress = false;
45 |
46 | export const loginToPwBackend = async (
47 | chainId: number,
48 | address: string,
49 | signFunction: Account['signMessage'],
50 | ) => {
51 | alreadyInProgress = true;
52 | // const nonce = await fetchNonce()
53 | // const nonce = generateRandomString(16
54 |
55 | const message = 'Signing in to Pairwise servers';
56 |
57 | const signature = await signFunction({
58 | message,
59 | });
60 |
61 | // Verify signature
62 | const { data } = await axios.post<{ token: string; isNewUser: boolean }>(
63 | '/auth/login',
64 | {
65 | ...{ message, signature: `${signature}`, address, chainId },
66 | },
67 | );
68 |
69 | const token = data.token;
70 | window.localStorage.setItem('auth', token);
71 | window.localStorage.setItem('loggedInAddress', address);
72 | axios.defaults.headers.common['auth'] = token;
73 |
74 | alreadyInProgress = false;
75 |
76 | return data;
77 | };
78 |
79 | export const logoutFromPwBackend = async () => {
80 | try {
81 | window.localStorage.removeItem('auth');
82 | window.localStorage.removeItem('loggedInAddress');
83 | if (axios.defaults.headers.common['auth']) {
84 | delete axios.defaults.headers.common['auth'];
85 | }
86 | // await axios.post('/auth/logout')
87 | } catch (err) {
88 | console.error(err);
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryProjectRankingCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import { IProject } from '../types';
3 | import { truncate } from '@/app/helpers/text-helpers';
4 | import { useState } from 'react';
5 | import Drawer from '@/app/components/Drawer';
6 | import CategoriesProjectDrawerContent from './CategoriesProjectDrawerContent';
7 | import { motion } from 'framer-motion';
8 | import IconEye from 'public/images/icons/IconEye';
9 |
10 | interface ICategoryProjectRankingCardProps {
11 | project: IProject;
12 | hasSeenProjectDetails: boolean;
13 | setHasSeenProjectDetails: (value: boolean) => void;
14 | }
15 |
16 | const CategoryProjectRankingCard = ({
17 | project,
18 | hasSeenProjectDetails,
19 | setHasSeenProjectDetails,
20 | }: ICategoryProjectRankingCardProps) => {
21 | const [isDrawerOpen, setIsDrawerOpen] = useState(false);
22 |
23 | const variants = {
24 | hidden: { opacity: 0 },
25 | show: {
26 | opacity: 1,
27 | transition: {
28 | duration: 0.5,
29 | },
30 | },
31 | exit: {
32 | opacity: 0,
33 | transition: {
34 | duration: 0.5,
35 | },
36 | },
37 | };
38 | console.log('project', project);
39 | return (
40 |
46 |
47 |
48 | {project.image ? (
49 |
56 | ) : (
57 |
58 |
59 | {project.name}
60 |
61 |
62 | )}
63 |
64 |
{
67 | setHasSeenProjectDetails(true);
68 | setIsDrawerOpen(true);
69 | }}
70 | >
71 |
72 |
73 |
74 |
77 |
78 | {truncate(project.impactDescription, 90)}
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default CategoryProjectRankingCard;
89 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryPairwiseCard.tsx:
--------------------------------------------------------------------------------
1 | import IconAlertCircle from 'public/images/icons/IconAlertCircle';
2 | import { IProject } from '../types';
3 | import { truncate } from '@/app/helpers/text-helpers';
4 |
5 | import Drawer from '@/app/components/Drawer';
6 | import CategoriesProjectDrawerContent from './CategoriesProjectDrawerContent';
7 | import { useState } from 'react';
8 | import { AnimatePresence, motion } from 'framer-motion';
9 | import { cn } from '@/app/helpers/cn';
10 |
11 | interface ICategoryPairwiseCardProps {
12 | project: IProject;
13 | }
14 |
15 | const variants = {
16 | hidden: { opacity: 0 },
17 | visible: { opacity: 1 },
18 | };
19 |
20 | const CategoryPairwiseCard = ({ project }: ICategoryPairwiseCardProps) => {
21 | const [isDrawerOpen, setIsDrawerOpen] = useState(false);
22 |
23 | const imgNumber = (project.id % 5) + 1;
24 | const imgSrc = `/images/defaults/category/category-${imgNumber}.png`;
25 | return (
26 |
27 |
34 |
35 |
50 | {!project.image && (
51 |
52 | {project.name}
53 |
54 | )}
55 |
56 |
57 |
58 |
59 | {truncate(project.name, 16)}
60 |
61 |
{
63 | e.stopPropagation();
64 | setIsDrawerOpen(true);
65 | }}
66 | >
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | };
79 |
80 | export default CategoryPairwiseCard;
81 |
--------------------------------------------------------------------------------
/src/app/connect/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Image from 'next/image';
4 | // Bandada logo removed
5 | import React from 'react';
6 | import { useAccount } from 'wagmi';
7 | import { Routes } from '../constants/Routes';
8 | import { useRouter } from 'next/navigation';
9 | import ConnectSplashMessage from './components/ConnectSplashMessage';
10 |
11 | const steps = [
12 | {
13 | title: 'Go to Pairwise voting app',
14 | description: 'Get the code from Pairwise connect modal',
15 | },
16 | { title: 'Sign', description: 'Sign with your connected wallet' },
17 | {
18 | title: 'Connect your OP Account',
19 | description:
20 | 'Success! Your OP account is now secretly connected to the account you will vote with on Pairwise',
21 | },
22 | ];
23 |
24 | const ConnectHomePage = () => {
25 | const { isConnected } = useAccount();
26 | const router = useRouter();
27 |
28 | const handleNavigation = () => {
29 | const currentParams = new URLSearchParams(window.location.search);
30 |
31 | router.push(`${Routes.ConnectOtp}?${currentParams.toString()}`);
32 | };
33 |
34 | return (
35 |
36 |
37 |
38 |
45 |
46 |
Connect your OP Account
47 |
Pseudonymously
48 |
49 | {/* Bandada attribution removed */}
50 |
51 | {steps.map((step, index) => (
52 |
56 |
61 |
62 |
63 | {step.title}
64 |
65 |
66 | {step.description}
67 |
68 |
69 |
70 | ))}
71 |
76 | Next
77 |
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export default ConnectHomePage;
85 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryPairwiseCardWithMetrics.tsx:
--------------------------------------------------------------------------------
1 | import { IProject } from '../types';
2 | import { truncate } from '@/app/helpers/text-helpers';
3 | import { cn } from '@/app/helpers/cn';
4 |
5 | interface ICategoryPairwiseCardWithMetricsProps {
6 | project: IProject;
7 | onClick: () => void;
8 | onInfoClick: () => void;
9 | }
10 |
11 | const CategoryPairwiseCardWithMetrics = ({
12 | project,
13 | onClick,
14 | onInfoClick,
15 | }: ICategoryPairwiseCardWithMetricsProps) => {
16 | const getDescription = (project: IProject) => {
17 | if (project.impactDescription.length > 0)
18 | return project.impactDescription;
19 | if (
20 | project.contributionDescription &&
21 | project.contributionDescription.length > 0
22 | )
23 | return project.contributionDescription;
24 |
25 | return null;
26 | };
27 | return (
28 |
29 |
30 |
46 | {!project.image && (
47 |
48 | {project.name}
49 |
50 | )}
51 |
52 |
53 |
54 |
55 | {truncate(project.name, 16)}
56 |
57 |
74 |
75 |
76 |
77 | {project.shortDescription ||
78 | truncate(getDescription(project) || '', 80)}
79 |
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default CategoryPairwiseCardWithMetrics;
87 |
--------------------------------------------------------------------------------
/src/app/category-ranking/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect } from 'react';
4 | import LoadingSpinner from '@/app/components/LoadingSpinner';
5 | import { useGetCategoryPairs } from '../features/categories/getCategoryPairs';
6 | import CategoryPairwiseCard from '../categories/components/CategoryPairwiseCard';
7 | import { useUpdateCategoryVote } from '../features/categories/updateCategoryVote';
8 | import TopRouteIndicator from '../components/TopRouteIndicator';
9 | import { useRouter } from 'next/navigation';
10 |
11 | const CategoryPairwiseRankingPage = () => {
12 | const router = useRouter();
13 |
14 | const { mutate, isPending: isVotingPending } = useUpdateCategoryVote();
15 |
16 | const {
17 | data: pairwisePairs,
18 | isLoading: isPairwisePairsLoading,
19 | isFetching: isFetchingPairwise,
20 | } = useGetCategoryPairs();
21 | console.log('PairwiseData', pairwisePairs);
22 |
23 | const [firstCategory, secondCategory] = pairwisePairs ?? [];
24 |
25 | useEffect(() => {
26 | if (pairwisePairs?.length === 0)
27 | router.push('/category-ranking/comment');
28 | }, [pairwisePairs, router]);
29 |
30 | const isLoading = false;
31 | const handleVote = async (pickedId: number) => {
32 | mutate({
33 | data: {
34 | collection1Id: firstCategory.id,
35 | collection2Id: secondCategory.id,
36 | pickedId,
37 | },
38 | });
39 | };
40 |
41 | if (isPairwisePairsLoading || pairwisePairs?.length === 0) {
42 | return ;
43 | }
44 |
45 | return (
46 |
47 |
48 |
49 |
50 | {`Which category should receive more RetroPGF funding?`}
51 |
52 |
53 |
{
56 | console.log('Clicking on First', isLoading);
57 | !isLoading && handleVote(firstCategory.id);
58 | }}
59 | className={`${isLoading ? 'cursor-not-allowed opacity-50' : 'opacity-100'} cursor-pointer`}
60 | >
61 |
62 |
63 |
66 | !isLoading && handleVote(secondCategory.id)
67 | }
68 | className={`${isLoading ? 'cursor-not-allowed opacity-50' : 'opacity-100'} cursor-pointer`}
69 | >
70 |
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | export default CategoryPairwiseRankingPage;
79 |
--------------------------------------------------------------------------------
/src/app/categories/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect, useState } from 'react';
4 | import CategoryItem from './components/CategoryItem';
5 | import { useCategories } from '../features/categories/getCategories';
6 | import LoadingSpinner from '../components/LoadingSpinner';
7 | import { useGetCategoryPairs } from '../features/categories/getCategoryPairs';
8 | import { CategoryPairwiseModal } from '../category-ranking/components/CategoryPairwiseModal';
9 | import { useRouter } from 'next/navigation';
10 | import LogoutModal from '../components/LogoutModal';
11 | import CategoryCardView from './components/CategoryCardView';
12 | import CategoryToggleButton from './components/CategoryToggleButton';
13 | import CategoryRewaredBanner from './components/CategoryRewardBanner';
14 |
15 | const CategoriesPage = () => {
16 | const [isModalOpen, setModalOpen] = useState(false);
17 | const { data: categories, isLoading } = useCategories();
18 | const { data: categoryPairs, isLoading: isGettingCategoryPairs } =
19 | useGetCategoryPairs();
20 | const router = useRouter();
21 |
22 | const [isCardView, setIsCardView] = useState(() => {
23 | const savedState = localStorage.getItem('isCardView');
24 | return savedState !== null ? JSON.parse(savedState) : false;
25 | });
26 |
27 | useEffect(() => {
28 | const bool = categoryPairs && categoryPairs.length > 0;
29 | setModalOpen(bool || false);
30 | }, [categoryPairs]);
31 |
32 | const closeModal = () => setModalOpen(false);
33 |
34 | const handleSubmit = () => router.push('/category-ranking');
35 |
36 | if (isLoading) {
37 | return ;
38 | }
39 |
40 | const toggleView = () => {
41 | localStorage.setItem('isCardView', JSON.stringify(!isCardView));
42 | setIsCardView(!isCardView);
43 | };
44 | return (
45 |
46 |
47 |
52 |
53 |
54 |
Categories
55 |
Select one to begin ranking
56 |
57 |
58 |
59 | {isCardView ? (
60 |
61 | {categories?.data?.map((category, index) => (
62 |
67 | ))}
68 |
69 | ) : (
70 | categories?.data?.map((category, index) => (
71 |
76 | ))
77 | )}
78 |
79 |
80 | );
81 | };
82 |
83 | export default CategoriesPage;
84 |
--------------------------------------------------------------------------------
/public/images/icons/CardIcon.tsx:
--------------------------------------------------------------------------------
1 | const CardIcon = () => {
2 | return (
3 |
10 |
17 |
24 |
31 |
38 |
39 | );
40 | };
41 |
42 | export default CardIcon;
43 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Image from 'next/image'; // Make sure to install 'next/image'
4 | import { CollectionProgressStatus, ICategory } from '../types';
5 | import { useRouter } from 'next/navigation';
6 | import { Routes } from '@/app/constants/Routes';
7 | import CategoryBadge from './CategoryBadge';
8 | import { truncate } from '@/app/helpers/text-helpers';
9 | import { useGetBadges } from '@/app/features/badges/getBadges';
10 | import { useConnect } from '@/app/providers/ConnectProvider';
11 | import { useAccount } from 'wagmi';
12 |
13 | export interface ICategoryProps {
14 | category: ICategory;
15 | progress?: CollectionProgressStatus;
16 | imageNumber?: number;
17 | }
18 |
19 | const CategoryItem = ({ category, progress, imageNumber }: ICategoryProps) => {
20 | const router = useRouter();
21 |
22 | const { data: badges } = useGetBadges();
23 |
24 | const { handleConnect, setIsConnectDrawerOpen } = useConnect();
25 | const { isConnected } = useAccount();
26 |
27 | const imgNumber = imageNumber || (category.id % 5) + 1;
28 | const imgSrc = `/images/defaults/category/category-${imgNumber}.png`;
29 | const onCategoryClick = () => {
30 | if (progress) return null;
31 | switch (category.progress) {
32 | case 'Filtered':
33 | router.push(
34 | `${Routes.Categories}/${category.id}/project-ranking/summary`,
35 | );
36 | break;
37 | case 'Filtering':
38 | router.push(
39 | `${Routes.Categories}/${category.id}/project-ranking`,
40 | );
41 | break;
42 | case 'Finished':
43 | router.push(
44 | `${Routes.Categories}/${category.id}/pairwise-ranking/ranking-list`,
45 | );
46 | break;
47 | case 'Attested':
48 | router.push(
49 | `${Routes.Categories}/${category.id}/pairwise-ranking/done`,
50 | );
51 | break;
52 | case 'WIP':
53 | case 'WIP - Threshold':
54 | router.push(
55 | `${Routes.Categories}/${category.id}/pairwise-ranking`,
56 | );
57 | break;
58 | default:
59 | router.push(`${Routes.Categories}/${category.id}`);
60 | }
61 | };
62 |
63 | const checkConnectionThenRedirect = () => {
64 | if (badges) {
65 | onCategoryClick();
66 | } else if (!isConnected) {
67 | setIsConnectDrawerOpen(true);
68 | } else {
69 | handleConnect();
70 | }
71 | };
72 |
73 | return (
74 |
78 |
79 |
86 |
87 | {category.name}
88 |
89 |
90 |
91 |
{category.name}
92 |
93 | {truncate(category.impactDescription, 70)}
94 |
95 |
96 |
97 |
98 | );
99 | };
100 |
101 | export default CategoryItem;
102 |
--------------------------------------------------------------------------------
/src/app/categories/components/CategoryCardView.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { CollectionProgressStatus, ICategory } from '../types';
5 | import { useRouter } from 'next/navigation';
6 | import { Routes } from '@/app/constants/Routes';
7 | import CategoryBadge from './CategoryBadge';
8 | import { truncate } from '@/app/helpers/text-helpers';
9 | import { useGetBadges } from '@/app/features/badges/getBadges';
10 | import { useConnect } from '@/app/providers/ConnectProvider';
11 | import { useAccount } from 'wagmi';
12 |
13 | export interface ICategoryProps {
14 | category: ICategory;
15 | progress?: CollectionProgressStatus;
16 | imageNumber?: number;
17 | }
18 |
19 | const CategoryCardView = ({
20 | category,
21 | progress,
22 | imageNumber,
23 | }: ICategoryProps) => {
24 | const router = useRouter();
25 |
26 | const { data: badges } = useGetBadges();
27 |
28 | const { handleConnect, setIsConnectDrawerOpen } = useConnect();
29 | const { isConnected } = useAccount();
30 |
31 | const imgNumber = imageNumber || (category.id % 5) + 1;
32 | const imgSrc = `/images/defaults/category/category-${imgNumber}.png`;
33 |
34 | const onCategoryClick = () => {
35 | if (progress) return null;
36 | switch (category.progress) {
37 | case 'Filtered':
38 | router.push(
39 | `${Routes.Categories}/${category.id}/project-ranking/summary`,
40 | );
41 | break;
42 | case 'Filtering':
43 | router.push(
44 | `${Routes.Categories}/${category.id}/project-ranking`,
45 | );
46 | break;
47 | case 'Finished':
48 | router.push(
49 | `${Routes.Categories}/${category.id}/pairwise-ranking/ranking-list`,
50 | );
51 | break;
52 | case 'Attested':
53 | router.push(
54 | `${Routes.Categories}/${category.id}/pairwise-ranking/done`,
55 | );
56 | break;
57 | case 'WIP':
58 | case 'WIP - Threshold':
59 | router.push(
60 | `${Routes.Categories}/${category.id}/pairwise-ranking`,
61 | );
62 | break;
63 | default:
64 | router.push(`${Routes.Categories}/${category.id}`);
65 | }
66 | };
67 |
68 | const checkConnectionThenRedirect = () => {
69 | if (badges) {
70 | onCategoryClick();
71 | } else if (!isConnected) {
72 | setIsConnectDrawerOpen(true);
73 | } else {
74 | handleConnect();
75 | }
76 | };
77 |
78 | return (
79 |
83 |
84 |
85 |
86 |
91 |
92 | {category.name}
93 |
94 |
95 |
96 |
99 |
{' '}
100 |
101 | {category.name}
102 |
103 |
104 | {truncate(category.impactDescription, 50)}
105 |
106 |
107 |
108 |
109 | );
110 | };
111 |
112 | export default CategoryCardView;
113 |
--------------------------------------------------------------------------------
/src/app/login/components/OtpInput.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/app/helpers/cn';
2 | import React, { FC, useEffect, useState } from 'react';
3 | import OTPInput from 'react-otp-input';
4 | import { ErrorBox } from './ErrorBox';
5 | import { DotsLoader } from './bouncing-dots/DotsLoader';
6 | import { Edit2 } from 'public/images/icons/Edit2';
7 |
8 | export enum OtpState {
9 | InProgress,
10 | Ready,
11 | Loading,
12 | Invalid,
13 | Valid,
14 | }
15 | interface Props {
16 | onSubmit: () => void;
17 | resend: () => void;
18 | otp: string;
19 | email: string;
20 | setOtp: (otp: string) => void;
21 | state: OtpState;
22 | setState: (state: OtpState) => void;
23 | error: string | false;
24 | }
25 |
26 | const OtpLength = 6;
27 | const ResendTime = 60;
28 |
29 | export const OtpInput: FC = ({
30 | otp,
31 | setOtp,
32 | state,
33 | email,
34 | setState,
35 | onSubmit,
36 | error,
37 | resend,
38 | }) => {
39 | const [resendTimer, setResendTimer] = useState(ResendTime);
40 |
41 | const handleResend = () => {
42 | setResendTimer(ResendTime);
43 | setOtp('');
44 | setState(OtpState.InProgress);
45 | resend();
46 | };
47 |
48 | useEffect(() => {
49 | setTimeout(() => {
50 | setResendTimer(Math.max(0, resendTimer - 1));
51 | }, 1000);
52 | }, [resendTimer, setResendTimer]);
53 |
54 | const handleOTPChange = (otp: string) => {
55 | if (otp.length === OtpLength) setState(OtpState.Ready);
56 | else setState(OtpState.InProgress);
57 | setOtp(otp);
58 | };
59 |
60 | return (
61 |
62 |
Verify Email
63 |
64 | Please enter the 6 digit secure code sent to your email
65 | {email}
66 |
67 |
68 |
69 |
70 |
71 | }
83 | shouldAutoFocus
84 | />
85 | {error && }
86 |
87 |
96 |
97 | {state === OtpState.Loading ? : 'Submit'}
98 |
99 |
100 |
101 | Didnt receive the code?
102 |
103 | {resendTimer === 0 ? (
104 |
108 | Resend Code
109 |
110 | ) : (
111 |
112 | {`Resend code in`}
113 |
114 | {' '}
115 | {`${resendTimer}s`}{' '}
116 |
117 |
118 | )}
119 |
120 |
121 |
122 | );
123 | };
124 |
--------------------------------------------------------------------------------
/src/app/categories/[categoryId]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import TopNavigation from '@/app/components/TopNavigation';
4 | import { useParams, useRouter } from 'next/navigation';
5 | import React, { useEffect, useState } from 'react';
6 | import { Routes } from '@/app/constants/Routes';
7 | import CategoryBadge from '../components/CategoryBadge';
8 | import CategoryProjectItem from '../components/CategoryProjectItem';
9 | import Button from '@/app/components/Button';
10 | import { useProjectsByCategoryId } from '@/app/features/categories/getProjectsByCategoryId';
11 | import LoadingSpinner from '@/app/components/LoadingSpinner';
12 | import { useCategoryById } from '@/app/features/categories/getCategoryById';
13 | import { truncate } from '@/app/helpers/text-helpers';
14 | import posthog from 'posthog-js';
15 | import { IProject } from '../types';
16 |
17 | const CategoryPage = () => {
18 | const router = useRouter();
19 | const { categoryId } = useParams();
20 | const [sortedProjects, setSortedProjects] = useState();
21 |
22 | const selectedCategoryId =
23 | typeof categoryId === 'string' ? categoryId : categoryId[0];
24 | const { data: projects, isLoading: isProjectsLoading } =
25 | useProjectsByCategoryId(+selectedCategoryId);
26 |
27 | const { data, isLoading: isCategoryLoading } =
28 | useCategoryById(+selectedCategoryId);
29 |
30 | const selectedCategory = data?.data?.collection;
31 | const selectedCategoryProgress = data?.data.progress;
32 |
33 | useEffect(() => {
34 | if (projects?.data) {
35 | const sorted = projects.data
36 | .slice()
37 | .sort((a, b) => a.name.localeCompare(b.name));
38 | setSortedProjects(sorted);
39 | }
40 | }, [projects]);
41 |
42 | useEffect(() => {
43 | posthog.capture('User goes to the Categories page', {
44 | categoryName: `${selectedCategory?.name}`,
45 | });
46 | }, []);
47 |
48 | if (isProjectsLoading || isCategoryLoading) {
49 | return ;
50 | }
51 |
52 | const minimumProjects = projects?.data?.length
53 | ? Math.ceil(projects?.data?.length * 0.21)
54 | : 2;
55 |
56 | return (
57 |
58 |
59 |
63 |
64 |
65 |
66 | {selectedCategory?.name}
67 |
68 |
69 |
70 |
71 |
72 | {truncate(
73 | selectedCategory?.impactDescription || '',
74 | 400,
75 | )}
76 |
77 |
78 |
79 |
80 | {`Important: In the next stage You must choose at least ${minimumProjects} projects.`}
81 |
82 |
83 | Projects ({projects?.data?.length})
84 |
85 |
86 | {sortedProjects?.map(project => (
87 |
88 |
89 |
90 | ))}
91 |
92 |
93 |
94 |
97 | router.push(
98 | `${Routes.Categories}/${categoryId}/filter-guide`,
99 | )
100 | }
101 | >
102 | Start
103 |
104 |
105 |
106 | );
107 | };
108 |
109 | export default CategoryPage;
110 |
--------------------------------------------------------------------------------
/src/app/connect/components/ConnectButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Button from '@/app/components/Button';
4 | import Drawer from '@/app/components/Drawer';
5 | import IconWallet from 'public/images/icons/IconWallet';
6 | import { useAccount, useConnect, useDisconnect } from 'wagmi';
7 | import Image from 'next/image';
8 | import { useState } from 'react';
9 | import { formatAddress } from '@/app/helpers/text-helpers';
10 | import { walletsLogos } from '@/app/constants/WalletIcons';
11 | import { isMobile } from 'react-device-detect';
12 |
13 | const ConnectButton = () => {
14 | const { connectors, connectAsync } = useConnect();
15 | const { address } = useAccount();
16 | const { disconnect } = useDisconnect();
17 | const [isConnectDrawerOpen, setIsConnectDrawerOpen] = useState(false);
18 |
19 | const hasMetaMaskIO = connectors.some(
20 | connector => connector.id === 'io.metamask',
21 | );
22 | const filteredConnectors = hasMetaMaskIO
23 | ? connectors.filter(connector => connector.id !== 'metaMask')
24 | : connectors;
25 |
26 | console.log('connectors', connectors);
27 |
28 | return (
29 |
30 | {address ? (
31 |
disconnect()}
34 | >
35 | {formatAddress(address)}
36 |
37 | ) : (
38 |
39 |
setIsConnectDrawerOpen(true)}
41 | className='bg-primary'
42 | >
43 |
44 |
45 |
Connect Wallet
46 |
47 |
48 |
52 |
53 |
54 | Connect Wallet
55 |
56 | {isMobile && (
57 |
58 | You can connect to your wallet using
59 | WalletConnect
60 |
61 | )}
62 |
63 |
64 | {filteredConnectors.map(connector => (
65 |
{
69 | setIsConnectDrawerOpen(false);
70 | await connectAsync({
71 | connector,
72 | });
73 | }}
74 | >
75 |
76 | {connector.icon &&
77 | connector.id !== 'walletConnect' ? (
78 |
85 | ) : (
86 |
97 | )}
98 |
99 | {connector.name}
100 |
101 | ))}
102 |
103 |
112 |
113 |
114 |
115 | )}
116 |
117 | );
118 | };
119 |
120 | export default ConnectButton;
121 |
--------------------------------------------------------------------------------
/src/lib/third-web/AutoConnect.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { client, smartWalletConfig } from './provider';
4 | import React, {
5 | ReactNode,
6 | useCallback,
7 | useContext,
8 | useEffect,
9 | useState,
10 | } from 'react';
11 | import { WalletId, createWallet } from 'thirdweb/wallets';
12 | import { useActiveAccount, useActiveWallet, useConnect } from 'thirdweb/react';
13 | import { LAST_CONNECT_PERSONAL_WALLET_ID, activeChain } from './constants';
14 | import { alreadyInProgress, isLoggedIn, loginToPwBackend } from '@/utils/auth';
15 |
16 | export enum LogginToPwBackendState {
17 | Initial,
18 | Error,
19 | LoggedIn,
20 | }
21 |
22 | const AuthContext = React.createContext<{
23 | isAutoConnecting: boolean | null;
24 | setIsAutoConnecting: (bool: boolean | null) => void;
25 | loggedToPw: LogginToPwBackendState;
26 | isNewUser: boolean;
27 | setLoggedToPw: (bool: LogginToPwBackendState) => void;
28 | setIsNewUser: (bool: boolean) => void;
29 | }>({
30 | isAutoConnecting: null,
31 | setIsAutoConnecting: () => {},
32 | loggedToPw: LogginToPwBackendState.Initial,
33 | isNewUser: false,
34 | setLoggedToPw: () => {},
35 | setIsNewUser: () => {},
36 | });
37 |
38 | export const AuthProvider = ({ children }: { children: ReactNode }) => {
39 | const [isAutoConnecting, setIsAutoConnecting] = useState(
40 | null,
41 | );
42 | const [loggedToPw, setLoggedToPw] = useState(
43 | LogginToPwBackendState.Initial,
44 | );
45 | const [isNewUser, setIsNewUser] = useState(false);
46 |
47 | return (
48 |
58 | {children}
59 |
60 | );
61 | };
62 |
63 | export const useAuth = () => {
64 | const {
65 | isAutoConnecting,
66 | setIsAutoConnecting,
67 | loggedToPw,
68 | setLoggedToPw,
69 | setIsNewUser,
70 | isNewUser,
71 | } = useContext(AuthContext);
72 |
73 | const { connect } = useConnect();
74 | const account = useActiveAccount();
75 | const wallet = useActiveWallet();
76 |
77 | useEffect(() => {
78 | const main = async () => {
79 | try {
80 | const personalWalletId = localStorage.getItem(
81 | LAST_CONNECT_PERSONAL_WALLET_ID,
82 | );
83 | if (!personalWalletId) return;
84 | setIsAutoConnecting(true);
85 | const personalWallet = createWallet(
86 | personalWalletId as WalletId,
87 | );
88 | const personalAccount = await personalWallet.autoConnect({
89 | client: client,
90 | });
91 | const smartWallet = createWallet('smart', smartWalletConfig);
92 | await smartWallet.connect({ personalAccount, client: client });
93 | await connect(smartWallet);
94 | } finally {
95 | setIsAutoConnecting(false);
96 | }
97 | };
98 |
99 | main();
100 | }, [setIsAutoConnecting, connect]);
101 |
102 | const checkLoginFlow = useCallback(async () => {
103 | try {
104 | if (account && wallet) {
105 | const validToken = await isLoggedIn();
106 | if (validToken) setLoggedToPw(LogginToPwBackendState.LoggedIn);
107 | else if (!alreadyInProgress) {
108 | const res = await loginToPwBackend(
109 | activeChain.id,
110 | account.address,
111 | account.signMessage,
112 | );
113 | if (res.isNewUser) {
114 | setIsNewUser(true);
115 | }
116 | setLoggedToPw(LogginToPwBackendState.LoggedIn);
117 | }
118 | }
119 | } catch (e) {
120 | setLoggedToPw(LogginToPwBackendState.Error);
121 | }
122 | }, [account, wallet, setLoggedToPw, setIsNewUser]);
123 |
124 | useEffect(() => {
125 | checkLoginFlow();
126 | }, [wallet, isAutoConnecting, checkLoginFlow]);
127 |
128 | return {
129 | isAutoConnecting,
130 | setIsAutoConnecting,
131 | loggedToPw,
132 | setLoggedToPw,
133 | setIsNewUser,
134 | isNewUser,
135 | };
136 | };
137 |
--------------------------------------------------------------------------------
/src/app/badges/components/BadgeCard.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 |
3 | export type MedalTypes =
4 | | 'Bronze'
5 | | 'Diamond'
6 | | 'Platnium'
7 | | 'Gold'
8 | | 'Silver'
9 | | 'WHALE';
10 |
11 | export type BadgeData = {
12 | holderPoints?: number;
13 | delegatePoints?: number;
14 | recipientsPoints?: 1;
15 | badgeholderPoints?: 1;
16 | holderAmount?: number;
17 | delegateAmount?: number;
18 | holderType?: MedalTypes;
19 | delegateType?: MedalTypes;
20 | };
21 |
22 | interface BadgeCardProps {
23 | type: BadgeType;
24 | points: number;
25 | medal?: BadgeData['holderType'];
26 | amount?: number;
27 | }
28 |
29 | export const badgeTypeMapping = {
30 | holderPoints: 'Holder',
31 | delegatePoints: 'Delegate',
32 | recipientsPoints: 'Recipient',
33 | badgeholderPoints: 'Badgeholder',
34 | };
35 |
36 | type BadgeType = keyof typeof badgeTypeMapping;
37 |
38 | const BadgeCard: React.FC = ({
39 | type,
40 | points,
41 | medal,
42 | amount,
43 | }) => {
44 | const formatAmount = (amount: number | undefined) => {
45 | if (amount === undefined) return '';
46 | return amount >= 1000000
47 | ? `${(amount / 1000000).toFixed(2)}M`
48 | : amount.toString();
49 | };
50 | const handleBadgesImage = () => {
51 | switch (type) {
52 | case 'holderPoints':
53 | return '/images/badges/1.png';
54 | case 'delegatePoints':
55 | return '/images/badges/2.png';
56 | case 'recipientsPoints':
57 | return '/images/badges/4.png';
58 | case 'badgeholderPoints':
59 | return '/images/badges/3.png';
60 | default:
61 | return '/images/badges/1.png';
62 | }
63 | };
64 |
65 | const handleBadgeInfo = (amount?: number, points?: number) => {
66 | switch (type) {
67 | case 'holderPoints':
68 | case 'delegatePoints':
69 | return (
70 |
71 |
72 |
79 |
80 | {formatAmount(amount)}
81 |
82 |
83 |
84 |
Weight
85 |
{points}
86 |
87 |
88 | );
89 | case 'recipientsPoints':
90 | case 'badgeholderPoints':
91 | return (
92 |
93 |
94 |
1 Address
95 |
1 Vote
96 |
97 |
98 | );
99 | default:
100 | return null;
101 | }
102 | };
103 |
104 | return (
105 |
106 |
107 |
114 |
115 |
116 |
117 | BADGE
118 |
119 |
120 | {badgeTypeMapping[type]}
121 |
122 |
123 | {(type === 'holderPoints' || type === 'delegatePoints') && (
124 |
125 |
126 | TYPE
127 |
128 |
{medal}
129 |
130 | )}
131 |
132 |
133 |
BADGE INFO
134 | {handleBadgeInfo(amount, points)}
135 |
136 |
137 |
138 | );
139 | };
140 |
141 | export default BadgeCard;
142 |
--------------------------------------------------------------------------------