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({
);
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],