diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d10448c..0187a55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: node-version: "20" - name: Install dependencies - run: npm install --legacy-peer-deps + run: npm install --force - name: Run build run: npm run build \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 53fed30..d072f9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "blockopoly", "version": "0.1.0", "dependencies": { + "@starknet-react/chains": "^3.1.3", + "framer-motion": "^12.9.4", + "lucide-react": "^0.507.0", "next": "15.3.1", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -870,6 +873,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@starknet-react/chains": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@starknet-react/chains/-/chains-3.1.3.tgz", + "integrity": "sha512-b16VQyxqZXfiVmlKEkjfg+Oj8fdSnGWh1KU87O/unn6NpmaD9h511az1Cs6aW/j3qCIF1o5CrqfEnU1NWV7MVA==" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -3116,6 +3124,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.9.4", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.9.4.tgz", + "integrity": "sha512-yaeGDmGQ3eCQEwZ95/pRQMaSh/Q4E2CK6JYOclG/PdjyQad0MULJ+JFVV8911Fl5a6tF6o0wgW8Dpl5Qx4Adjg==", + "dependencies": { + "motion-dom": "^12.9.4", + "motion-utils": "^12.9.4", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4269,6 +4303,14 @@ "loose-envify": "cli.js" } }, + "node_modules/lucide-react": { + "version": "0.507.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.507.0.tgz", + "integrity": "sha512-XfgE6gvAHwAtnbUvWiTTHx4S3VGR+cUJHEc0vrh9Ogu672I1Tue2+Cp/8JJqpytgcBHAB1FVI297W4XGNwc2dQ==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4326,6 +4368,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "12.9.4", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.9.4.tgz", + "integrity": "sha512-25TWkQPj5I18m+qVjXGtCsxboY11DaRC5HMjd29tHKExazW4Zf4XtAagBdLpyKsVuAxEQ6cx5/E4AB21PFpLnQ==", + "dependencies": { + "motion-utils": "^12.9.4" + } + }, + "node_modules/motion-utils": { + "version": "12.9.4", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.9.4.tgz", + "integrity": "sha512-BW3I65zeM76CMsfh3kHid9ansEJk9Qvl+K5cu4DVHKGsI52n76OJ4z2CUJUV+Mn3uEP9k1JJA3tClG0ggSrRcg==" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 8b35a0c..e8cb270 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,23 @@ "lint": "next lint" }, "dependencies": { + "@starknet-react/chains": "^3.1.3", + "@starknet-react/core": "^3.7.4", + "framer-motion": "^12.9.4", + "lucide-react": "^0.507.0", + "next": "15.3.1", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.3.1" + "react-dom": "^19.0.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.3.1", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/src/app/components/icons/logo.tsx b/src/app/components/icons/logo.tsx new file mode 100644 index 0000000..a2c12dd --- /dev/null +++ b/src/app/components/icons/logo.tsx @@ -0,0 +1,16 @@ +export default function Logo() { + return ( + + + + ); +} diff --git a/src/app/components/navbar.tsx b/src/app/components/navbar.tsx new file mode 100644 index 0000000..632a532 --- /dev/null +++ b/src/app/components/navbar.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { MoreVertical, House, Volume2 } from "lucide-react"; +import { useWalletContext } from "./walletProvider"; +import AnimationWrapper from "../motion/animation-wrapper"; +import WalletConnectModal from "./wallet-connect-modal"; +import WalletDisconnectModal from "./wallet-disconnect-modal"; +import Logo from "./icons/logo"; + +export default function Navbar() { + const [isConnectModalOpen, setIsConnectModalOpen] = useState(false); + const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + const { account, connectWallet, disconnectWallet, connectors } = + useWalletContext(); + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsDropdownOpen(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + const toggleDropdown = () => setIsDropdownOpen(!isDropdownOpen); + const handleWalletSelect = (walletId: string) => { + const connector = connectors.find((c) => c.id === walletId); + if (connector) { + connectWallet(connector); // invoke Starknet-React’s useConnect() :contentReference[oaicite:3]{index=3} + } + setIsConnectModalOpen(false); + }; + const handleConnectWallet = () => { + setIsConnectModalOpen(true); + }; + const handleWalletClick = () => { + setIsDisconnectModalOpen(true); + }; + const handleDisconnect = () => { + disconnectWallet(); // real Starknet-React disconnect :contentReference[oaicite:4]{index=4} + setIsDisconnectModalOpen(false); + }; + + return ( + <> +
+
+ + + + + + + {/* Wallet Connection Button or Connected Wallet */} + +
+
+ +
+
+ +
+ + {!account ? ( + + ) : ( +
+
+
+ Wallet Avatar +
+ + + {account.slice(0, 6)}…{account.slice(-4)} + + + +
+ + {/* Custom Dropdown Menu */} + + {isDropdownOpen && ( +
+
+ + + + + +
+
+ )} +
+ )} +
+
+ + {/* Mobile Menu Button */} + +
+ + {!account ? ( + + ) : ( +
+
+ Wallet Avatar +
+ + + {account.slice(0, 6)}…{account.slice(-4)} + +
+ )} +
+
+
+
+ + setIsConnectModalOpen(false)} + onSelect={handleWalletSelect} + /> + + setIsDisconnectModalOpen(false)} + onDisconnect={handleDisconnect} + /> + + ); +} diff --git a/src/app/components/provider.tsx b/src/app/components/provider.tsx new file mode 100644 index 0000000..f84ab81 --- /dev/null +++ b/src/app/components/provider.tsx @@ -0,0 +1,36 @@ +"use client"; + +import React from "react"; + +import { sepolia, mainnet } from "@starknet-react/chains"; + +import { + StarknetConfig, + publicProvider, + argent, + braavos, + useInjectedConnectors, + voyager, +} from "@starknet-react/core"; + +export function StarknetProvider({ children }: { children: React.ReactNode }) { + const { connectors } = useInjectedConnectors({ + // Show these connectors if the user has no connector installed. + recommended: [argent(), braavos()], + // Hide recommended connectors if the user has any connector installed. + includeRecommended: "always", + // Randomize the order of the connectors. + order: "random", + }); + + return ( + + {children} + + ); +} diff --git a/src/app/components/wallet-connect-modal.tsx b/src/app/components/wallet-connect-modal.tsx new file mode 100644 index 0000000..4d8f0ca --- /dev/null +++ b/src/app/components/wallet-connect-modal.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { X } from "lucide-react"; +import Image from "next/image"; +import { useWalletContext } from "./walletProvider"; +import AnimationWrapper from "../motion/animation-wrapper"; + + +interface WalletConnectModalProps { + isOpen: boolean; + onClose: () => void; + onSelect: (wallet: string) => void; +} + +export default function WalletConnectModal({ + isOpen, + onClose, +}: WalletConnectModalProps) { + const [selectedWallet, setSelectedWallet] = useState(null); + const { connectors, connectAsync} = useWalletContext(); + const handleSelect = (walletId: string) => { + setSelectedWallet(walletId); + }; + // ② On confirm, look up the connector object and call connectWallet + const handleConfirm = async () => { + if (!selectedWallet) return; + const connector = connectors.find((c) => c.id === selectedWallet); + if (!connector) { + console.error("Connector not found:", selectedWallet); + return; + } + + try { + await connectAsync({ connector }); // ■ await the wallet prompt + //router.push("/dashboard"); // ■ now safe to navigate + onClose(); + } catch (err) { + console.error("Wallet connection failed:", err); // ■ handle rejections + } + }; + + const modalVariants = { + hidden: { opacity: 0, scale: 0.9 }, + visible: { + opacity: 1, + scale: 1, + transition: { + duration: 0.2, + ease: "easeOut", + }, + }, + + exit: { + opacity: 0, + scale: 0.9, + transition: { + duration: 0.2, + ease: "easeIn", + }, + }, + }; + + const backdropVariants = { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + exit: { opacity: 0 }, + }; + + // helper to get icon source + function getIconSource( + icon: string | { dark: string; light: string } + ): string { + if (typeof icon === "string") { + // If it's a string, use it directly + return icon; + } else { + // If it's an object, use the dark variant (or light, as needed) + return icon.dark; // Or icon.light, depending on your theme + } + } + + return ( + + {isOpen && ( +
+ + + +
+

+ Connect Wallet +

+ + +
+ +

+ Choose your preferred wallet +

+ +
+ {connectors.map((wallet, index) => ( + + + + ))} +
+ + {/* ③ Confirmation button */} + + + + +
+
+ )} +
+ ); +} diff --git a/src/app/components/wallet-disconnect-modal.tsx b/src/app/components/wallet-disconnect-modal.tsx new file mode 100644 index 0000000..0b09676 --- /dev/null +++ b/src/app/components/wallet-disconnect-modal.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { motion, AnimatePresence } from "framer-motion"; +import { useRouter } from "next/navigation"; +import { usePathname } from "next/navigation"; +import { X } from "lucide-react"; +import AnimationWrapper from "../motion/animation-wrapper"; + +interface WalletDisconnectModalProps { + isOpen: boolean; + onClose: () => void; + onDisconnect: () => void; +} + +export default function WalletDisconnectModal({ + isOpen, + onClose, + onDisconnect, +}: WalletDisconnectModalProps) { + //pathname check + const pathName = usePathname(); + const userDashboardPath = "/dashboard/user"; + const institutionDashboardPath = "/dashboard/institution"; + //router + const router = useRouter(); + const handleDisconnect = () => { + if ( + userDashboardPath === pathName || + institutionDashboardPath === pathName + ) { + router.push("/"); // ■ now safe to navigate + } + onDisconnect(); + }; + + const modalVariants = { + hidden: { opacity: 0, scale: 0.9 }, + visible: { + opacity: 1, + scale: 1, + transition: { + duration: 0.2, + ease: "easeOut", + }, + }, + + exit: { + opacity: 0, + scale: 0.9, + transition: { + duration: 0.2, + ease: "easeIn", + }, + }, + }; + + const backdropVariants = { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + exit: { opacity: 0 }, + }; + + return ( + + {isOpen && ( +
+ + + +
+

+ Disconnect Wallet +

+ +
+ + +

+ Are you sure you want to disconnect your wallet? +

+
+ +
+ + + +
+
+
+ )} +
+ ); +} diff --git a/src/app/components/walletProvider.tsx b/src/app/components/walletProvider.tsx new file mode 100644 index 0000000..07db231 --- /dev/null +++ b/src/app/components/walletProvider.tsx @@ -0,0 +1,71 @@ +"use client"; + +import React, { + createContext, + useContext, + ReactNode, + useCallback, +} from "react"; + +import { + useConnect, + useAccount, + useDisconnect, + Connector, + ConnectVariables, +} from "@starknet-react/core"; + +interface WalletContextProps { + account: string | null; + connectors: Connector[]; // ← Exposed connectors + connectWallet: (connector: Connector) => void; // ← Takes connector arg + disconnectWallet: () => void; + connectAsync: (args?: ConnectVariables) => Promise; +} + +const WalletContext = createContext({ + account: null, + connectors: [], // ← Default empty + connectWallet: () => {}, + disconnectWallet: () => {}, + connectAsync: () => Promise.resolve(), +}); + +export const WalletProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const { connect, connectors, connectAsync } = useConnect(); + const { address } = useAccount(); + const { disconnect } = useDisconnect(); + + // Accept a specific connector when connecting + const connectWallet = useCallback( + (connector: Connector) => { + connect({ connector }); + }, + [connect] + ); + + + return ( + + {children} + + ); +}; + +export const useWalletContext = () => { + const ctx = useContext(WalletContext); + if (!ctx) { + throw new Error("useWalletContext must be inside WalletProvider"); + } + return ctx; +}; diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..f0e5e41 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -8,8 +8,9 @@ @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); + /* --font-sans: var(--font-geist-sans); */ --font-mono: var(--font-geist-mono); + --font-orbitron: var(--font-orbitron-sans) } @media (prefers-color-scheme: dark) { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..d111b8d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,14 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; +import { Geist_Mono, Orbitron } from "next/font/google"; import "./globals.css"; +import { StarknetProvider } from "./components/provider"; +import { WalletProvider } from "./components/walletProvider"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const orbitron = Orbitron({ + variable: "--font-orbitron-sans", + weight: ["400", "500", "700"], subsets: ["latin"], + display: "swap", }); const geistMono = Geist_Mono({ @@ -25,9 +29,13 @@ export default function RootLayout({ return ( - {children} + + < WalletProvider> + {children} + + ); diff --git a/src/app/motion/animation-wrapper.tsx b/src/app/motion/animation-wrapper.tsx new file mode 100644 index 0000000..52c6adb --- /dev/null +++ b/src/app/motion/animation-wrapper.tsx @@ -0,0 +1,92 @@ +"use client"; + +import type { ReactNode } from "react"; + +import { motion, type MotionProps } from "framer-motion"; + +type AnimationVariant = + | "fadeIn" + | "slideUp" + | "slideDown" + | "slideLeft" + | "slideRight" + | "scale" + | "bounce"; + +interface AnimationWrapperProps extends Omit { + children: ReactNode; + variant?: AnimationVariant; + delay?: number; + duration?: number; + className?: string; + once?: boolean; +} + +const variants = { + fadeIn: { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + }, + + slideUp: { + hidden: { y: 50, opacity: 0 }, + visible: { y: 0, opacity: 1 }, + }, + + slideDown: { + hidden: { y: -50, opacity: 0 }, + visible: { y: 0, opacity: 1 }, + }, + + slideLeft: { + hidden: { x: 50, opacity: 0 }, + visible: { x: 0, opacity: 1 }, + }, + + slideRight: { + hidden: { x: -50, opacity: 0 }, + visible: { x: 0, opacity: 1 }, + }, + + scale: { + hidden: { scale: 0.8, opacity: 0 }, + visible: { scale: 1, opacity: 1 }, + }, + + bounce: { + hidden: { y: 50, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + transition: { + type: "spring", + stiffness: 300, + damping: 15, + }, + }, + }, +}; + +export default function AnimationWrapper({ + children, + variant = "fadeIn", + delay = 0, + duration = 0.5, + className = "", + once = true, + ...props +}: AnimationWrapperProps) { + return ( + + {children} + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index e68abe6..821a588 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,9 @@ -import Image from "next/image"; +import Navbar from "./components/navbar"; export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- - -
- +
+
); }