diff --git a/bun.lockb b/bun.lockb old mode 100644 new mode 100755 index d3914e8..52c74ee Binary files a/bun.lockb and b/bun.lockb differ diff --git a/scripts/qa-onchain-check.js b/scripts/qa-onchain-check.js new file mode 100644 index 0000000..5f92994 --- /dev/null +++ b/scripts/qa-onchain-check.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node +import { JsonRpcProvider, Contract } from 'ethers'; + +async function main() { + const rpc = process.env.VITE_BASE_RPC || process.env.BASE_RPC || 'https://mainnet.base.org'; + const postsAddr = process.env.VITE_BASELINE_POSTS || process.env.BASELINE_POSTS; + + console.log('Using RPC:', rpc); + console.log('Posts contract address:', postsAddr); + + if (!postsAddr) { + console.error('No Posts contract address provided in env VITE_BASELINE_POSTS'); + process.exit(2); + } + + const provider = new JsonRpcProvider(rpc); + + const POSTS_ABI = [ + 'function getAllPosts() view returns (uint256[])', + 'function getPost(uint256) view returns (uint256 id, address author, string content, uint256 timestamp, uint256 likesCount, uint256 commentsCount, bool exists)' + ]; + + try { + const contract = new Contract(postsAddr, POSTS_ABI, provider); + console.log('Calling getAllPosts...'); + const ids = await contract.getAllPosts(); + console.log('Received ids length:', ids.length); + if (ids.length > 0) { + const first = ids[0]; + console.log('Fetching first post id:', first.toString()); + const post = await contract.getPost(first); + console.log('Post:', { + id: post.id.toString(), + author: post.author, + content: (post.content || '').slice(0, 120), + timestamp: post.timestamp.toString(), + likes: (post.likesCount?.toString?.() || post[4]?.toString?.()) + }); + } else { + console.log('No posts found on-chain (getAllPosts returned empty array).'); + } + } catch (err) { + console.error('On-chain checks failed:', (err && err.message) || err); + process.exit(1); + } +} + +main(); diff --git a/src/components/ConnectWallet.tsx b/src/components/ConnectWallet.tsx index 14cc1df..9f9b98b 100644 --- a/src/components/ConnectWallet.tsx +++ b/src/components/ConnectWallet.tsx @@ -1,8 +1,10 @@ -import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + import { useConnect, useAccount } from "wagmi"; import { Wallet, Shield, Zap, ChevronRight } from "lucide-react"; -import { useEffect } from "react"; +import { base } from 'wagmi/chains'; +import React, { useEffect } from "react"; import heroImage from "@/assets/baseline-hero.jpg"; interface ConnectWalletProps { @@ -12,6 +14,7 @@ interface ConnectWalletProps { const ConnectWallet = ({ onConnect }: ConnectWalletProps) => { const { connectors, connect } = useConnect(); const { isConnected } = useAccount(); + const baseRpc = (import.meta.env.VITE_BASE_RPC as string | undefined) ?? 'https://mainnet.base.org'; useEffect(() => { if (isConnected) { @@ -19,6 +22,47 @@ const ConnectWallet = ({ onConnect }: ConnectWalletProps) => { } }, [isConnected, onConnect]); + useEffect(() => { + if (!isConnected) return; + + const ensureBase = async () => { + try { + const ethereum = (window as any).ethereum; + if (!ethereum?.request) { + try { (await import('sonner')).toast.error('No Ethereum provider available to switch networks'); } catch { /* ignore */ } + return; + } + + const currentChainHex = await ethereum.request({ method: 'eth_chainId' }) as string; + const currentChainId = parseInt(currentChainHex, 16); + if (currentChainId === base.id) return; + + await ethereum.request({ + method: 'wallet_addEthereumChain', + params: [{ + chainId: '0x' + base.id.toString(16), + chainName: 'Base Mainnet', + rpcUrls: [baseRpc], + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + blockExplorerUrls: ['https://base.blockscout.com/'], + }], + }); + + await ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: '0x' + base.id.toString(16) }], + }); + + try { (await import('sonner')).toast.success('Switched to Base Mainnet'); } catch { /* ignore */ } + } catch (err) { + console.error('Network switch failed', err); + try { (await import('sonner')).toast.error('Please switch your wallet to Base Mainnet'); } catch { /* ignore */ } + } + }; + + ensureBase(); + }, [isConnected, baseRpc]); + const wallets = []; return ( @@ -34,7 +78,7 @@ const ConnectWallet = ({ onConnect }: ConnectWalletProps) => { {/* Header */}
- โšก + B

Welcome to BaseLine @@ -61,26 +105,40 @@ const ConnectWallet = ({ onConnect }: ConnectWalletProps) => {

Connect Your Wallet

+
- {connectors.map((connector) => ( - - ))} +
+

@@ -90,17 +148,10 @@ const ConnectWallet = ({ onConnect }: ConnectWalletProps) => {
- {/* Network Info */} -
-
-
- Base Mainnet (Chain ID: 8453) -
-
); }; -export default ConnectWallet; \ No newline at end of file +export default ConnectWallet; diff --git a/src/components/CreatePost.tsx b/src/components/CreatePost.tsx index 5091136..e812067 100644 --- a/src/components/CreatePost.tsx +++ b/src/components/CreatePost.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import React, { useState } from "react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; @@ -33,13 +33,23 @@ const CreatePost = ({ onPost }: CreatePostProps) => { const handlePost = async () => { if (!content.trim() || isPosting) return; - + setIsPosting(true); - await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate blockchain tx - onPost(content); - setContent(""); - setCharCount(0); - setIsPosting(false); + try { + const res = onPost ? onPost(content) : null; + if (res && typeof (res as { then?: unknown }).then === 'function') { + await res as Promise; + } else { + // fallback simulated tx + await new Promise(resolve => setTimeout(resolve, 1000)); + } + setContent(""); + setCharCount(0); + } catch (err) { + console.error('Post failed', err); + } finally { + setIsPosting(false); + } }; const getCharCountColor = () => { @@ -133,4 +143,4 @@ const CreatePost = ({ onPost }: CreatePostProps) => { ); }; -export default CreatePost; \ No newline at end of file +export default CreatePost; diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 50c42ae..d255f30 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import React, { useState } from "react"; import Sidebar from "./Sidebar"; import Feed from "./Feed"; import RightPanel from "./RightPanel"; @@ -43,4 +43,4 @@ const Dashboard = ({ onDisconnect }: DashboardProps) => { ); }; -export default Dashboard; \ No newline at end of file +export default Dashboard; diff --git a/src/components/Feed.tsx b/src/components/Feed.tsx index d2e1b6a..e477432 100644 --- a/src/components/Feed.tsx +++ b/src/components/Feed.tsx @@ -1,21 +1,22 @@ -import { useState, useEffect } from "react"; +import React, { useEffect, useState } from "react"; import PostCard from "./PostCard"; import CreatePost from "./CreatePost"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { RefreshCw, TrendingUp, Hash, Users } from "lucide-react"; +import { usePosts } from "@/hooks/usePosts"; interface Post { id: string; author: string; - username: string; + username?: string; content: string; - timestamp: Date; + timestamp: number | Date; likes: number; comments: number; reposts: number; - isLiked: boolean; + isLiked?: boolean; avatarUrl?: string; txHash?: string; } @@ -33,42 +34,6 @@ const mockPosts: Post[] = [ isLiked: false, txHash: "0xabc123..." }, - { - id: "2", - author: "0x9876...5432", - username: "nft_collector", - content: "Love how I can use my @BoredApeYC as my profile picture here! Finally, true NFT utility in social media. This is what we've been waiting for.", - timestamp: new Date(Date.now() - 15 * 60 * 1000), - likes: 128, - comments: 23, - reposts: 45, - isLiked: true, - txHash: "0xdef456..." - }, - { - id: "3", - author: "0x5555...7777", - username: "defi_degen", - content: "GM Web3! ๐ŸŒ… The future of social media is decentralized. No more censorship, no more data harvesting. Just pure, on-chain expression. #DecentralizedSocial", - timestamp: new Date(Date.now() - 45 * 60 * 1000), - likes: 89, - comments: 15, - reposts: 28, - isLiked: false, - txHash: "0x789abc..." - }, - { - id: "4", - author: "0x8888...9999", - username: "base_maxi", - content: "Building on Base feels like magic โœจ Fast, cheap, and secure. BaseLine is going to change how we think about social networks forever!", - timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000), - likes: 156, - comments: 34, - reposts: 67, - isLiked: true, - txHash: "0x456def..." - } ]; interface FeedProps { @@ -76,30 +41,53 @@ interface FeedProps { } const Feed = ({ activeTab }: FeedProps) => { + const { posts: onChainPosts, isLoading, refetch, createPost, likePost, unlikePost } = usePosts(); const [posts, setPosts] = useState(mockPosts); const [isRefreshing, setIsRefreshing] = useState(false); - const handleNewPost = (content: string) => { - const newPost: Post = { - id: Date.now().toString(), - author: "0x1234...5678", - username: "baseline_user", - content, - timestamp: new Date(), - likes: 0, - comments: 0, - reposts: 0, - isLiked: false, - txHash: `0x${Math.random().toString(16).substr(2, 8)}...` - }; - setPosts([newPost, ...posts]); + useEffect(() => { + if (onChainPosts && onChainPosts.length > 0) { + setPosts(onChainPosts.map((p) => ({ ...p, timestamp: new Date(p.timestamp as number) })) as Post[]); + } + }, [onChainPosts]); + + const handleNewPost = async (content: string) => { + if (createPost) { + await createPost(content); + await refetch(); + } else { + const newPost: Post = { + id: Date.now().toString(), + author: "0x1234...5678", + username: "baseline_user", + content, + timestamp: new Date(), + likes: 0, + comments: 0, + reposts: 0, + isLiked: false, + txHash: `0x${Math.random().toString(16).substr(2, 8)}...` + }; + setPosts([newPost, ...posts]); + } }; - const handleLike = (postId: string) => { - setPosts(posts.map(post => - post.id === postId - ? { - ...post, + const handleLike = async (postId: string) => { + // try to like/unlike via contract + const found = posts.find(p => p.id === postId); + if (!found) return; + + if (found.isLiked) { + await unlikePost(postId).catch(() => {}); + } else { + await likePost(postId).catch(() => {}); + } + + // optimistic UI update + setPosts(posts.map(post => + post.id === postId + ? { + ...post, isLiked: !post.isLiked, likes: post.isLiked ? post.likes - 1 : post.likes + 1 } @@ -109,12 +97,11 @@ const Feed = ({ activeTab }: FeedProps) => { const handleComment = (postId: string) => { console.log("Comment on post:", postId); - // TODO: Implement comment functionality }; const handleRepost = (postId: string) => { - setPosts(posts.map(post => - post.id === postId + setPosts(posts.map(post => + post.id === postId ? { ...post, reposts: post.reposts + 1 } : post )); @@ -122,7 +109,7 @@ const Feed = ({ activeTab }: FeedProps) => { const handleRefresh = async () => { setIsRefreshing(true); - await new Promise(resolve => setTimeout(resolve, 1000)); + await refetch(); setIsRefreshing(false); }; @@ -131,7 +118,11 @@ const Feed = ({ activeTab }: FeedProps) => { case 'trending': return [...posts].sort((a, b) => b.likes - a.likes); case 'explore': - return [...posts].sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + return [...posts].sort((a, b) => { + const ta = typeof a.timestamp === 'number' ? a.timestamp : a.timestamp.getTime(); + const tb = typeof b.timestamp === 'number' ? b.timestamp : b.timestamp.getTime(); + return tb - ta; + }); default: return posts; } @@ -237,4 +228,4 @@ const Feed = ({ activeTab }: FeedProps) => { ); }; -export default Feed; \ No newline at end of file +export default Feed; diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index 48acffc..3ef62a3 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import React, { useState } from "react"; import { Card } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; @@ -166,4 +166,4 @@ const PostCard = ({ post, onLike, onComment, onRepost }: PostCardProps) => { ); }; -export default PostCard; \ No newline at end of file +export default PostCard; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index 68d5378..ad32c15 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -21,7 +21,7 @@ const Command = React.forwardRef< )); Command.displayName = CommandPrimitive.displayName; -interface CommandDialogProps extends DialogProps {} +type CommandDialogProps = DialogProps; const CommandDialog = ({ children, ...props }: CommandDialogProps) => { return ( diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 4a5643e..18eba7f 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -export interface TextareaProps extends React.TextareaHTMLAttributes {} +export type TextareaProps = React.TextareaHTMLAttributes; const Textarea = React.forwardRef(({ className, ...props }, ref) => { return ( diff --git a/src/hooks/useComments.ts b/src/hooks/useComments.ts new file mode 100644 index 0000000..933f990 --- /dev/null +++ b/src/hooks/useComments.ts @@ -0,0 +1,83 @@ +import { useCallback } from 'react'; +import { usePublicClient, useWriteContract } from 'wagmi'; +import { CONTRACT_ADDRESSES, COMMENTS_ABI } from '@/lib/contracts'; +import { toast } from 'sonner'; + +export function useComments() { + const publicClient = usePublicClient(); + const { writeContract: writeAddComment } = useWriteContract(); + + const addComment = useCallback(async (postId: string, content: string) => { + const commentsAddr = CONTRACT_ADDRESSES.Comments; + if (!commentsAddr) { + toast.error('Comments contract not configured'); + return; + } + + try { + await writeAddComment({ + address: commentsAddr as `0x${string}`, + abi: COMMENTS_ABI as unknown as import('abitype').Abi, + functionName: 'addComment', + args: [BigInt(postId), content], + }); + toast.success('Comment tx submitted'); + } catch (err: unknown) { + console.error('addComment failed', err); + toast.error('Failed to create comment'); + } + }, [writeAddComment]); + + const getCommentsForPost = useCallback(async (postId: string) => { + const commentsAddr = CONTRACT_ADDRESSES.Comments; + if (!commentsAddr) return []; + try { + const ids: unknown = await publicClient.readContract({ + address: commentsAddr as `0x${string}`, + abi: COMMENTS_ABI as unknown as import('abitype').Abi, + functionName: 'getCommentsForPost', + args: [BigInt(postId)], + }); + + if (!Array.isArray(ids)) return []; + + const items = await Promise.all( + (ids as unknown[]).map(async (id: unknown) => { + const raw: unknown = await publicClient.readContract({ + address: commentsAddr as `0x${string}`, + abi: COMMENTS_ABI as unknown as import('abitype').Abi, + functionName: 'comments', + args: [id as unknown], + }); + + const arr = raw as unknown as [ + bigint | number | string, + bigint | number | string, + string, + string, + bigint | number + ]; + + return { + id: arr[0]?.toString(), + postId: arr[1]?.toString(), + author: arr[2], + content: arr[3], + timestamp: Number(arr[4] ?? Date.now()), + likes: 0, + }; + }) + ); + + return items; + } catch (err: unknown) { + console.error('getCommentsForPost failed', err); + return []; + } + }, [publicClient]); + + return { + addComment, + getCommentsForPost, + }; +} diff --git a/src/hooks/useFollow.ts b/src/hooks/useFollow.ts new file mode 100644 index 0000000..fcbd757 --- /dev/null +++ b/src/hooks/useFollow.ts @@ -0,0 +1,115 @@ +import { useCallback } from 'react'; +import { useCallback } from 'react'; +import { usePublicClient, useWriteContract } from 'wagmi'; +import { CONTRACT_ADDRESSES, FOLLOW_ABI } from '@/lib/contracts'; +import { toast } from 'sonner'; + +export function useFollow() { + const publicClient = usePublicClient(); + const { writeContract: writeFollow } = useWriteContract(); + const { writeContract: writeUnfollow } = useWriteContract(); + + const followUser = useCallback(async (target: string) => { + const followAddr = CONTRACT_ADDRESSES.Follow; + if (!followAddr) { + toast.error('Follow contract not configured'); + return; + } + + try { + await writeFollow({ + address: followAddr as `0x${string}`, + abi: FOLLOW_ABI as unknown as import('abitype').Abi, + functionName: 'followUser', + args: [target], + }); + toast.success('Follow tx submitted'); + } catch (err: unknown) { + console.error('followUser failed', err); + toast.error('Failed to follow'); + } + }, [writeFollow]); + + const unfollowUser = useCallback(async (target: string) => { + const followAddr = CONTRACT_ADDRESSES.Follow; + if (!followAddr) { + toast.error('Follow contract not configured'); + return; + } + + try { + await writeUnfollow({ + address: followAddr as `0x${string}`, + abi: FOLLOW_ABI as unknown as import('abitype').Abi, + functionName: 'unfollowUser', + args: [target], + }); + toast.success('Unfollow tx submitted'); + } catch (err: unknown) { + console.error('unfollowUser failed', err); + toast.error('Failed to unfollow'); + } + }, [writeUnfollow]); + + const getFollowerCount = useCallback(async (user: string) => { + const followAddr = CONTRACT_ADDRESSES.Follow; + if (!followAddr) return 0n; + + try { + const res: unknown = await publicClient.readContract({ + address: followAddr as `0x${string}`, + abi: FOLLOW_ABI as unknown as import('abitype').Abi, + functionName: 'followerCount', + args: [user], + }); + return BigInt(String(res ?? '0')) ; + } catch (err: unknown) { + console.error('getFollowerCount failed', err); + return 0n; + } + }, [publicClient]); + + const getFollowingCount = useCallback(async (user: string) => { + const followAddr = CONTRACT_ADDRESSES.Follow; + if (!followAddr) return 0n; + + try { + const res: unknown = await publicClient.readContract({ + address: followAddr as `0x${string}`, + abi: FOLLOW_ABI as unknown as import('abitype').Abi, + functionName: 'followingCount', + args: [user], + }); + return BigInt(String(res ?? '0')) ; + } catch (err: unknown) { + console.error('getFollowingCount failed', err); + return 0n; + } + }, [publicClient]); + + const isFollowing = useCallback(async (follower: string, user: string) => { + const followAddr = CONTRACT_ADDRESSES.Follow; + if (!followAddr) return false; + + try { + const res: unknown = await publicClient.readContract({ + address: followAddr as `0x${string}`, + abi: FOLLOW_ABI as unknown as import('abitype').Abi, + functionName: 'isFollowing', + args: [follower, user], + }); + return Boolean(res as boolean); + } catch (err: unknown) { + console.error('isFollowing failed', err); + return false; + } + }, [publicClient]); + + return { + followUser, + unfollowUser, + getFollowerCount, + getFollowingCount, + isFollowing, + }; +} diff --git a/src/hooks/usePosts.ts b/src/hooks/usePosts.ts new file mode 100644 index 0000000..6f32841 --- /dev/null +++ b/src/hooks/usePosts.ts @@ -0,0 +1,345 @@ +import { useEffect, useState, useCallback } from 'react'; +import { usePublicClient, useAccount, useWriteContract, useWaitForTransactionReceipt, useConnect } from 'wagmi'; +import type { Abi } from 'abitype'; +import { CONTRACT_ADDRESSES, POSTS_ABI } from '@/lib/contracts'; +import { toast } from 'sonner'; + +export interface Post { + id: string; + author: string; + username?: string; + content: string; + timestamp: number; + likes: number; + comments: number; + reposts: number; + isLiked?: boolean; + avatarUrl?: string; + txHash?: string; +} + +const MOCK_POSTS: Post[] = [ + { + id: 'mock-1', + author: '0xmock...1', + content: 'Welcome to BaseLine! This is a demo post.', + timestamp: Date.now() - 1000 * 60 * 5, + likes: 5, + comments: 1, + reposts: 0, + }, + { + id: 'mock-2', + author: '0xmock...2', + content: 'This platform is running in demo mode โ€” on-chain contract calls failed.', + timestamp: Date.now() - 1000 * 60 * 60, + likes: 12, + comments: 3, + reposts: 1, + }, +]; + +export function usePosts() { + const publicClient = usePublicClient(); + const { address: connected } = useAccount(); + const [posts, setPosts] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [useMock, setUseMock] = useState(false); + + const { writeContract: writeCreatePost, data: createHash } = useWriteContract(); + const { writeContract: writeLikePost, data: likeHash } = useWriteContract(); + const { writeContract: writeUnlikePost, data: unlikeHash } = useWriteContract(); + + const { connectors, connect } = useConnect(); + + const { isLoading: isCreating } = useWaitForTransactionReceipt({ hash: createHash }); + const { isLoading: isLiking } = useWaitForTransactionReceipt({ hash: likeHash }); + const { isLoading: isUnliking } = useWaitForTransactionReceipt({ hash: unlikeHash }); + + const fetchPosts = useCallback(async () => { + setIsLoading(true); + setError(null); + + const postsAddr = CONTRACT_ADDRESSES.Posts; + if (!postsAddr) { + // Posts contract not configured โ€” switch to demo/mock mode so users can still create posts locally + setUseMock(true); + setPosts(MOCK_POSTS); + setError('Posts contract not configured โ€” running in demo mode'); + setIsLoading(false); + return; + } + + try { + // Try to use getAllPosts if available + let items: Post[] = []; + try { + const ids: unknown = await publicClient.readContract({ + address: postsAddr as `0x${string}`, + abi: POSTS_ABI as unknown as Abi, + functionName: 'getAllPosts', + }); + + const idsArr: unknown[] = Array.isArray(ids) ? (ids as unknown[]) : []; + + items = await Promise.all( + idsArr.map(async (id: unknown) => { + const raw: unknown = await publicClient.readContract({ + address: postsAddr as `0x${string}`, + abi: POSTS_ABI as unknown as Abi, + functionName: 'getPost', + args: [id as unknown], + }); + + const arr = raw as unknown as [ + bigint | number | string, + string, + string, + bigint | number + ]; + + const parsed: Post = { + id: arr[0]?.toString?.() ?? String(id), + author: arr[1] ?? '0x0', + content: arr[2] ?? '', + timestamp: Number(arr[3] ?? Date.now()), + likes: 0, + comments: 0, + reposts: 0, + }; + + // try to read likeCounts if available + try { + const likeRes: unknown = await publicClient.readContract({ + address: postsAddr as `0x${string}`, + abi: POSTS_ABI as unknown as Abi, + functionName: 'likeCounts', + args: [BigInt(parsed.id)], + }); + parsed.likes = Number(likeRes ?? 0); + } catch { + // ignore + } + + return parsed; + }) + ); + } catch (errGetAll) { + // getAllPosts not available โ€” fallback to sequential posts(index) + const collected: Post[] = []; + const maxScan = 200; // limit + for (let i = 0; i < maxScan; i++) { + try { + const raw: unknown = await publicClient.readContract({ + address: postsAddr as `0x${string}`, + abi: POSTS_ABI as unknown as Abi, + functionName: 'posts', + args: [BigInt(i)], + }); + + const arr = raw as unknown as [ + bigint | number | string, + string, + string, + bigint | number + ]; + + // if id is falsy and i>0 assume end + const idVal = Number(arr[0] ?? 0); + if (!idVal) break; + + const parsed: Post = { + id: arr[0]?.toString?.() ?? String(i), + author: arr[1] ?? '0x0', + content: arr[2] ?? '', + timestamp: Number(arr[3] ?? Date.now()), + likes: 0, + comments: 0, + reposts: 0, + }; + + try { + const likeRes: unknown = await publicClient.readContract({ + address: postsAddr as `0x${string}`, + abi: POSTS_ABI as unknown as Abi, + functionName: 'likeCounts', + args: [BigInt(i)], + }); + parsed.likes = Number(likeRes ?? 0); + } catch { + // ignore + } + + collected.push(parsed); + } catch (innerErr) { + // stop scanning on errors + break; + } + } + items = collected; + } + + // sort by timestamp desc + items.sort((a, b) => b.timestamp - a.timestamp); + setPosts(items); + } catch (err: unknown) { + console.error('Failed to fetch posts', err); + setError((err as { message?: string })?.message ?? 'Failed to fetch posts'); + // fallback to local mock data so UI remains functional + setUseMock(true); + setPosts(MOCK_POSTS); + } finally { + setIsLoading(false); + } + }, [publicClient]); + + useEffect(() => { + fetchPosts(); + }, [fetchPosts]); + + const createPost = useCallback(async (content: string) => { + // If user not connected, trigger wallet popup + if (!connected) { + try { + const preferred = ["MetaMask","Coinbase Wallet"]; + const findPreferred = connectors.find((c) => preferred.some((p) => c.name.toLowerCase().includes(p.toLowerCase()))); + const fallback = connectors.find((c) => c.ready) || connectors[0]; + const target = findPreferred || fallback; + if (target && connect) { + await connect({ connector: target }); + try { toast('Please approve the connection in your wallet'); } catch { /* ignore */ } + return; + } + } catch (err) { + console.error('connect trigger failed', err); + } + + try { toast('Please connect your wallet to post'); } catch { /* ignore */ } + return; + } + + const postsAddr = CONTRACT_ADDRESSES.Posts; + if (!postsAddr) { + toast.error('Posts contract not configured'); + return; + } + + try { + if (useMock) { + // simulate tx in mock mode + const newPost: Post = { + id: Date.now().toString(), + author: connected ?? '0xdemo', + content, + timestamp: Date.now(), + likes: 0, + comments: 0, + reposts: 0, + }; + setPosts((p) => [newPost, ...p]); + toast.success('Post created (demo)'); + return; + } + + await writeCreatePost({ + address: postsAddr as `0x${string}`, + abi: POSTS_ABI as unknown as Abi, + functionName: 'createPost', + args: [content], + }); + toast.success('Transaction submitted'); + // wait a bit and refetch when confirmed by hook + const wait = new Promise((res) => setTimeout(res, 1200)); + await wait; + await fetchPosts(); + } catch (err: unknown) { + console.error('createPost failed', err); + toast.error('Failed to create post, switched to demo mode'); + setUseMock(true); + // fallback to local mock post + const newPost: Post = { + id: Date.now().toString(), + author: connected ?? '0xdemo', + content, + timestamp: Date.now(), + likes: 0, + comments: 0, + reposts: 0, + }; + setPosts((p) => [newPost, ...p]); + return; + } + }, [writeCreatePost, fetchPosts, connected, useMock, connectors, connect]); + + const likePost = useCallback(async (postId: string) => { + const postsAddr = CONTRACT_ADDRESSES.Posts; + if (!postsAddr) { + toast.error('Posts contract not configured'); + return; + } + + try { + if (useMock) { + setPosts((p) => p.map((post) => post.id === postId ? { ...post, isLiked: true, likes: post.likes + 1 } : post)); + toast.success('Liked (demo)'); + return; + } + + await writeLikePost({ + address: postsAddr as `0x${string}`, + abi: POSTS_ABI as unknown as Abi, + functionName: 'likePost', + args: [BigInt(postId)], + }); + toast.success('Like tx submitted'); + await new Promise((res) => setTimeout(res, 800)); + await fetchPosts(); + } catch (err: unknown) { + console.error('like failed', err); + toast.error('Failed to like'); + } + }, [writeLikePost, fetchPosts, useMock]); + + const unlikePost = useCallback(async (postId: string) => { + const postsAddr = CONTRACT_ADDRESSES.Posts; + if (!postsAddr) { + toast.error('Posts contract not configured'); + return; + } + + try { + if (useMock) { + setPosts((p) => p.map((post) => post.id === postId ? { ...post, isLiked: false, likes: Math.max(0, post.likes - 1) } : post)); + toast.success('Unliked (demo)'); + return; + } + + await writeUnlikePost({ + address: postsAddr as `0x${string}`, + abi: POSTS_ABI as unknown as Abi, + functionName: 'unlikePost', + args: [BigInt(postId)], + }); + toast.success('Unlike tx submitted'); + await new Promise((res) => setTimeout(res, 800)); + await fetchPosts(); + } catch (err: unknown) { + console.error('unlike failed', err); + toast.error('Failed to unlike'); + } + }, [writeUnlikePost, fetchPosts, useMock]); + + return { + posts, + isLoading, + error, + refetch: fetchPosts, + createPost, + likePost, + unlikePost, + isCreating, + isLiking, + isUnliking, + }; +} diff --git a/src/hooks/useProfile.ts b/src/hooks/useProfile.ts index f224a1e..45dc4e0 100644 --- a/src/hooks/useProfile.ts +++ b/src/hooks/useProfile.ts @@ -1,4 +1,5 @@ import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'; +import React, { useState, useEffect, useCallback } from 'react'; import { CONTRACT_ADDRESSES, PROFILES_ABI } from '@/lib/contracts'; import { toast } from 'sonner'; @@ -6,113 +7,95 @@ export function useProfile(address?: `0x${string}`) { const { address: connectedAddress } = useAccount(); const targetAddress = address || connectedAddress; - // For now, use mock data since contracts aren't deployed yet - const mockProfile = { - username: 'demo_user', - bio: 'Welcome to BaseLine!', - nftContract: '0x0000000000000000000000000000000000000000', - nftTokenId: 0, - createdAt: Date.now(), - exists: true - }; - - const hasProfile = !!targetAddress; - const profile = hasProfile ? mockProfile : null; - - const { writeContract: createProfile, data: createHash } = useWriteContract(); - const { writeContract: updateProfile, data: updateHash } = useWriteContract(); - const { writeContract: setAvatar, data: avatarHash } = useWriteContract(); + const [profile, setProfile] = useState<{ + user: string; + username: string; + bio: string; + avatarContract: string; + avatarTokenId: number; + exists: boolean; + } | null>(null); - const { isLoading: isCreating } = useWaitForTransactionReceipt({ - hash: createHash, - }); + const { writeContract: writeUpdateProfile } = useWriteContract(); + const { writeContract: writeUpdateProfileWithAvatar } = useWriteContract(); - const { isLoading: isUpdating } = useWaitForTransactionReceipt({ - hash: updateHash, - }); + const { isLoading: isUpdating } = useWaitForTransactionReceipt({ hash: undefined }); - const { isLoading: isSettingAvatar } = useWaitForTransactionReceipt({ - hash: avatarHash, - }); + const fetchProfile = useCallback(async () => { + const profilesAddr = CONTRACT_ADDRESSES.Profiles; + if (!profilesAddr || !targetAddress) return; - const handleCreateProfile = (username: string, bio: string) => { - if (!targetAddress) return; - - // Mock implementation for now - toast.success(`Profile created for ${username}`); - - // TODO: Uncomment when contracts are deployed - /* try { - createProfile({ - address: CONTRACT_ADDRESSES.Profiles as `0x${string}`, - abi: PROFILES_ABI, - functionName: 'createProfile', - args: [username, bio], + const res: unknown = await (await import('wagmi')).publicClient.readContract({ + address: profilesAddr as `0x${string}`, + abi: PROFILES_ABI as unknown as import('abitype').Abi, + functionName: 'getProfile', + args: [targetAddress as `0x${string}`], }); - } catch (error) { - toast.error('Failed to create profile'); - console.error(error); - } - */ - }; - const handleUpdateProfile = (bio: string) => { - if (!targetAddress) return; - - // Mock implementation for now - toast.success('Profile updated'); - - // TODO: Uncomment when contracts are deployed - /* - try { - updateProfile({ - address: CONTRACT_ADDRESSES.Profiles as `0x${string}`, - abi: PROFILES_ABI, - functionName: 'updateProfile', - args: [bio], - }); - } catch (error) { - toast.error('Failed to update profile'); - console.error(error); + // res may be a tuple + const tup = res as unknown as [string, string, string, string, bigint | number, boolean]; + const parsed = { + user: tup[0], + username: tup[1], + bio: tup[2], + avatarContract: tup[3], + avatarTokenId: Number(tup[4] ?? 0), + exists: Boolean(tup[5]), + }; + setProfile(parsed); + } catch (err: unknown) { + console.error('fetchProfile failed', err); } - */ - }; + }, [targetAddress]); + + useEffect(() => { + if (targetAddress) fetchProfile(); + }, [targetAddress, fetchProfile]); + + const handleUpdateProfile = useCallback(async (username: string, bio: string, avatarContract?: string, avatarTokenId?: number) => { + const profilesAddr = CONTRACT_ADDRESSES.Profiles; + if (!profilesAddr || !targetAddress) return; - const handleSetAvatar = (nftContract: string, tokenId: string) => { - if (!targetAddress) return; - - // Mock implementation for now - toast.success('Avatar updated'); - - // TODO: Uncomment when contracts are deployed - /* try { - setAvatar({ - address: CONTRACT_ADDRESSES.Profiles as `0x${string}`, - abi: PROFILES_ABI, - functionName: 'setAvatar', - args: [nftContract as `0x${string}`, BigInt(tokenId)], - }); - } catch (error) { - toast.error('Failed to set avatar'); - console.error(error); + if (avatarContract) { + await writeUpdateProfileWithAvatar({ + address: profilesAddr as `0x${string}`, + abi: PROFILES_ABI as unknown as import('abitype').Abi, + functionName: 'updateProfile', + args: [username, bio, avatarContract, BigInt(avatarTokenId ?? 0)], + }); + } else { + await writeUpdateProfile({ + address: profilesAddr as `0x${string}`, + abi: PROFILES_ABI as unknown as import('abitype').Abi, + functionName: 'updateProfile', + args: [username, bio, '0x0000000000000000000000000000000000000000', BigInt(0)], + }); + } + toast.success('Profile update submitted'); + // refetch + setTimeout(() => fetchProfile(), 1200); + } catch (err: unknown) { + console.error('updateProfile failed', err); + toast.error('Failed to update profile'); } - */ - }; + }, [writeUpdateProfile, writeUpdateProfileWithAvatar, fetchProfile, targetAddress]); return { - hasProfile, + hasProfile: Boolean(targetAddress), profile, - createProfile: handleCreateProfile, + createProfile: async (username: string, bio: string) => { + // For this contract we use updateProfile to create/update + await handleUpdateProfile(username, bio); + }, updateProfile: handleUpdateProfile, - setAvatar: handleSetAvatar, - isCreating, - isUpdating, - isSettingAvatar, - refetch: () => { - // Mock refetch - console.log('Refetching profile...'); + setAvatar: async (nftContract: string, tokenId: string) => { + await handleUpdateProfile(profile?.username ?? '', profile?.bio ?? '', nftContract, Number(tokenId)); }, + isCreating: false, + isUpdating, + isSettingAvatar: false, + refetch: fetchProfile, }; -} \ No newline at end of file +} diff --git a/src/lib/contracts.ts b/src/lib/contracts.ts index abeb74e..10f0584 100644 --- a/src/lib/contracts.ts +++ b/src/lib/contracts.ts @@ -1,55 +1,54 @@ -// Contract addresses (update after deployment) +// Contract addresses sourced from Vite environment variables export const CONTRACT_ADDRESSES = { - Profiles: '0x0000000000000000000000000000000000000000', // Update after deployment - Posts: '0x0000000000000000000000000000000000000000', // Update after deployment - Comments: '0x0000000000000000000000000000000000000000', // Update after deployment - Follow: '0x0000000000000000000000000000000000000000', // Update after deployment + Profiles: import.meta.env.VITE_BASELINE_PROFILES as string | undefined, + Posts: import.meta.env.VITE_BASELINE_POSTS as string | undefined, + Comments: import.meta.env.VITE_BASELINE_COMMENTS as string | undefined, + Follow: import.meta.env.VITE_BASELINE_FOLLOW as string | undefined, } as const; -// Contract ABIs (simplified for key functions) -export const PROFILES_ABI = [ - 'function createProfile(string username, string bio) external', - 'function updateProfile(string bio) external', - 'function setAvatar(address nftContract, uint256 nftTokenId) external', - 'function getProfile(address user) external view returns (tuple(string username, string bio, address nftContract, uint256 nftTokenId, uint256 createdAt, bool exists))', - 'function hasProfile(address user) external view returns (bool)', - 'function isUsernameAvailable(string username) external view returns (bool)', - 'event ProfileCreated(address indexed user, string username)', - 'event ProfileUpdated(address indexed user, string username, string bio)', - 'event AvatarUpdated(address indexed user, address nftContract, uint256 nftTokenId)' +// ABIs provided by the user (full ABIs) +export const POSTS_ABI = [ + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "uint256", "name": "id", "type": "uint256" }, + { "indexed": true, "internalType": "address", "name": "author", "type": "address" }, + { "indexed": false, "internalType": "string", "name": "content", "type": "string" }, + { "indexed": false, "internalType": "uint256", "name": "timestamp", "type": "uint256" } + ], + "name": "PostCreated", + "type": "event" + }, + { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "uint256", "name": "id", "type": "uint256" }, { "indexed": true, "internalType": "address", "name": "liker", "type": "address" } ], "name": "PostLiked", "type": "event" }, + { "inputs": [ { "internalType": "string", "name": "content", "type": "string" } ], "name": "createPost", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "nonpayable", "type": "function" }, + { "inputs": [ { "internalType": "uint256", "name": "id", "type": "uint256" } ], "name": "getPost", "outputs": [ { "components": [ { "internalType": "uint256", "name": "id", "type": "uint256" }, { "internalType": "address", "name": "author", "type": "address" }, { "internalType": "string", "name": "content", "type": "string" }, { "internalType": "uint256", "name": "timestamp", "type": "uint256" } ], "internalType": "struct Posts.Post", "name": "", "type": "tuple" }, { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "likeCounts", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "uint256", "name": "id", "type": "uint256" } ], "name": "likePost", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" }, { "internalType": "address", "name": "", "type": "address" } ], "name": "liked", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "posts", "outputs": [ { "internalType": "uint256", "name": "id", "type": "uint256" }, { "internalType": "address", "name": "author", "type": "address" }, { "internalType": "string", "name": "content", "type": "string" }, { "internalType": "uint256", "name": "timestamp", "type": "uint256" } ], "stateMutability": "view", "type": "function" } ] as const; -export const POSTS_ABI = [ - 'function createPost(string content) external', - 'function likePost(uint256 postId) external', - 'function unlikePost(uint256 postId) external', - 'function getPost(uint256 postId) external view returns (tuple(uint256 id, address author, string content, uint256 timestamp, uint256 likesCount, uint256 commentsCount, bool exists))', - 'function getUserPosts(address user) external view returns (uint256[])', - 'function getAllPosts() external view returns (uint256[])', - 'function hasUserLiked(uint256 postId, address user) external view returns (bool)', - 'event PostCreated(uint256 indexed postId, address indexed author, string content, uint256 timestamp)', - 'event PostLiked(uint256 indexed postId, address indexed liker, uint256 newLikesCount)', - 'event PostUnliked(uint256 indexed postId, address indexed unliker, uint256 newLikesCount)' +export const PROFILES_ABI = [ + { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "user", "type": "address" }, { "indexed": false, "internalType": "string", "name": "username", "type": "string" }, { "indexed": false, "internalType": "string", "name": "bio", "type": "string" }, { "indexed": false, "internalType": "address", "name": "avatarContract", "type": "address" }, { "indexed": false, "internalType": "uint256", "name": "avatarTokenId", "type": "uint256" } ], "name": "ProfileUpdated", "type": "event" }, + { "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], "name": "getProfile", "outputs": [ { "components": [ { "internalType": "address", "name": "user", "type": "address" }, { "internalType": "string", "name": "username", "type": "string" }, { "internalType": "string", "name": "bio", "type": "string" }, { "internalType": "address", "name": "avatarContract", "type": "address" }, { "internalType": "uint256", "name": "avatarTokenId", "type": "uint256" }, { "internalType": "bool", "name": "exists", "type": "bool" } ], "internalType": "struct Profiles.Profile", "name": "", "type": "tuple" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "string", "name": "username", "type": "string" }, { "internalType": "string", "name": "bio", "type": "string" }, { "internalType": "address", "name": "avatarContract", "type": "address" }, { "internalType": "uint256", "name": "avatarTokenId", "type": "uint256" } ], "name": "updateProfile", "outputs": [], "stateMutability": "nonpayable", "type": "function" } ] as const; export const COMMENTS_ABI = [ - 'function createComment(uint256 postId, string content) external', - 'function likeComment(uint256 commentId) external', - 'function unlikeComment(uint256 commentId) external', - 'function getComment(uint256 commentId) external view returns (tuple(uint256 id, uint256 postId, address author, string content, uint256 timestamp, uint256 likesCount, bool exists))', - 'function getPostComments(uint256 postId) external view returns (uint256[])', - 'function hasUserLikedComment(uint256 commentId, address user) external view returns (bool)', - 'event CommentCreated(uint256 indexed commentId, uint256 indexed postId, address indexed author, string content, uint256 timestamp)' + { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "uint256", "name": "postId", "type": "uint256" }, { "indexed": false, "internalType": "uint256", "name": "commentId", "type": "uint256" }, { "indexed": true, "internalType": "address", "name": "author", "type": "address" }, { "indexed": false, "internalType": "string", "name": "content", "type": "string" }, { "indexed": false, "internalType": "uint256", "name": "timestamp", "type": "uint256" } ], "name": "CommentAdded", "type": "event" }, + { "inputs": [ { "internalType": "uint256", "name": "postId", "type": "uint256" }, { "internalType": "string", "name": "content", "type": "string" } ], "name": "addComment", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "nonpayable", "type": "function" }, + { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "comments", "outputs": [ { "internalType": "uint256", "name": "id", "type": "uint256" }, { "internalType": "uint256", "name": "postId", "type": "uint256" }, { "internalType": "address", "name": "author", "type": "address" }, { "internalType": "string", "name": "content", "type": "string" }, { "internalType": "uint256", "name": "timestamp", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "uint256", "name": "", "type": "uint256" }, { "internalType": "uint256", "name": "", "type": "uint256" } ], "name": "commentsByPost", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "uint256", "name": "postId", "type": "uint256" } ], "name": "getCommentsForPost", "outputs": [ { "internalType": "uint256[]", "name": "", "type": "uint256[]" } ], "stateMutability": "view", "type": "function" } ] as const; export const FOLLOW_ABI = [ - 'function followUser(address target) external', - 'function unfollowUser(address target) external', - 'function getFollowers(address user) external view returns (address[])', - 'function getFollowing(address user) external view returns (address[])', - 'function getFollowersCount(address user) external view returns (uint256)', - 'function getFollowingCount(address user) external view returns (uint256)', - 'function checkIsFollowing(address follower, address target) external view returns (bool)', - 'event Followed(address indexed follower, address indexed followed)', - 'event Unfollowed(address indexed follower, address indexed unfollowed)' -] as const; \ No newline at end of file + { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "follower", "type": "address" }, { "indexed": true, "internalType": "address", "name": "following", "type": "address" } ], "name": "Followed", "type": "event" }, + { "anonymous": false, "inputs": [ { "indexed": true, "internalType": "address", "name": "follower", "type": "address" }, { "indexed": true, "internalType": "address", "name": "followingEvent", "type": "address" } ], "name": "Unfollowed", "type": "event" }, + { "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], "name": "followUser", "outputs": [], "stateMutability": "nonpayable", "type": "function" }, + { "inputs": [ { "internalType": "address", "name": "", "type": "address" } ], "name": "followerCount", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "address", "name": "", "type": "address" }, { "internalType": "address", "name": "", "type": "address" } ], "name": "following", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "address", "name": "", "type": "address" } ], "name": "followingCount", "outputs": [ { "internalType": "uint256", "name": "", "type": "uint256" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "address", "name": "follower", "type": "address" }, { "internalType": "address", "name": "user", "type": "address" } ], "name": "isFollowing", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "view", "type": "function" }, + { "inputs": [ { "internalType": "address", "name": "user", "type": "address" } ], "name": "unfollowUser", "outputs": [], "stateMutability": "nonpayable", "type": "function" } +] as const; diff --git a/src/lib/wagmi.ts b/src/lib/wagmi.ts index dae1629..a5a9dad 100644 --- a/src/lib/wagmi.ts +++ b/src/lib/wagmi.ts @@ -1,34 +1,41 @@ import { createConfig, http } from 'wagmi'; +import type { Connector } from 'wagmi'; import { base, baseSepolia, hardhat } from 'wagmi/chains'; import { coinbaseWallet, metaMask, walletConnect } from 'wagmi/connectors'; -// WalletConnect project ID (get from https://cloud.walletconnect.com) -const projectId = 'YOUR_WALLETCONNECT_PROJECT_ID'; +const walletConnectProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID as string | undefined; +const baseRpc = (import.meta.env.VITE_BASE_RPC as string | undefined) ?? 'https://mainnet.base.org'; + +const connectors = [ + metaMask(), + coinbaseWallet({ + appName: 'BaseLine', + appLogoUrl: '/baseline-logo.png', + }), + ...(walletConnectProjectId + ? [ + walletConnect({ + projectId: walletConnectProjectId, + metadata: { + name: 'BaseLine', + description: 'A Web3 microblogging platform on Base', + url: 'https://baseline.app', + icons: ['/baseline-logo.png'], + }, + }), + ] + : []), +].filter(Boolean) as unknown as Connector[]; export const config = createConfig({ chains: [base, baseSepolia, hardhat], - connectors: [ - metaMask(), - coinbaseWallet({ - appName: 'BaseLine', - appLogoUrl: '/baseline-logo.png' - }), - walletConnect({ - projectId, - metadata: { - name: 'BaseLine', - description: 'A Web3 microblogging platform on Base', - url: 'https://baseline.app', - icons: ['/baseline-logo.png'] - } - }), - ], + connectors, transports: { - [base.id]: http('https://mainnet.base.org'), + [base.id]: http(baseRpc), [baseSepolia.id]: http('https://sepolia.base.org'), [hardhat.id]: http('http://127.0.0.1:8545'), }, }); export const SUPPORTED_CHAINS = [base, baseSepolia, hardhat]; -export const DEFAULT_CHAIN = base; \ No newline at end of file +export const DEFAULT_CHAIN = base; diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 661cacc..9f2361a 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import React, { useState } from "react"; import ConnectWallet from "@/components/ConnectWallet"; import Dashboard from "@/components/Dashboard"; diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index f9cf7c5..cb46323 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -1,5 +1,5 @@ import { useLocation } from "react-router-dom"; -import { useEffect } from "react"; +import React, { useEffect } from "react"; const NotFound = () => { const location = useLocation(); diff --git a/tailwind.config.ts b/tailwind.config.ts index 022a0d4..d12ac77 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,5 +1,8 @@ import type { Config } from "tailwindcss"; +import type { Config } from "tailwindcss"; +import tailwindcssAnimate from "tailwindcss-animate"; + export default { darkMode: ["class"], content: ["./pages/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./src/**/*.{ts,tsx}"], @@ -114,5 +117,5 @@ export default { }, }, }, - plugins: [require("tailwindcss-animate")], + plugins: [tailwindcssAnimate], } satisfies Config;