diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 0000000..c30e5a9 --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b2e0055 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Public Provider(s) - Useful for testing +NEXT_PUBLIC_PROVIDER_PUBLIC=true + +# Alchemy: https://www.alchemy.com +NEXT_PUBLIC_ALCHEMY_API_KEYs= + +# Infura: https://www.infura.io +NEXT_PUBLIC_INFURA_API_KEY= diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..48adcef --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,68 @@ +{ + "extends": ["next/core-web-vitals", "prettier"], + "plugins": ["prettier"], + "rules": { + "prettier/prettier": ["error"] + }, + "overrides": [ + // Configuration for TypeScript files + { + "files": ["**/*.ts", "**/*.tsx"], + "plugins": ["unused-imports", "tailwindcss", "simple-import-sort"], + "extends": ["plugin:tailwindcss/recommended", "next/core-web-vitals", "plugin:prettier/recommended"], + "parserOptions": { + "project": "./tsconfig.json" + }, + "rules": { + "prettier/prettier": [ + "error", + { + "singleQuote": true, + "endOfLine": "auto" + } + ], + "react/destructuring-assignment": "off", // Vscode doesn't support automatically destructuring, it's a pain to add a new variable + "jsx-a11y/anchor-is-valid": "off", // Next.js use his own internal link system + "react/require-default-props": "off", // Allow non-defined react props as undefined + "react/jsx-props-no-spreading": "off", // _app.tsx uses spread operator and also, react-hook-form + "react-hooks/exhaustive-deps": "off", // Incorrectly report needed dependency with Next.js router + "@next/next/no-img-element": "off", // We currently not using next/image because it isn't supported with SSG mode + "@typescript-eslint/comma-dangle": "off", // Avoid conflict rule between Eslint and Prettier + "@typescript-eslint/consistent-type-imports": "off", // Ensure `import type` is used when it's necessary + "import/prefer-default-export": "off", // Named export is easier to refactor automatically + "unused-imports/no-unused-vars": "off", + "tailwindcss/classnames-order": [ + "warn", + { + "officialSorting": true + } + ], + "sort-imports": [ + 1, + { + "ignoreDeclarationSort": true + } + ], + "import/order": [ + 1, + { + "groups": ["builtin", "external", "internal"], + "pathGroups": [ + { + "pattern": "react", + "group": "external", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": [""], + "newlines-between": "always", + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ] + } + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6dac076 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# turborepo +.turbo + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env +.env*.local +.env*.prd + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..5426a93 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..c743d18 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +# npm run lint diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..f87a044 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +auto-install-peers=true \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..bb5adcf --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "trailingComma": "es5", + "semi": false, + "singleQuote": true, + "printWidth": 150, + "bracketSameLine": true, + "useTabs": false, + "tabWidth": 2 +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f2d9e00 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "mikestead.dotenv", + "csstools.postcss", + "bradlc.vscode-tailwindcss", + "Orta.vscode-jest" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3fbd647 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Next: Chrome", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + }, + { + "type": "node", + "request": "launch", + "name": "Next: Node", + "program": "${workspaceFolder}/node_modules/.bin/next", + "args": ["dev"], + "autoAttachChildProcesses": true, + "skipFiles": ["/**"], + "console": "integratedTerminal" + } + ], + "compounds": [ + { + "name": "Next: Full", + "configurations": ["Next: Node", "Next: Chrome"] + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fd897a0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,37 @@ +{ + "editor.tabSize": 2, + "editor.detectIndentation": false, + "search.exclude": { + "package-lock.json": true + }, + "editor.defaultFormatter": "dbaeumer.vscode-eslint", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": [ + "source.addMissingImports", + "source.fixAll.eslint" + ], + "eslint.validate": [ + "typescript", + "javascript", + "javascriptreact" + ], + "jest.autoRun": { + "watch": true, // Start the jest with the watch flag + "onStartup": [ + "all-tests" + ] // Run all tests upon project launch + }, + "jest.showCoverageOnLoad": true, // Show code coverage when the project is launched + "jest.showTerminalOnLaunch": false, // Don't automatically open test explorer terminal on launch + // Multiple language settings for json and jsonc files + "[json][jsonc]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "jest.jestCommandLine": "jest", + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "typescript.tsdk": "node_modules/.pnpm/typescript@4.9.4/node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f02b913 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,21 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Project wide type checking with TypeScript", + "type": "npm", + "script": "check-types", + "problemMatcher": ["$tsc"], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "clear": true, + "reveal": "never" + } + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c29ae4a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 wslyvh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d10ead --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +![image](https://user-images.githubusercontent.com/3408362/230732083-1c98e451-08af-41c2-b522-126370e8c6a5.png) + +# ⚡ TurboETH - Web3 App Template +Web3 App Template built using Next.js, RainbowKit, SIWE, Disco, and more! + +### Start Kit Examples +- [Main](https://light.turboeth.xyz) - `main` branch +- [Heavy](https://turboeth.xyz) - `integrations` branch + +Deploy TurboETH `main` directly to [Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fturbo-eth%2Ftemplate-web3-app&project-name=TurboETH&repository-name=turbo-eth&demo-title=TurboETH&env=APP_ADMINS,NEXT_PUBLIC_ALCHEMY_API_KEY,NEXTAUTH_SECRET,ETHERSCAN_API_KEY,ETHERSCAN_API_KEY_OPTIMISM,ETHERSCAN_API_KEY_ARBITRUM,ETHERSCAN_API_KEY_POLYGON,DATABASE_URL&envDescription=How%20to%20get%20these%20env%20variables%3A&envLink=https%3A%2F%2Fgithub.com%2Fturbo-eth%2Ftemplate-web3-app%2Fblob%2Fmain%2F.env.example) + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fhello-world) + +### [Documentation](https://docs.turboeth.xyz) +- Getting Started + - [Environment Variables](https://docs.turboeth.xyz/getting-started/environment) + - [JSON-RPC](https://docs.turboeth.xyz/getting-started/json-rpc) + - [WAGMI CLI](https://docs.turboeth.xyz/getting-started/wagmi-cli) + - [UI Components](https://docs.turboeth.xyz/getting-started/design-system) + - [Backend Database](https://docs.turboeth.xyz/getting-started/database) + - Core Integrations + - [🌈 RainbowKit](https://docs.turboeth.xyz/integration/rainbowkit) + - [🔏 Sign-In With Ethereum](https://docs.turboeth.xyz/integration/sign-in-with-ethereum) +- Smart Contract Integrations + - [ERC20](https://docs.turboeth.xyz/integration/smart-contract-erc20) +- API Integrations + - [Disco](https://docs.turboeth.xyz/integration/disco) + - [Etherscan](https://docs.turboeth.xyz/integration/etherscan) + +# Getting Started + +The `pnpm` CLI is the recommended package manager but `npm` and `yarn` should work too. + +```bash +pnpm install +``` + +#### Development +```bash +pnpm dev +``` + +#### Build +```bash +pnpm build +``` + +### Web3 Core +- [WAGMI CLI](https://wagmi.sh/cli/getting-started) - Automatic React Hook Generation +- [RainbowKit](https://www.rainbowkit.com/) - Wallet connection manager +- [Sign-In With Ethereum](https://login.xyz/) - Account authentication + +### Web2 Frameworks +- [Vercel](https://vercel.com/) - App Infrastructure +- [Prisma](https://www.prisma.io/) - Database ORM + +### Developer Experience +- [TypeScript](https://www.typescriptlang.org/) – Static type checker for end-to-end typesafety +- [Prettier](https://prettier.io/) – Opinionated code formatter for consistent code style +- [ESLint](https://eslint.org/) – Pluggable linter for Next.js and TypeScript + +### User Interface +- [TailwindCSS](https://tailwindcss.com) – Utility-first CSS framework for rapid UI development +- [Radix](https://www.radix-ui.com/) – Primitives like modal, popover, etc. to build a stellar user experience +- [Framer Motion](https://www.framer.com/motion/) – Motion library for React to animate components with ease +- [Lucide](https://lucide.dev/docs/lucide-react) – Beautifully simple, pixel-perfect icons + +The [ui.shadcn.com](https://ui.shadcn.com) components are included in the `/components/shared/ui` folder. + +# đŸ’ģ Developer Experience + +### 🐕 What is husky +Husky improves your git commits. + +You can use it to lint your commit messages, run tests, lint code, etc... when you commit or push. Husky supports all Git hooks. + +#### đŸĒ Hooks +- pre-commit: lint app codebase +- commit-msg: apply commintlint + +### 📋 What is commitlint + +commitlint checks if your commit messages meet the [conventional commit format](https://conventionalcommits.org). + +In general the pattern mostly looks like this: + +```sh +type(scope?): subject #scope is optional; multiple scopes are supported (current delimiter options: "/", "\" and ",") +``` + +Real world examples can look like this: + +``` +chore: run tests on travis ci +``` + +``` +fix(server): send cors headers +``` + +``` +feat(blog): add comment section +``` + +Common types according to [commitlint-config-conventional (based on the Angular convention)](https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional#type-enum) can be: + +- build +- chore +- ci +- docs +- feat +- fix +- perf +- refactor +- revert +- style +- test + +# Acknowledgements + +Original template was forked from https://github.com/wslyvh/nexth + +Thank you @wslyvh 🙏 + +
+ +Copyright 2023 [Kames Geraghty](https://twitter.com/KamesGeraghty) diff --git a/app/create/page.tsx b/app/create/page.tsx new file mode 100644 index 0000000..ab7328a --- /dev/null +++ b/app/create/page.tsx @@ -0,0 +1,67 @@ +'use client' + +import { motion } from 'framer-motion' + +import { ButtonPlaceFactoryDeploy } from '@/components/places-factory-deploy' +import PlaceFactoryEventPlaceCreated from '@/components/places-factory-event-place-created' +import { PlacesFactoriyWriteCreatePlace } from '@/components/places-factory-write-create-place' +import { FADE_DOWN_ANIMATION_VARIANTS } from '@/config/design' +import { placeFactoryContracts } from '@/data/place-factory-contracts' + +export default function PageCreate1() { + return ( + <> +
+ + +
+
+

Create Place

+

Create a new Place ERC721 smart contract.

+
+
+ +
+
+ Deployment Status: + +
+
+
+
+

Create Place Factory

+

Deploy a new PlaceFactory.sol contract to a new blockchain

+
+ + + +
+
+ {Object.values(placeFactoryContracts).map((placeFactoryContract, index) => { + const chains = Object.keys(placeFactoryContracts) + return ( +
+

ChainId: {chains[index]}

+

{placeFactoryContract}

+
+ ) + })} +
+
+
+
+ + ) +} diff --git a/app/head.tsx b/app/head.tsx new file mode 100644 index 0000000..dafdb61 --- /dev/null +++ b/app/head.tsx @@ -0,0 +1,26 @@ +import { siteConfig } from '@/config/site' + +export default function Head() { + const url = process.env.NEXTAUTH_URL || 'http://localhost:3000' + const ogUrl = new URL(`${url}/api/og`) + + return ( + <> + {`${siteConfig.name} - ${siteConfig.description}`} + + + + + + + + + + + + + + + + ) +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..81e43be --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,69 @@ +'use client' +import '@/styles/app.css' +import '@/styles/gradient.css' +import '@/styles/periphery.css' +import { Raleway } from '@next/font/google' +import { Inter as FontSans } from '@next/font/google' +import localFont from '@next/font/local' +import classNames from 'clsx' + +import { NetworkStatus } from '@/components/blockchain/network-status' +import { WalletConnect } from '@/components/blockchain/wallet-connect' +import { Footer } from '@/components/layout/footer' +import { Header } from '@/components/layout/header' +import RootProvider from '@/components/providers/root-provider' +import { Toaster } from '@/components/ui/toaster' +import { cn } from '@/lib/utils' + +const sfPro = localFont({ + src: '../assets/fonts/SF-Pro-Display-Medium.otf', + variable: '--font-sf', +}) + +const raleway = Raleway({ + subsets: ['latin'], + weight: ['100', '200', '400', '500', '600', '700', '800', '900'], + variable: '--font-raleway', +}) + +const fontSans = FontSans({ + subsets: ['latin'], + variable: '--font-sans', +}) + +export default function RootLayout({ children }: any) { + const classes = classNames('GeneralLayout', 'bg-gradient-dark min-h-[100vh] flex flex-col pb-10 lg:pb-12') + return ( + <> + + + + + <> +
+
+
{children}
+
+ +
+
+ +
+
+
+ {/* TODO: Add position controls */} + + +
+ + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..34bc2bc --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,45 @@ +'use client' + +import { motion } from 'framer-motion' +import { useAccount } from 'wagmi' + +import { WalletConnect } from '@/components/blockchain/wallet-connect' +import { BranchIsWalletConnected } from '@/components/shared/branch-is-wallet-connected' +import { DiscoProfileBasic } from '@/integrations/disco/components/disco-profile-basic' +import { DiscoProfileCredentials } from '@/integrations/disco/components/disco-profile-credentials' +import { BranchIsAuthenticated } from '@/integrations/siwe/components/branch-is-authenticated' +import { ButtonSIWELogin } from '@/integrations/siwe/components/button-siwe-login' + +export default function Home() { + const { address } = useAccount() + + return ( + <> +
+
+ + +
+ +

🎒 Data Backpack

+
+ +
+ <> +
+

đŸĒŠ

+ +

+ Accessing the Disco API requires
+ authenticating with an Ethereum Account. +

+
+ +
+ +
+
+
+ + ) +} diff --git a/app/place/[address]/page.tsx b/app/place/[address]/page.tsx new file mode 100644 index 0000000..b21e68f --- /dev/null +++ b/app/place/[address]/page.tsx @@ -0,0 +1,78 @@ +'use client' + +import { useEffect, useState } from 'react' + +import { motion } from 'framer-motion' +import Image from 'next/image' + +import ButtonPlaceMint from '@/components/button-place-mint' +import { FADE_DOWN_ANIMATION_VARIANTS } from '@/config/design' +import { collection } from '@/data/collection' +import { usePlaceNextTokenId } from '@/lib/blockchain' +import { useERC721ContractMetadata } from '@/lib/hooks/use-erc721-contract-uri' + +export default function PageCreate({ params }: { params: { address: `0x${string}` } }) { + const [contractDetails, setContractDetails] = useState() + useEffect(() => { + if (collection) { + collection.filter((place) => { + if (place.address.toLowerCase() === params.address.toLowerCase()) { + setContractDetails(place) + } + }) + } + }, [collection]) + + const metadata = useERC721ContractMetadata({ + address: params.address as `0x${string}`, + }) + + const { data: totalSupply } = usePlaceNextTokenId({ + address: params.address as `0x${string}`, + }) + + return ( + <> +
+ + +
+
+ {metadata?.name &&

{metadata?.name}

} + {metadata?.description &&

{metadata?.description}

} +
+ Price: Ξ {contractDetails?.price} + + Mint Place + +
+ {totalSupply &&

Total Minted: {Number(totalSupply) - 1}

} +
+
+ Place +
+
+
+
+
+ + ) +} diff --git a/assets/fonts/Inter-Bold.ttf b/assets/fonts/Inter-Bold.ttf new file mode 100644 index 0000000..8e82c70 Binary files /dev/null and b/assets/fonts/Inter-Bold.ttf differ diff --git a/assets/fonts/Inter-Regular.ttf b/assets/fonts/Inter-Regular.ttf new file mode 100644 index 0000000..8d4eebf Binary files /dev/null and b/assets/fonts/Inter-Regular.ttf differ diff --git a/assets/fonts/SF-Pro-Display-Medium.otf b/assets/fonts/SF-Pro-Display-Medium.otf new file mode 100644 index 0000000..b2f7dac Binary files /dev/null and b/assets/fonts/SF-Pro-Display-Medium.otf differ diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..4fedde6 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +module.exports = { extends: ['@commitlint/config-conventional'] } diff --git a/components/blockchain/network-status.tsx b/components/blockchain/network-status.tsx new file mode 100644 index 0000000..d215aab --- /dev/null +++ b/components/blockchain/network-status.tsx @@ -0,0 +1,34 @@ +import React from 'react' + +import classNames from 'clsx' +import { useBlockNumber, useNetwork } from 'wagmi' + +import { GetNetworkColor } from '@/lib/utils/get-network-color' + +import { LinkComponent } from '../shared/link-component' + +export function NetworkStatus({ className }: any) { + const block = useBlockNumber({ watch: true }) + const network = useNetwork() + const explorerUrl = network.chain?.blockExplorers?.default.url + const classes = classNames(className, 'NetworkStatus', 'dark:bg-gray-800 bg-gray-100 z-10 flex shadow-md items-center rounded-full overflow-hidden') + const classesBadge = classNames( + 'Badge uppercase text-xs font-bold tracking-wider leading-none rounded-full px-2', + `bg-${GetNetworkColor(network.chain?.network)}-200`, + `text-${GetNetworkColor(network.chain?.network)}-700 dark:text-${GetNetworkColor(network.chain?.network)}-700 py-2` + ) + + return ( +
+ + {network.chain?.name ?? 'Ethereum'} + + {explorerUrl && ( + + <>#{block.data} + + )} + {!explorerUrl && # {block.data}} +
+ ) +} diff --git a/components/blockchain/wallet-connect.tsx b/components/blockchain/wallet-connect.tsx new file mode 100644 index 0000000..7ff8ced --- /dev/null +++ b/components/blockchain/wallet-connect.tsx @@ -0,0 +1,26 @@ +'use client' +import * as React from 'react' + +import { ConnectButton } from '@rainbow-me/rainbowkit' + +interface WalletConnectProps { + className?: string +} + +export const WalletConnect = ({ className }: WalletConnectProps) => { + return ( + + + + ) +} diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx new file mode 100644 index 0000000..610b02a --- /dev/null +++ b/components/layout/footer.tsx @@ -0,0 +1,34 @@ +import React from 'react' + +import classNames from 'clsx' +import { FaGithub, FaTwitter } from 'react-icons/fa' + +import { siteConfig } from '@/config/site' + +import { LinkComponent } from '../shared/link-component' + +interface Props { + className?: string +} + +export function Footer(props: Props) { + const classes = classNames(props.className, 'Footer', 'px-4 py-6 flex flex-col justify-center items-center') + + return ( + + ) +} diff --git a/components/layout/header.tsx b/components/layout/header.tsx new file mode 100644 index 0000000..a9eb69c --- /dev/null +++ b/components/layout/header.tsx @@ -0,0 +1,65 @@ +import React from 'react' + +import classNames from 'clsx' + +import { siteConfig } from '@/config/site' +import { BranchIsAuthenticated } from '@/integrations/siwe/components/branch-is-authenticated' +import { ButtonSIWELogin } from '@/integrations/siwe/components/button-siwe-login' +import { ButtonSIWELogout } from '@/integrations/siwe/components/button-siwe-logout' +import useScroll from '@/lib/hooks/use-scroll' + +import { LinkComponent } from '../shared/link-component' +import { ResponsiveMobileAndDesktop } from '../shared/responsive-mobile-and-desktop' +import { ThemeToggle } from '../shared/theme-toggle' + +interface Props { + className?: string +} + +export function Header(props: Props) { + const scrolled = useScroll(50) + const classes = classNames( + props.className, + 'Header', + 'fixed top-0 w-full', + 'px-6 lg:px-10 py-3 mb-8 flex items-center', + { + 'border-b border-gray-200 bg-white/50 backdrop-blur-xl dark:bg-black/50 dark:border-gray-800': scrolled, + }, + 'z-30 transition-all' + ) + return ( +
+ + <> +
+ + {siteConfig.emoji} + +
+ + Create + + +
+
+ + <> + + {siteConfig.emoji} +

{siteConfig.name}

+
+
+ +
+ + + + + +
+ +
+
+ ) +} diff --git a/components/providers/rainbow-kit.tsx b/components/providers/rainbow-kit.tsx new file mode 100644 index 0000000..b0d718d --- /dev/null +++ b/components/providers/rainbow-kit.tsx @@ -0,0 +1,47 @@ +'use client' +import '@rainbow-me/rainbowkit/styles.css' +import { ReactNode } from 'react' + +import { RainbowKitProvider, darkTheme, lightTheme } from '@rainbow-me/rainbowkit' +import { connectorsForWallets } from '@rainbow-me/rainbowkit' +import { coinbaseWallet, injectedWallet, metaMaskWallet, rainbowWallet, walletConnectWallet } from '@rainbow-me/rainbowkit/wallets' +import { WagmiConfig, createClient } from 'wagmi' + +import { chains, provider } from '@/config/networks' +import { siteConfig } from '@/config/site' +import { useColorMode } from '@/lib/state/color-mode' + +interface Props { + children: ReactNode + autoConnect?: boolean +} + +const connectors = connectorsForWallets([ + { + groupName: 'Recommended', + wallets: [ + injectedWallet({ chains }), + metaMaskWallet({ chains }), + rainbowWallet({ chains }), + coinbaseWallet({ chains, appName: siteConfig.name }), + walletConnectWallet({ chains }), + ], + }, +]) + +const wagmiClient = createClient({ + autoConnect: true, + connectors, + provider, +}) + +export function RainbowKit(props: Props) { + const [colorMode] = useColorMode() + return ( + + + {props.children} + + + ) +} diff --git a/components/providers/root-provider.tsx b/components/providers/root-provider.tsx new file mode 100644 index 0000000..8f858a7 --- /dev/null +++ b/components/providers/root-provider.tsx @@ -0,0 +1,35 @@ +'use client' + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { fetchJson } from 'ethers/lib/utils.js' +import { ThemeProvider } from 'next-themes' +import { SWRConfig } from 'swr' + +import { RainbowKit } from '@/components/providers/rainbow-kit' +import { useIsMounted } from '@/lib/hooks/use-is-mounted' + +const queryClient = new QueryClient() +interface RootProviderProps { + children: React.ReactNode +} + +export default function RootProvider({ children }: RootProviderProps) { + const isMounted = useIsMounted() + return ( + { + console.error(err) + }, + }}> + {isMounted && ( + + + {children} + + + )} + + ) +} diff --git a/components/shared/Component.tsx b/components/shared/Component.tsx new file mode 100644 index 0000000..ef9a764 --- /dev/null +++ b/components/shared/Component.tsx @@ -0,0 +1,41 @@ +/* +Notes(@kames): This is a helper component that will render a React component or a React element. +It might be a good idea to move this to a separate package or remove it completely. +*/ + +// @ts-nocheck +import React, { ReactElement, ReactNode } from 'react' + +export function isClassComponent(component: unknown) { + return typeof component === 'function' && !!component.prototype && component.prototype.isReactComponent +} + +export function isFunctionComponent(component: any) { + return typeof component === 'function' && String(component).includes('return React.createElement') +} + +export function isInlineFunctionComponent(component: any) { + return typeof component === 'function' +} + +export function isReactComponent(component: any) { + return !!(isClassComponent(component) || isFunctionComponent(component) || isInlineFunctionComponent(component)) +} + +export function isElement(element) { + return React.isValidElement(element) +} + +export function isDOMTypeElement(element) { + return isElement(element) && typeof element.type === 'string' +} + +export function isCompositeTypeElement(element) { + return isElement(element) && typeof element.type === 'export function' +} + +export const Component = (component: ReactElement | ReactNode, props: any): React.ReactNode => { + return isReactComponent(component) ? React.createElement(component, props) : isElement(component) ? React.cloneElement(component, props) : <> +} + +export default Component diff --git a/components/shared/branch-color-mode.tsx b/components/shared/branch-color-mode.tsx new file mode 100644 index 0000000..e44e9a8 --- /dev/null +++ b/components/shared/branch-color-mode.tsx @@ -0,0 +1,7 @@ +import { useColorMode } from '@/lib/state/color-mode' + +export function BranchColorMode({ children }: any) { + const [colorMode] = useColorMode() + + return colorMode === 'light' ? children[0] : children[1] +} diff --git a/components/shared/branch-is-wallet-connected.tsx b/components/shared/branch-is-wallet-connected.tsx new file mode 100644 index 0000000..7c0abf7 --- /dev/null +++ b/components/shared/branch-is-wallet-connected.tsx @@ -0,0 +1,17 @@ +import React from 'react' + +import { useAccount } from 'wagmi' + +interface BranchIsWalletConnectedProps { + children?: React.ReactElement | Array +} + +// @ts-ignore +export function BranchIsWalletConnected({ children }: BranchIsWalletConnectedProps): React.ReactElement | null { + const { address } = useAccount() + + if (!children) return <> + if (address && children && !Array.isArray(children)) return children + if (address && Array.isArray(children)) return children[0] + if (!address && Array.isArray(children)) return children[1] +} diff --git a/components/shared/icons.tsx b/components/shared/icons.tsx new file mode 100644 index 0000000..819713a --- /dev/null +++ b/components/shared/icons.tsx @@ -0,0 +1,22 @@ +import { Laptop, type Icon as LucideIcon, LucideProps, Moon, SunMedium, Twitter } from 'lucide-react' + +export type Icon = LucideIcon + +export const Icons = { + sun: SunMedium, + moon: Moon, + laptop: Laptop, + twitter: Twitter, + logo: (props: LucideProps) => ( + + + + ), + gitHub: (props: LucideProps) => ( + + + + ), +} diff --git a/components/shared/link-component.tsx b/components/shared/link-component.tsx new file mode 100644 index 0000000..081bccc --- /dev/null +++ b/components/shared/link-component.tsx @@ -0,0 +1,37 @@ +'use client' +import React, { ReactNode } from 'react' + +import classNames from 'clsx' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { useRouter } from 'next/router' + +interface LinkComponentProps { + href: string + children: ReactNode + isExternal?: boolean + className?: string + target?: string +} + +export function LinkComponent({ href, children, isExternal, className, target = '_blank' }: LinkComponentProps) { + const pathname = usePathname() + const classes = classNames(className, 'LinkComponent', { + active: pathname === href, + }) + const isExternalEnabed = href.match(/^([a-z0-9]*:|.{0})\/\/.*$/) || isExternal + + if (isExternalEnabed) { + return ( + + {children} + + ) + } + + return ( + + {children} + + ) +} diff --git a/components/shared/responsive-mobile-and-desktop.tsx b/components/shared/responsive-mobile-and-desktop.tsx new file mode 100644 index 0000000..9680a23 --- /dev/null +++ b/components/shared/responsive-mobile-and-desktop.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' + +import { useMediaQuery } from 'react-responsive' +interface ResponsiveMobileAndDesktopProps { + children?: React.ReactElement | Array +} + +export const ResponsiveMobileAndDesktop = ({ children }: ResponsiveMobileAndDesktopProps) => { + const isTabletOrMobile = useMediaQuery({ query: '(max-width: 1224px)' }) + + if (!children || (Array.isArray(children) && children.length != 2)) return Invalid Component Branch + if (isTabletOrMobile && children && Array.isArray(children)) return children[0] + if (!isTabletOrMobile && children && Array.isArray(children)) return children[1] + return null +} diff --git a/components/shared/theme-toggle.tsx b/components/shared/theme-toggle.tsx new file mode 100644 index 0000000..509c964 --- /dev/null +++ b/components/shared/theme-toggle.tsx @@ -0,0 +1,61 @@ +import * as React from 'react' + +import { useTheme } from 'next-themes' + +import { BranchColorMode } from '@/components/shared/branch-color-mode' +import { Icons } from '@/components/shared/icons' +import { Button } from '@/components/ui/button' +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { useColorMode } from '@/lib/state/color-mode' + +export function ThemeToggle() { + const { setTheme } = useTheme() + const [colorMode, toggleMode, setMode] = useColorMode() + + const handleSetLightTheme = (_e: any) => { + setTheme('light') + setMode('light') + } + + const handleSetDarkTheme = (_e: any) => { + setTheme('dark') + setMode('dark') + } + + const handleSetSystemTheme = (_e: any) => { + setTheme('system') + setMode('system') + } + + React.useEffect(() => { + colorMode === 'system' ? setTheme('system') : colorMode === 'dark' ? setTheme('dark') : setTheme('light') + }, [colorMode]) + + return ( + + + + + + + + Light + + + + Dark + + + + System + + + + ) +} diff --git a/components/shared/time-from-epoch.tsx b/components/shared/time-from-epoch.tsx new file mode 100644 index 0000000..fb1aeff --- /dev/null +++ b/components/shared/time-from-epoch.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' + +import classNames from 'clsx' +import { DateTime } from 'luxon' + +interface TimeFromEpochProps { + className?: string + epoch?: number | string +} + +export const TimeFromEpoch = ({ className, epoch }: TimeFromEpochProps) => { + const [timestamp, setTimestamp] = React.useState() + React.useEffect(() => { + if (epoch) { + setTimestamp(DateTime.fromSeconds(Number(epoch)).toLocaleString(DateTime.DATETIME_MED)) + } + }, []) + const containerClassName = classNames(className, 'TimeFromEpoch') + return {timestamp} +} + +export default TimeFromEpoch diff --git a/components/shared/time-from-utc.tsx b/components/shared/time-from-utc.tsx new file mode 100644 index 0000000..5906aaa --- /dev/null +++ b/components/shared/time-from-utc.tsx @@ -0,0 +1,20 @@ +import * as React from 'react' + +import classNames from 'clsx' +import { DateTime } from 'luxon' + +interface TimeFromUtcProps { + className?: string + date: string +} + +export const TimeFromUtc = ({ className, date }: TimeFromUtcProps) => { + const [timestamp, setTimestamp] = React.useState() + React.useEffect(() => { + if (date) { + setTimestamp(DateTime.fromISO(date).toLocaleString(DateTime.DATETIME_MED)) + } + }, []) + const containerClassName = classNames(className, 'TimeFromUtc') + return {timestamp} +} diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..b851bcb --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,117 @@ +'use client' + +import * as React from 'react' + +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '@/lib/utils' + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = ({ className, children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => ( + +
{children}
+
+) +AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..794c6f4 --- /dev/null +++ b/components/ui/aspect-ratio.tsx @@ -0,0 +1,7 @@ +'use client' + +import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..717c137 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,33 @@ +'use client' + +import * as React from 'react' + +import * as AvatarPrimivite from '@radix-ui/react-avatar' + +import { cn } from '@/lib/utils' + +const Avatar = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( + + ) +) +Avatar.displayName = AvatarPrimivite.Root.displayName + +const AvatarImage = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => +) +AvatarImage.displayName = AvatarPrimivite.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimivite.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..af956ca --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,39 @@ +import * as React from 'react' + +import { VariantProps, cva } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const buttonVariants = cva( + 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800', + { + variants: { + variant: { + default: 'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900', + outline: 'bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100', + subtle: 'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100', + ghost: + 'bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent', + link: 'bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent', + }, + size: { + default: 'h-10 py-2 px-4', + sm: 'h-9 px-2 rounded-md', + lg: 'h-11 px-8 rounded-md', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +) + +export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps {} + +const Button = React.forwardRef(({ className, variant, size, ...props }, ref) => { + return