├── .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 |
11 | 12 | 13 | 14 |
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 |
6 |
7 |
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 | 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 | sing in success 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 |
9 |
10 | logo 16 | 17 |
18 |
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 | Welcome character 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 |
14 |
18 |
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 | sing in success 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 | 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 | 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 | 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 | 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 | logo 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 | 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 | Logo 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 | 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 | Logo 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 | 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 | 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 | logo 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 | {image.alt} 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 | category-image 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 | 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 | Logo 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 |
17 |
18 |
19 | Powered by 20 | 21 |
22 | {/* Add Docs and etc here when they are ready */} 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |

Crafted with

36 |
❤️
37 |

by

38 | 39 |
40 |
41 |
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 | {image.alt} 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 | Logo 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 | filter-guide 32 | filter-guide 38 |
39 |
40 |
41 | 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 | 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 | Logo 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 |
40 |
Ranked
41 | 42 |
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 | 37 | 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 | 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 |
33 | 39 | 50 |
51 | {emailError && ( 52 |
53 | {' '} 54 | {' '} 55 |
56 | )} 57 | 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 | 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