From 0dda4f03bcb2c13382697a824c1d15965ff33de9 Mon Sep 17 00:00:00 2001 From: Assad Isah Date: Sat, 20 Sep 2025 10:42:08 +0100 Subject: [PATCH 1/3] feat: add ContractDemo component for interacting with Proof of Fandom contract --- app/profile/page.tsx | 95 ++++++++++++++++++++++++++++++++ components/web3/ContractDemo.tsx | 95 ++++++++++++++++++++++++++++++++ lib/abi/ProofOfFandom.json | 28 ++++++++++ lib/abi/contracts.ts | 12 ++++ 4 files changed, 230 insertions(+) create mode 100644 app/profile/page.tsx create mode 100644 components/web3/ContractDemo.tsx create mode 100644 lib/abi/ProofOfFandom.json create mode 100644 lib/abi/contracts.ts diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..f304d50 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,95 @@ +'use client' + +import { proofOfFandomContract } from '@/lib/contracts' +import { + useAccount, + useReadContract, + useWriteContract, + useWaitForTransactionReceipt, +} from 'wagmi' +import { useState } from 'react' + +export default function ContractDemo() { + const { address, isConnected } = useAccount() + const [minted, setMinted] = useState(false) + + const { data: balance, isLoading: isBalanceLoading } = useReadContract({ + ...proofOfFandomContract, + functionName: 'balanceOf', + args: [address!], + query: { + enabled: isConnected, + }, + }) + + const { data: hash, writeContract, isPending } = useWriteContract() + + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ + hash, + }) + + const handleMint = () => { + writeContract({ + ...proofOfFandomContract, + functionName: 'mint', + args: [], + }) + } + + if (isConfirmed && !minted) { + setMinted(true) + } + + if (!isConnected) { + return ( +

+ Please connect your wallet to interact with the contract. +

+ ) + } + + return ( +
+

Proof of Fandom Contract

+ +
+

Your Fandom Token Balance:

+ {isBalanceLoading ? ( + Loading... + ) : ( + + {balance?.toString() ?? '0'} + + )} +
+ +
+ + {hash && ( +

+ Transaction Hash: {`${hash.slice(0, 6)}...${hash.slice(-4)}`} +

+ )} + {minted && ( +

Minted Successfully!

+ )} +
+

+ Note: This interacts with a dummy contract address. The transaction will + be sent but is expected to fail on-chain. This demo is to verify the + frontend flow. +

+
+ ) +} diff --git a/components/web3/ContractDemo.tsx b/components/web3/ContractDemo.tsx new file mode 100644 index 0000000..f304d50 --- /dev/null +++ b/components/web3/ContractDemo.tsx @@ -0,0 +1,95 @@ +'use client' + +import { proofOfFandomContract } from '@/lib/contracts' +import { + useAccount, + useReadContract, + useWriteContract, + useWaitForTransactionReceipt, +} from 'wagmi' +import { useState } from 'react' + +export default function ContractDemo() { + const { address, isConnected } = useAccount() + const [minted, setMinted] = useState(false) + + const { data: balance, isLoading: isBalanceLoading } = useReadContract({ + ...proofOfFandomContract, + functionName: 'balanceOf', + args: [address!], + query: { + enabled: isConnected, + }, + }) + + const { data: hash, writeContract, isPending } = useWriteContract() + + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ + hash, + }) + + const handleMint = () => { + writeContract({ + ...proofOfFandomContract, + functionName: 'mint', + args: [], + }) + } + + if (isConfirmed && !minted) { + setMinted(true) + } + + if (!isConnected) { + return ( +

+ Please connect your wallet to interact with the contract. +

+ ) + } + + return ( +
+

Proof of Fandom Contract

+ +
+

Your Fandom Token Balance:

+ {isBalanceLoading ? ( + Loading... + ) : ( + + {balance?.toString() ?? '0'} + + )} +
+ +
+ + {hash && ( +

+ Transaction Hash: {`${hash.slice(0, 6)}...${hash.slice(-4)}`} +

+ )} + {minted && ( +

Minted Successfully!

+ )} +
+

+ Note: This interacts with a dummy contract address. The transaction will + be sent but is expected to fail on-chain. This demo is to verify the + frontend flow. +

+
+ ) +} diff --git a/lib/abi/ProofOfFandom.json b/lib/abi/ProofOfFandom.json new file mode 100644 index 0000000..9486142 --- /dev/null +++ b/lib/abi/ProofOfFandom.json @@ -0,0 +1,28 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/lib/abi/contracts.ts b/lib/abi/contracts.ts new file mode 100644 index 0000000..47bcfeb --- /dev/null +++ b/lib/abi/contracts.ts @@ -0,0 +1,12 @@ +import proofOfFandomAbi from './abi/ProofOfFandom.json' +import { flare, songbird } from './chains' + +const proofOfFandomAddress = + '0x1234567890123456789012345678901234567890' as const + +export const proofOfFandomContract = { + address: proofOfFandomAddress, + abi: proofOfFandomAbi, + + chains: [flare.id, songbird.id], +} From 8718bcc2e2edd0cbf5a6994f46dc2c71a2c581b3 Mon Sep 17 00:00:00 2001 From: Assad Isah Date: Sat, 20 Sep 2025 11:34:46 +0100 Subject: [PATCH 2/3] feat: implement FandomMatchModal for minting Proof of Fandom badges and update swipe handling for fandom matches --- components/shared/SwipeDeck.tsx | 13 ++-- components/web3/FandomMatchModal.tsx | 97 ++++++++++++++++++++++++++++ lib/api-schema.ts | 2 +- lib/msw/handlers.ts | 6 +- stores/auth-store.ts | 27 +++++++- 5 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 components/web3/FandomMatchModal.tsx diff --git a/components/shared/SwipeDeck.tsx b/components/shared/SwipeDeck.tsx index e19bd46..293156f 100644 --- a/components/shared/SwipeDeck.tsx +++ b/components/shared/SwipeDeck.tsx @@ -41,7 +41,7 @@ const postSwipe = async ( export default function SwipeDeck() { const [users, setUsers] = useState([]) const queryClient = useQueryClient() - const openMatchModal = useAuthStore((state) => state.openMatchModal) + const { openMatchModal, openFandomModal } = useAuthStore() const { isLoading, isError } = useQuery({ queryKey: ['potentialMatches'], @@ -54,18 +54,19 @@ export default function SwipeDeck() { const swipeMutation = useMutation({ mutationFn: postSwipe, onSuccess: ({ res, swipedUser }) => { - // Check for match and the new details payload if (res.isMatch && res.compatibilityDetails) { - openMatchModal(swipedUser, res.compatibilityDetails) // Pass details to the store + if (res.isFandomMatch) { + openFandomModal(swipedUser, res.compatibilityDetails) + } else { + openMatchModal(swipedUser, res.compatibilityDetails) + } } + queryClient.prefetchQuery({ queryKey: ['potentialMatches'], queryFn: fetchPotentialMatches, }) }, - onError: (error) => { - console.error('Swipe mutation failed:', error) - }, }) const onSwipe = (direction: 'left' | 'right') => { diff --git a/components/web3/FandomMatchModal.tsx b/components/web3/FandomMatchModal.tsx new file mode 100644 index 0000000..3b84080 --- /dev/null +++ b/components/web3/FandomMatchModal.tsx @@ -0,0 +1,97 @@ +'use client' + +import { proofOfFandomContract } from '@/lib/abi/contracts' +import { useAuthStore } from '@/stores/auth-store' +import { AnimatePresence, motion } from 'framer-motion' +import Image from 'next/image' +import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi' + +export default function FandomMatchModal() { + const { + isFandomModalOpen, + matchedUser, + compatibilityDetails, + closeFandomModal, + } = useAuthStore() + + const { data: hash, writeContract, isPending, error } = useWriteContract() + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ hash }) + + const handleMint = () => { + writeContract({ + ...proofOfFandomContract, + functionName: 'mint', + args: [], + }) + } + + const getButtonText = () => { + if (isConfirming) return 'Minting...' + if (isPending) return 'Check Wallet...' + if (isConfirmed) return 'Minted Successfully!' + return "Mint 'Proof of Fandom' Badge" + } + + return ( + + {isFandomModalOpen && matchedUser && compatibilityDetails && ( + + e.stopPropagation()} + > +

Fandom Match!

+

+ You and {matchedUser.name} both love{' '} + + {compatibilityDetails.artist.overlap[0]} + + ! +

+

+ Mint a unique, non-transferable Soulbound Token (SBT) on the Flare + network to commemorate this connection forever. +

+ +
+ +
+ + {hash && ( +

+ Tx Hash: {`${hash.slice(0, 8)}...`} +

+ )} + {error && ( +

+ Error: {error.shortMessage} +

+ )} + + +
+
+ )} +
+ ) +} diff --git a/lib/api-schema.ts b/lib/api-schema.ts index f1390ec..2070eae 100644 --- a/lib/api-schema.ts +++ b/lib/api-schema.ts @@ -70,6 +70,6 @@ export type CompatibilityDetails = { export type SwipeResponse = { isMatch: boolean matchId?: string - // Add the new compatibility details + isFandomMatch?: boolean compatibilityDetails?: CompatibilityDetails } diff --git a/lib/msw/handlers.ts b/lib/msw/handlers.ts index 1b61789..62ad47e 100644 --- a/lib/msw/handlers.ts +++ b/lib/msw/handlers.ts @@ -99,7 +99,6 @@ export const handlers = [ let response: SwipeResponse if (direction === 'right' && isMatch) { - // Generate fake compatibility data for the chart const compatibilityDetails: CompatibilityDetails = { genre: { score: faker.number.int({ min: 60, max: 95 }), @@ -113,10 +112,13 @@ export const handlers = [ obscurity: { score: faker.number.int({ min: 40, max: 75 }) }, } + const isFandomMatch = Math.random() < 0.25 + response = { isMatch: true, matchId: faker.string.uuid(), - compatibilityDetails, // Add the new data to the response + compatibilityDetails, + isFandomMatch, } } else { response = { diff --git a/stores/auth-store.ts b/stores/auth-store.ts index a3bc94d..bf84c9c 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -1,5 +1,10 @@ import { create } from 'zustand' -import type { Message, UserProfile } from '@/lib/api-schema' +import type { + CompatibilityDetails, + Message, + UserProfile, +} from '@/lib/api-schema' +import { details } from 'framer-motion/client' interface AuthState { user: UserProfile | null @@ -10,6 +15,7 @@ interface AuthState { compatibilityDetails: CompatibilityDetails | null isMatchModalOpen: boolean matchedUser: UserProfile | null + isFandomModalOpen: boolean login: (user: UserProfile, token: string) => void logout: () => void @@ -23,6 +29,8 @@ interface AuthState { tempId: string, status: 'sending' | 'failed' ) => void + openFandomModal: (user: UserProfile, details: CompatibilityDetails) => void + closeFandomModal: () => void } export const useAuthStore = create((set) => ({ @@ -34,6 +42,7 @@ export const useAuthStore = create((set) => ({ selectedMatchId: null, messageQueue: [], compatibilityDetails: null, + isFandomModalOpen: false, login: (user, token) => set({ @@ -80,4 +89,20 @@ export const useAuthStore = create((set) => ({ m.tempId === tempId ? { ...m, status } : m ), })), + + openFandomModal: (user, details) => + set({ + isFandomModalOpen: true, + + matchedUser: user, + compatibilityDetails: details, + }), + + closeFandomModal: () => + set({ + isFandomModalOpen: false, + + matchedUser: null, + compatibilityDetails: null, + }), })) From 2f68971956fda3a388b332db256470b3fc1a191f Mon Sep 17 00:00:00 2001 From: Assad Isah Date: Sat, 20 Sep 2025 12:02:52 +0100 Subject: [PATCH 3/3] feat: update Home component to use AuthManager and FandomMatchModal for improved user authentication flow --- app/page.tsx | 14 ++++++++++---- components/AuthManager.tsx | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 components/AuthManager.tsx diff --git a/app/page.tsx b/app/page.tsx index 83570fb..4c6c85f 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,20 +1,26 @@ 'use client' +import AuthManager from '@/components/AuthManager' import MatchModal from '@/components/shared/MatchModal' -import SpotifyConnect from '@/components/shared/SpotifyConnect' import SwipeDeck from '@/components/shared/SwipeDeck' +import FandomMatchModal from '@/components/web3/FandomMatchModal' +import { useAuthStore } from '@/stores/auth-store' import { useSession } from 'next-auth/react' import Link from 'next/link' export default function Home() { - const { data: session, status } = useSession() + const { isAuthenticated } = useAuthStore() + const { data: session } = useSession() + + const isLoggedIn = isAuthenticated || session?.user return (
+

Chordially

- {status === 'authenticated' && ( + {isLoggedIn && ( )}
- {status === 'authenticated' ? : } + {isLoggedIn ? : }
) } diff --git a/components/AuthManager.tsx b/components/AuthManager.tsx new file mode 100644 index 0000000..e9209c6 --- /dev/null +++ b/components/AuthManager.tsx @@ -0,0 +1,34 @@ +'use client' + +import { useAuthStore } from '@/stores/auth-store' +import { useAccount } from 'wagmi' +import SpotifyConnect from '../shared/SpotifyConnect' +import ConnectWalletButton from './ConnectWalletButton' +import SignInWithFlareButton from './SignInWithFlareButton' + +export default function AuthManager() { + const { isAuthenticated } = useAuthStore() + const { isConnected } = useAccount() + + if (isAuthenticated) { + return null // Don't show anything if the user is already logged in + } + + return ( +
+

Choose Your Login Method

+
+ {/* Web3 Login Flow */} +
+ + {isConnected && } +
+ +
+ + {/* Web2 Login Flow */} + +
+
+ ) +}