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) => (
-
);
};
-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;