Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className='flex min-h-screen flex-col items-center justify-center p-4 md:p-8'>
<FandomMatchModal />
<MatchModal />
<div className='w-full max-w-2xl text-center mb-8'>
<h1 className='text-4xl font-bold'>Chordially</h1>
{status === 'authenticated' && (
{isLoggedIn && (
<Link
href='/chat'
className='text-blue-500 hover:underline mt-4 inline-block'
Expand All @@ -23,7 +29,7 @@ export default function Home() {
</Link>
)}
</div>
{status === 'authenticated' ? <SwipeDeck /> : <SpotifyConnect />}
{isLoggedIn ? <SwipeDeck /> : <AuthManager />}
</main>
)
}
13 changes: 10 additions & 3 deletions app/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useSession } from 'next-auth/react'
import { redirect } from 'next/navigation'
import type { UserProfile } from '@/lib/api-schema'
import ProfileForm from '@/components/profile/ProfileForm'
import ContractDemo from '@/components/web3/ContractDemo'

const fetchProfile = async (): Promise<UserProfile> => {
const res = await fetch('/api/profile')
Expand All @@ -13,7 +14,6 @@ const fetchProfile = async (): Promise<UserProfile> => {
}

export default function ProfilePage() {
// Protect the route
const { status } = useSession({
required: true,
onUnauthenticated() {
Expand All @@ -36,8 +36,15 @@ export default function ProfilePage() {

return (
<main className='container mx-auto max-w-2xl py-12'>
<h1 className='text-3xl font-bold mb-8'>Edit Your Profile</h1>
{profile && <ProfileForm profile={profile} />}
<div>
<h1 className='text-3xl font-bold mb-8'>Edit Your Profile</h1>
{profile && <ProfileForm profile={profile} />}
</div>

<div>
<h2 className='text-2xl font-bold mb-4'>Web3 Contract Interaction</h2>
<ContractDemo />
</div>
</main>
)
}
34 changes: 34 additions & 0 deletions components/AuthManager.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className='flex flex-col items-center gap-4 p-8 border rounded-lg'>
<h2 className='text-xl font-semibold'>Choose Your Login Method</h2>
<div className='flex items-center gap-4'>
{/* Web3 Login Flow */}
<div className='flex flex-col items-center gap-2'>
<ConnectWalletButton />
{isConnected && <SignInWithFlareButton />}
</div>

<div className='self-stretch border-l mx-4'></div>

{/* Web2 Login Flow */}
<SpotifyConnect />
</div>
</div>
)
}
13 changes: 7 additions & 6 deletions components/shared/SwipeDeck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const postSwipe = async (
export default function SwipeDeck() {
const [users, setUsers] = useState<UserProfile[]>([])
const queryClient = useQueryClient()
const openMatchModal = useAuthStore((state) => state.openMatchModal)
const { openMatchModal, openFandomModal } = useAuthStore()

const { isLoading, isError } = useQuery({
queryKey: ['potentialMatches'],
Expand All @@ -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') => {
Expand Down
95 changes: 95 additions & 0 deletions components/web3/ContractDemo.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<p className='text-center text-gray-500'>
Please connect your wallet to interact with the contract.
</p>
)
}

return (
<div className='p-6 border rounded-lg space-y-4'>
<h3 className='text-xl font-bold'>Proof of Fandom Contract</h3>

<div>
<p>Your Fandom Token Balance:</p>
{isBalanceLoading ? (
<span className='text-gray-400'>Loading...</span>
) : (
<span className='text-2xl font-mono'>
{balance?.toString() ?? '0'}
</span>
)}
</div>

<div className='flex flex-col items-start space-y-2'>
<button
onClick={handleMint}
disabled={isPending || isConfirming}
className='bg-blue-600 hover:bg-blue-800 text-white font-bold py-2 px-4 rounded disabled:bg-gray-400'
>
{isPending
? 'Check Wallet...'
: isConfirming
? 'Minting...'
: 'Mint a Fandom Token'}
</button>
{hash && (
<p className='text-sm text-gray-500'>
Transaction Hash: {`${hash.slice(0, 6)}...${hash.slice(-4)}`}
</p>
)}
{minted && (
<p className='text-green-500 font-bold'>Minted Successfully!</p>
)}
</div>
<p className='text-xs text-gray-400'>
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.
</p>
</div>
)
}
97 changes: 97 additions & 0 deletions components/web3/FandomMatchModal.tsx
Original file line number Diff line number Diff line change
@@ -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'

Check warning on line 6 in components/web3/FandomMatchModal.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

'Image' is defined but never used
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 (
<AnimatePresence>
{isFandomModalOpen && matchedUser && compatibilityDetails && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4'
onClick={closeFandomModal}
>
<motion.div
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className='bg-gradient-to-br from-blue-600 to-indigo-800 p-6 rounded-2xl text-white text-center shadow-2xl w-full max-w-md'
onClick={(e) => e.stopPropagation()}
>
<h1 className='text-4xl font-bold'>Fandom Match!</h1>
<p className='text-lg my-4'>
You and {matchedUser.name} both love{' '}
<span className='font-bold'>
{compatibilityDetails.artist.overlap[0]}
</span>
!
</p>
<p className='text-md opacity-90'>
Mint a unique, non-transferable Soulbound Token (SBT) on the Flare
network to commemorate this connection forever.
</p>

<div className='my-6'>
<button
onClick={handleMint}
disabled={isPending || isConfirming || isConfirmed}
className='bg-white text-indigo-700 font-bold py-3 px-6 rounded-full disabled:opacity-50 transition-opacity'
>
{getButtonText()}
</button>
</div>

{hash && (
<p className='text-xs opacity-75'>
Tx Hash: {`${hash.slice(0, 8)}...`}
</p>
)}
{error && (
<p className='text-xs text-red-300 mt-2'>
Error: {error.shortMessage}
</p>
)}

<button
onClick={closeFandomModal}
className='mt-4 text-xs text-white/70 hover:text-white'
>
Maybe Later
</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}
28 changes: 28 additions & 0 deletions lib/abi/ProofOfFandom.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
12 changes: 12 additions & 0 deletions lib/abi/contracts.ts
Original file line number Diff line number Diff line change
@@ -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],
}
2 changes: 1 addition & 1 deletion lib/api-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@ export type CompatibilityDetails = {
export type SwipeResponse = {
isMatch: boolean
matchId?: string
// Add the new compatibility details
isFandomMatch?: boolean
compatibilityDetails?: CompatibilityDetails
}
Loading
Loading