diff --git a/.gitignore b/.gitignore index d74f21a..a5b92d4 100644 --- a/.gitignore +++ b/.gitignore @@ -23,8 +23,6 @@ coverage.json .vercel # Build Outputs -.next/ -out/ build dist diff --git a/README.md b/README.md index 9c92a3f..ed4682b 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,25 @@ bun run dev The development server will be available at http://localhost:3000/. The smart contracts are in the `contracts` directory, and the frontend is in the `apps` directory. + +## How to deploy + +### Build Settings + +- **Docker Image:** + `node:lts` + +- **Repository:** + [https://github.com/BlossomLabs/councilhaus](https://github.com/BlossomLabs/councilhaus) + +- **Build Command:** + ```bash + npm install -g bun && HUSKY=0 bun i && bun run --cwd apps/web/ build + ``` + +- **Publish Directory:** + `apps/web/build` + +### Environment Variables + +- Use `VITE_WALLETCONNECT_PROJECT_ID` for your wallet connect project ID. diff --git a/apps/web/.gitignore b/apps/web/.gitignore index f886745..6cb74a4 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -9,10 +9,6 @@ # testing /coverage -# next.js -/.next/ -/out/ - # production /build @@ -33,4 +29,3 @@ yarn-error.log* # typescript *.tsbuildinfo -next-env.d.ts diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs deleted file mode 100644 index ed677c3..0000000 --- a/apps/web/next.config.mjs +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - transpilePackages: ["@repo/ui"], - distDir: "build", - output: "export", - trailingSlash: true, - images: { - unoptimized: true, - }, -}; - -export default nextConfig; diff --git a/apps/web/package.json b/apps/web/package.json index 3c2acd7..bdf58e6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,25 +1,28 @@ { "name": "web", - "version": "1.0.0", + "version": "1.1.0", "private": true, + "sideEffects": false, + "type": "module", "scripts": { - "dev": "next dev --port 3000", - "build": "next build", - "start": "next start" + "build": "remix vite:build", + "dev": "remix vite:dev -l info" }, "dependencies": { "@rainbow-me/rainbowkit": "^2.1.5", + "@remix-run/node": "^2.12.1", + "@remix-run/react": "^2.12.1", "@repo/ui": "*", "@tanstack/react-query": "^5.55.4", "ethereum-blockies-base64": "^1.0.2", "graphql-request": "^7.1.0", - "next": "^14.2.8", "react": "^18", "react-dom": "^18", "viem": "2.x", "wagmi": "^2.12.8" }, "devDependencies": { + "@remix-run/dev": "^2.12.1", "@repo/typescript-config": "*", "@types/node": "^20", "@types/react": "^18", @@ -27,6 +30,8 @@ "autoprefixer": "^10", "postcss": "^8", "tailwindcss": "^3.4.10", - "typescript": "^5" + "typescript": "^5", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^5.0.0" } } diff --git a/apps/web/src/app/entry.client.tsx b/apps/web/src/app/entry.client.tsx new file mode 100644 index 0000000..f91c526 --- /dev/null +++ b/apps/web/src/app/entry.client.tsx @@ -0,0 +1,18 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser } from "@remix-run/react"; +import { StrictMode, startTransition } from "react"; +import { hydrateRoot } from "react-dom/client"; + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx deleted file mode 100644 index 58b4b0b..0000000 --- a/apps/web/src/app/layout.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import "@repo/ui/globals.css"; -import { ToastProvider } from "@repo/ui/components/ui/toast"; -import { Toaster } from "@repo/ui/components/ui/toaster"; -import type { Metadata } from "next"; -import { Newsreader } from "next/font/google"; -import { SITE_DESCRIPTION, SITE_NAME } from "../../../../constants"; -import Footer from "../components/Footer"; -import Header from "../components/Header"; -import { WalletProvider } from "../context/WalletProvider"; - -const newsreader = Newsreader({ - subsets: ["latin"], - weight: ["400", "500", "600"], - display: "swap", - variable: "--font-newsreader", -}); - -export const metadata: Metadata = { - title: SITE_NAME, - description: SITE_DESCRIPTION, -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}): JSX.Element { - return ( - - - - -
-
-
- {children} -
- -
-
-
- - - ); -} diff --git a/apps/web/src/app/root.tsx b/apps/web/src/app/root.tsx new file mode 100644 index 0000000..00659aa --- /dev/null +++ b/apps/web/src/app/root.tsx @@ -0,0 +1,74 @@ +import type { MetaFunction } from "@remix-run/node"; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +import { ToastProvider } from "@repo/ui/components/ui/toast"; +import { Toaster } from "@repo/ui/components/ui/toaster"; +import { TooltipProvider } from "@repo/ui/components/ui/tooltip"; +import { Layout } from "../components/Layout"; +import { WalletProvider } from "../context/WalletProvider"; + +import { + SITE_DESCRIPTION, + SITE_EMOJI, + SITE_NAME, + SITE_URL, + SOCIAL_TWITTER, +} from "../../../../constants"; + +import "@repo/ui/globals.css"; +import "@rainbow-me/rainbowkit/styles.css"; + +export const meta: MetaFunction = () => [ + { + charset: "utf-8", + title: SITE_NAME, + viewport: "width=device-width,initial-scale=1", + }, + { name: "description", content: SITE_DESCRIPTION }, + { name: "image", content: SITE_EMOJI }, + { name: "og:image", content: "/opengraph-image" }, + { name: "og:title", content: SITE_NAME }, + { name: "og:description", content: SITE_DESCRIPTION }, + { name: "og:url", content: SITE_URL }, + { name: "og:type", content: "website" }, + { name: "og:site_name", content: SITE_NAME }, + { name: "og:locale", content: "en_US" }, + { name: "twitter:card", content: "summary_large_image" }, + { name: "twitter:image", content: "/opengraph-image" }, + { name: "twitter:title", content: SITE_NAME }, + { name: "twitter:description", content: SITE_DESCRIPTION }, + { name: "twitter:site", content: SOCIAL_TWITTER }, + { name: "twitter:creator", content: SOCIAL_TWITTER }, +]; + +export default function App() { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/app/routes/_index.tsx b/apps/web/src/app/routes/_index.tsx new file mode 100644 index 0000000..ab1361d --- /dev/null +++ b/apps/web/src/app/routes/_index.tsx @@ -0,0 +1,13 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { DEFAULT_COUNCIL_ADDRESS, NETWORK } from "../../../../../constants"; + +export default function IndexPage() { + const navigate = useNavigate(); + + useEffect(() => { + navigate(`/c/${NETWORK}/${DEFAULT_COUNCIL_ADDRESS}`); + }, [navigate]); + + return null; +} diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/routes/c.$chain.$council.tsx similarity index 68% rename from apps/web/src/app/page.tsx rename to apps/web/src/app/routes/c.$chain.$council.tsx index 95ce524..cb6f352 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/routes/c.$chain.$council.tsx @@ -1,37 +1,53 @@ "use client"; +import { useParams } from "@remix-run/react"; import { Badge } from "@repo/ui/components/ui/badge"; import { Label } from "@repo/ui/components/ui/label"; import { Skeleton } from "@repo/ui/components/ui/skeleton"; import { cn } from "@repo/ui/lib/utils"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import { getAddress } from "viem"; import { useAccount, useChains } from "wagmi"; -import { DEFAULT_COUNCIL_ADDRESS, NETWORK } from "../../../../constants"; -import { CouncilImage } from "../components/CouncilImage"; -import { CouncilName } from "../components/CouncilName"; -import VotingCard from "../components/VotingCard"; -import { useAllocation } from "../hooks/useAllocation"; -import { useCouncil } from "../hooks/useCouncil"; +import { NETWORK } from "../../../../../constants"; +import { CouncilImage } from "../../components/CouncilImage"; +import { CouncilName } from "../../components/CouncilName"; +import VotingCard from "../../components/VotingCard"; +import { useAllocation } from "../../hooks/useAllocation"; +import { useCouncil } from "../../hooks/useCouncil"; -export default function Page() { - const router = useRouter(); - const [council, setCouncil] = useState<`0x${string}` | undefined>(undefined); +export default function CouncilPage() { + const { chain, council } = useParams(); + const navigate = useNavigate(); + const normalizedAddress = council ? getAddress(council) : null; useEffect(() => { - // Ensure the code runs only on the client side - if (!window.location.hash) { - router.push(`#${DEFAULT_COUNCIL_ADDRESS}`); + // Redirect if the chain is unsupported + if (chain !== NETWORK) { + navigate("/404"); + return; } - // Set the council value once the hash is present - const address = getAddress( - window.location.hash?.slice(1) || DEFAULT_COUNCIL_ADDRESS, - ); - setCouncil(address); - }, [router]); + // Redirect if council address is invalid + if (!normalizedAddress) { + navigate("/404"); + return; + } + + // Redirect if the normalized address doesn't match the URL + if (normalizedAddress !== council) { + navigate(`/c/${chain}/${normalizedAddress}`, { replace: true }); + } + }, [chain, council, normalizedAddress, navigate]); + + if (!normalizedAddress) { + return null; + } + + return ; +} + +function CouncilPageContent({ council }: { council: `0x${string}` }) { // Fetch data when the council is available const { address } = useAccount(); const { @@ -52,16 +68,17 @@ export default function Page() { return (
- - +
{totalVotingPower ? ( !address ? ( @@ -129,14 +146,22 @@ function ContractLinks({
- + Council - + - + Pool - +
); diff --git a/apps/web/src/components/AddressAvatar.tsx b/apps/web/src/components/AddressAvatar.tsx index ea4f334..083abeb 100644 --- a/apps/web/src/components/AddressAvatar.tsx +++ b/apps/web/src/components/AddressAvatar.tsx @@ -1,6 +1,5 @@ import { cn } from "@repo/ui/lib/utils"; import makeBlockie from "ethereum-blockies-base64"; -import Image from "next/image"; import { useMemo } from "react"; import { isAddress } from "viem"; import { normalize } from "viem/ens"; @@ -56,7 +55,7 @@ function AddressAvatar({ }, [ensAvatar, address, isLoading]); return ( - Built with ❤️ by{" "}

⚡️ Powered by{" "}

diff --git a/apps/web/src/components/Layout.tsx b/apps/web/src/components/Layout.tsx new file mode 100644 index 0000000..4675b9b --- /dev/null +++ b/apps/web/src/components/Layout.tsx @@ -0,0 +1,15 @@ +import type { PropsWithChildren } from "react"; +import Footer from "./Footer"; +import Header from "./Header"; + +export function Layout(props: PropsWithChildren) { + return ( +
+
+
+ {props.children} +
+
+
+ ); +} diff --git a/apps/web/src/components/VotingCard/index.tsx b/apps/web/src/components/VotingCard/index.tsx index 6f595b3..87c53e1 100644 --- a/apps/web/src/components/VotingCard/index.tsx +++ b/apps/web/src/components/VotingCard/index.tsx @@ -101,7 +101,7 @@ const VotingCard = ({ return ( - + Which project is doing better? diff --git a/apps/web/src/utils/wallet.ts b/apps/web/src/utils/wallet.ts index e745a8f..3f558c0 100644 --- a/apps/web/src/utils/wallet.ts +++ b/apps/web/src/utils/wallet.ts @@ -11,10 +11,10 @@ import { NETWORK } from "../../../../constants"; const chain = NETWORK === "optimism" ? optimism : base; export const WALLETCONNECT_PROJECT_ID = - process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID ?? ""; + import.meta.env.VITE_WALLETCONNECT_PROJECT_ID ?? ""; if (!WALLETCONNECT_PROJECT_ID) { console.warn( - "You need to provide a NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID env variable", + "You need to provide a VITE_WALLETCONNECT_PROJECT_ID env variable", ); } @@ -24,7 +24,7 @@ export const WALLETCONNECT_CONFIG: RainbowKitConfig = getDefaultConfig({ appName: SITE_NAME, projectId: WALLETCONNECT_PROJECT_ID || "dummy", chains: [chain], - ssr: true, + ssr: false, }); export const mainnetConfig = createConfig({ diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 0f919a9..e559d65 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,23 +1,25 @@ { - "extends": "@repo/typescript-config/nextjs.json", + "include": ["env.d.ts", "**/*.ts", "**/*.tsx"], "compilerOptions": { - "plugins": [ - { - "name": "next" - } - ], + "lib": ["DOM", "DOM.Iterable", "ES2023"], + "types": ["@remix-run/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", "paths": { - "~/*": ["./*"] - } - }, - "include": [ - "next-env.d.ts", - "next.config.mjs", - "postcss.config.mjs", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - "build/types/**/*.ts" - ], - "exclude": ["node_modules"] + "@/*": ["./app/*"] + }, + + // Remix takes care of building everything in `remix build`. + "noEmit": true + } } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..e8be755 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,21 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + appDirectory: "src/app", + ssr: false, + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], + server: { + port: 3000, + }, +}); diff --git a/bun.lockb b/bun.lockb index 1a5084d..e85ad3f 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/constants.ts b/constants.ts index b9662c6..0e7665a 100644 --- a/constants.ts +++ b/constants.ts @@ -1,6 +1,10 @@ export const SITE_NAME = "Councilhaus"; export const SITE_DESCRIPTION = "Democratically allocate a budget across projects"; +export const SITE_EMOJI = "🏜️"; +export const SITE_URL = "https://council.haus"; +export const SOCIAL_TWITTER = "blossom_labs"; +export const SOCIAL_GITHUB = "BlossomLabs/councilhaus"; export const DEFAULT_COUNCIL_ADDRESS = "0xfa942226e5dd1e2d4d99014982846786b09939da"; diff --git a/package.json b/package.json index a00fd0f..9c9fdcf 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ "format-and-lint:fix": "biome check . --write", "ui": "bun run --cwd packages/ui ui", "prepare": "husky", - "install:web": "bun run --cwd apps/web install", - "install:contracts": "bun run --cwd contracts/councilhaus install" + "install:web": "bun install --cwd apps/web", + "install:contracts": "bun install --cwd contracts/councilhaus" }, "devDependencies": { "@biomejs/biome": "1.8.3", @@ -22,7 +22,7 @@ "engines": { "node": ">=18" }, - "packageManager": "bun@1.1.4", + "packageManager": "bun@1.1.10", "workspaces": ["apps/*", "contracts/*", "packages/*"], "license": "AGPL-3.0-or-later" } diff --git a/packages/ui/package.json b/packages/ui/package.json index 47bd813..5c4b2f7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -20,9 +20,11 @@ "typescript": "^5" }, "dependencies": { + "@fontsource/newsreader": "^5.1.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.2.1", + "@radix-ui/react-tooltip": "^1.1.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "lucide-react": "^0.395.0", diff --git a/packages/ui/src/components/ui/tooltip.tsx b/packages/ui/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..a9255e5 --- /dev/null +++ b/packages/ui/src/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +"use client"; + +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; +import * as React from "react"; + +import { cn } from "@repo/ui/lib/utils"; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/packages/ui/src/globals.css b/packages/ui/src/globals.css index f199f82..de0081b 100644 --- a/packages/ui/src/globals.css +++ b/packages/ui/src/globals.css @@ -1,9 +1,16 @@ +@import "@fontsource/newsreader/400.css"; +@import "@fontsource/newsreader/500.css"; +@import "@fontsource/newsreader/600.css"; +@import "@fontsource/newsreader/700.css"; +@import "@fontsource/newsreader/800.css"; + @tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { + --font-newsreader: "Newsreader", serif; --radius: 0.5rem; } } @@ -13,7 +20,7 @@ @apply border-border; } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground font-newsreader; } } diff --git a/packages/ui/tailwind.config.ts b/packages/ui/tailwind.config.ts index 5172aa1..c7924ee 100644 --- a/packages/ui/tailwind.config.ts +++ b/packages/ui/tailwind.config.ts @@ -20,10 +20,10 @@ const config = { "2xl": "1400px", }, }, + fontFamily: { + newsreader: ["var(--font-newsreader)"], + }, extend: { - fontFamily: { - newsreader: ["var(--font-newsreader)", "serif"], - }, colors: { border: colors.yellow[500], input: colors.gray[800],