diff --git a/.eslintrc.json b/.eslintrc.json index 3f92d73..4060e80 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,6 +4,6 @@ "@next/next/no-img-element": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-empty-object-type": "off", - "@typescript-eslint/no-unused-vars":"off" + "@typescript-eslint/no-unused-vars": "off" } } diff --git a/app/host/page.tsx b/app/host/page.tsx new file mode 100644 index 0000000..1b63f2b --- /dev/null +++ b/app/host/page.tsx @@ -0,0 +1,122 @@ +'use client'; + +import EventForm from '@/components/custom/EventForm'; +import FeaturedEvent from '@/components/custom/FeaturedEvent'; +import Footer from '@/components/custom/footer'; +import Header from '@/components/custom/header'; +import { Button } from '@/components/ui/button'; +import { FlipWords } from '@/components/ui/flip-words'; +import { useRouter } from 'next/navigation'; +import React, { useEffect, useRef, useState } from 'react'; +import { motion } from 'framer-motion'; +import { EventFormData } from '@/components/custom/EventForm'; +import { createEvent } from '@/lib/createEvent'; + +const Page = () => { + const router = useRouter(); + const [isClient, setIsClient] = useState(false); + const inputRef = useRef(null); + + useEffect(() => { + setIsClient(true); + }, []); + + function searchForEvents() { + if (inputRef.current && inputRef.current.value === '') return; + + if (inputRef.current) + router.replace('/events?q=' + encodeURIComponent(inputRef.current.value)); + } + + function handleSubmit(data: { + name: string; + description: string; + capacity: number; + ticketPrice: number; + location: string; + eventStartTime: Date; + eventEndTime?: Date | undefined; + images?: string[] | undefined; + }) { + createEvent({ + name: data.name, + description: data.description, + location: data.location, + capacity: data.capacity, + ticketPrice: data.ticketPrice, + startDate: data.eventStartTime, + endDate: data.eventEndTime || new Date(), + images: data.images || [], + toast: ({ title, variant }) => { + alert(title); + }, + }); + router.push('/events'); + } + + const words = [ + 'adventure', + 'concert', + 'outing', + 'journey', + 'hackathon', + 'conference', + ]; + + return ( + <> +
+
+ {/* Video Background */} + {isClient && ( + + )} + + {/* Dark Overlay for Enhanced Readability */} +
+ + {/* Page Content Over the Video */} +
+
+ +
+ + Create your event here! + + + + +
+
+
+
+
+
+ + ); +}; + +export default Page; diff --git a/components/EventPage.tsx b/components/EventPage.tsx index 0c7e0eb..75287e2 100644 --- a/components/EventPage.tsx +++ b/components/EventPage.tsx @@ -16,3 +16,10 @@ const EventPage = () => { }; export default EventPage; + +// profile page + +// EventForm to Register Events +// on submit form +// fix ui to match ticketchain initial ui +// diff --git a/components/ProfilePage.tsx b/components/ProfilePage.tsx new file mode 100644 index 0000000..6bd0bb8 --- /dev/null +++ b/components/ProfilePage.tsx @@ -0,0 +1,123 @@ +import { useRouter } from 'next/router'; +// import { Input } from 'postcss'; // Removed incorrect import +import React, { useEffect, useRef, useState } from 'react'; +import FeaturedEvent from './custom/FeaturedEvent'; +import Footer from './custom/footer'; +import Header from './custom/header'; +import { Button } from './ui/button'; +import { FlipWords } from './ui/flip-words'; + +const ProfilePage = () => { + const router = useRouter(); + const [isClient, setIsClient] = useState(false); + const inputRef: any = useRef(null); + + useEffect(() => { + setIsClient(true); + }, []); + + function searchForEvents() { + if (inputRef.current.value == '') return; + router.replace('/events?q=' + encodeURIComponent(inputRef.current.value)); + } + + const words = [ + 'adventure', + 'concert', + 'outing', + 'journey', + 'hackathon', + 'conference', + ]; + + return ( + <> +
+
+ {/* Video Background */} + {isClient && ( + + )} + + {/* Dark Overlay for Enhanced Readability */} +
+ + {/* Page Content Over the Video */} +
+
+
+
+ Book your next + + on the Flare blockchain. +
+
+ +
+ + +
+
+
+ + + +
+
+
+
+
+
+ + ); +}; + +export default ProfilePage; diff --git a/components/custom/EventDescription.tsx b/components/custom/EventDescription.tsx index dd22d2d..974790d 100644 --- a/components/custom/EventDescription.tsx +++ b/components/custom/EventDescription.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Card, CardHeader, @@ -13,7 +13,7 @@ import { buyHandler } from '@/lib/buyHandler'; import { useToast } from '@/hooks/use-toast'; import NumberPicker from './TicketButton'; -interface EventDescriptionProps { +export interface EventDescriptionProps { eventDetails: { EventID: number; name: string; @@ -32,8 +32,10 @@ const EventDescription: React.FC = ({ eventDetails, }) => { const { toast } = useToast(); + const [numTickets, setNumTickets] = useState(1); + const handleBuyNow = () => { - buyHandler(eventDetails.EventID, toast); + buyHandler(eventDetails.EventID, numTickets, toast); }; return ( @@ -79,7 +81,10 @@ const EventDescription: React.FC = ({
diff --git a/components/custom/EventForm.tsx b/components/custom/EventForm.tsx index 9273dd0..f6dfe64 100644 --- a/components/custom/EventForm.tsx +++ b/components/custom/EventForm.tsx @@ -58,7 +58,7 @@ const eventSchema = z }); // Define the TypeScript type based on the Zod schema -type EventFormData = z.infer; +export type EventFormData = z.infer; interface EventFormProps { onSubmit: (data: EventFormData) => void; diff --git a/components/Profile.tsx b/components/custom/Profile.tsx similarity index 100% rename from components/Profile.tsx rename to components/custom/Profile.tsx diff --git a/components/custom/TicketButton.tsx b/components/custom/TicketButton.tsx index 0d42a1f..7c330f1 100644 --- a/components/custom/TicketButton.tsx +++ b/components/custom/TicketButton.tsx @@ -1,6 +1,5 @@ -'use client'; -import React, { useState } from 'react'; -import { Button } from '../ui/button'; // Adjust import path to where your shadcn Button component is located +import React from 'react'; +import { Button } from '../ui/button'; interface NumberPickerProps { initialCount?: number; @@ -15,25 +14,23 @@ const NumberPicker: React.FC = ({ max = 10, onChange, }) => { - const [count, setCount] = useState(initialCount); + const [count, setCount] = React.useState(initialCount); + + React.useEffect(() => { + if (onChange) { + onChange(count); + } + }, [count, onChange]); const increment = () => { if (count < max) { - const newCount = count + 1; - setCount(newCount); - if (onChange) { - onChange(newCount); - } + setCount(count + 1); } }; const decrement = () => { if (count > min) { - const newCount = count - 1; - setCount(newCount); - if (onChange) { - onChange(newCount); - } + setCount(count - 1); } }; diff --git a/components/custom/header.tsx b/components/custom/header.tsx index 72dbe82..286ba75 100644 --- a/components/custom/header.tsx +++ b/components/custom/header.tsx @@ -65,6 +65,16 @@ const Header = () => { +
  • + + + Host Event + + +
  • => { if (eventId < 0) { @@ -24,42 +25,62 @@ export const buyHandler = async ( return; } - try { - if (typeof window.ethereum === 'undefined') { - toast({ - title: 'Please install MetaMask or another Ethereum wallet', - variant: 'destructive', - }); - return; - } - - const provider = new ethers.providers.Web3Provider(window.ethereum); - const signer = provider.getSigner(); - const contract = getContract().connect(signer); - - let ticketCost = await contract.getEventPriceFlare(eventId); - ticketCost = ticketCost.mul(105).div(100); - const balance = await provider.getBalance(await signer.getAddress()); - - if (balance.lt(ticketCost)) { - toast({ - title: 'Insufficient balance to cover ticket cost and gas fees.', - variant: 'destructive', - }); - return; - } - - const tx = await contract.buyTicket(eventId, { value: ticketCost }); - const receipt = await tx.wait(); - + if (numTickets <= 0) { toast({ - title: `Ticket purchased successfully! Transaction Hash: ${receipt.transactionHash}`, - }); - } catch (error) { - console.error('Error buying ticket:', error); - toast({ - title: 'Transaction failed. Please try again.', + title: 'Please select at least one ticket.', variant: 'destructive', }); + return; + } + + toast({ + title: + 'You might be asked to approve multiple transactions if you buy multiple tickets', + }); + + while (numTickets > 0) { + try { + if (typeof window.ethereum === 'undefined') { + toast({ + title: 'Please install MetaMask or another Ethereum wallet', + variant: 'destructive', + }); + return; + } + + const provider = new ethers.providers.Web3Provider(window.ethereum); + const signer = provider.getSigner(); + const contract = getContract().connect(signer); + + const singleTicketCost = await contract.getEventPriceFlare(eventId); + const totalTicketCost = singleTicketCost + .mul(numTickets) + .mul(105) + .div(100); + + const balance = await provider.getBalance(await signer.getAddress()); + if (balance.lt(totalTicketCost)) { + toast({ + title: 'Insufficient balance to cover ticket cost and gas fees.', + variant: 'destructive', + }); + return; + } + + const tx = await contract.buyTicket(eventId, { value: totalTicketCost }); + const receipt = await tx.wait(); + + toast({ + title: `Tickets purchased successfully! Transaction Hash: ${receipt.transactionHash}`, + }); + + numTickets -= 1; + } catch (error) { + console.error('Error buying tickets:', error); + toast({ + title: 'Transaction failed. Please try again.', + variant: 'destructive', + }); + } } }; diff --git a/lib/createEvent.ts b/lib/createEvent.ts new file mode 100644 index 0000000..31a6f5b --- /dev/null +++ b/lib/createEvent.ts @@ -0,0 +1,91 @@ +import { ethers } from 'ethers'; +import { getContract } from './ethers'; + +interface CreateEventProps { + name: string; + description: string; + location: string; + capacity: number; + ticketPrice: number; + startDate: Date; + endDate: Date; + images: string[]; + toast: ToastFunction; +} + +type ToastFunction = (options: { + title: string; + variant?: 'default' | 'destructive' | null | undefined; +}) => void; + +declare global { + interface Window { + ethereumProvider?: ethers.providers.ExternalProvider & { + isMetaMask?: boolean; + request?: (method: string, params?: unknown[]) => Promise; + }; + } +} + +export const createEvent = async ({ + name, + description, + location, + capacity, + ticketPrice, + startDate, + endDate, + images, + toast, +}: CreateEventProps) => { + try { + console.log('Starting createEvent function'); + if (typeof window.ethereum === 'undefined') { + console.error('Ethereum provider not found'); + toast({ + title: 'Please install MetaMask or another Ethereum wallet', + variant: 'destructive', + }); + return; + } + + console.log('Connecting to Ethereum provider'); + const provider = new ethers.providers.Web3Provider(window.ethereum); + const signer = provider.getSigner(); + const contract = getContract().connect(signer); + + console.log('Preparing transaction data'); + const tx = await contract.createEvent( + name, + description, + capacity, + ethers.utils.parseEther(ticketPrice.toString()), + Math.floor(startDate.getTime() / 1000), + images + ); + + console.log('Transaction sent, waiting for confirmation'); + const receipt = await tx.wait(); + + console.log('Transaction confirmed:', receipt.transactionHash); + toast({ + title: `Event created successfully! Transaction Hash: ${receipt.transactionHash}`, + }); + + return receipt.transactionHash; + } catch (error) { + console.error('Error in createEvent:', error); + if (error instanceof Error) { + toast({ + title: `Transaction failed: ${error.message}`, + variant: 'destructive', + }); + } else { + toast({ + title: 'Transaction failed. Please try again.', + variant: 'destructive', + }); + } + throw error; + } +}; diff --git a/lib/ethers.ts b/lib/ethers.ts index f16d3ab..09df0fb 100644 --- a/lib/ethers.ts +++ b/lib/ethers.ts @@ -1,4 +1,5 @@ import { ethers } from 'ethers'; +import EventManagerABI from '../contracts/EventManagerABI.json'; const FLARE_TESTNET_RPC_URL = process.env.NEXT_PUBLIC_RPC_URL; const CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS; @@ -12,509 +13,6 @@ export function getFlareProvider() { export function getContract() { const provider = getFlareProvider(); const contractAddress = CONTRACT_ADDRESS; - const contractABI = [ - { - inputs: [ - { - internalType: 'uint256', - name: '_ticketId', - type: 'uint256', - }, - { - internalType: 'address', - name: '_to', - type: 'address', - }, - { - internalType: 'bool', - name: '_allowed', - type: 'bool', - }, - ], - name: 'approveTicket', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_eventId', - type: 'uint256', - }, - ], - name: 'buyTicket', - outputs: [ - { - internalType: 'uint256', - name: '_ticketId', - type: 'uint256', - }, - ], - stateMutability: 'payable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'string', - name: '_name', - type: 'string', - }, - { - internalType: 'string', - name: '_description', - type: 'string', - }, - { - internalType: 'uint256', - name: '_capacity', - type: 'uint256', - }, - { - internalType: 'uint256', - name: '_ticketPrice', - type: 'uint256', - }, - { - internalType: 'uint256', - name: '_eventDate', - type: 'uint256', - }, - { - internalType: 'string[]', - name: '_images', - type: 'string[]', - }, - ], - name: 'createEvent', - outputs: [ - { - internalType: 'uint256', - name: '_eventId', - type: 'uint256', - }, - ], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'eventId', - type: 'uint256', - }, - { - indexed: false, - internalType: 'string', - name: 'name', - type: 'string', - }, - { - indexed: false, - internalType: 'uint256', - name: 'eventDate', - type: 'uint256', - }, - ], - name: 'EventCreated', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'ticketId', - type: 'uint256', - }, - { - indexed: false, - internalType: 'uint256', - name: 'eventId', - type: 'uint256', - }, - { - indexed: false, - internalType: 'address', - name: 'buyer', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'price', - type: 'uint256', - }, - ], - name: 'TicketPurchased', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'ticketId', - type: 'uint256', - }, - { - indexed: false, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: false, - internalType: 'address', - name: 'trustee', - type: 'address', - }, - ], - name: 'TicketTransferApproved', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'ticketId', - type: 'uint256', - }, - { - indexed: false, - internalType: 'address', - name: 'from', - type: 'address', - }, - { - indexed: false, - internalType: 'address', - name: 'to', - type: 'address', - }, - ], - name: 'TicketTransferred', - type: 'event', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_ticketId', - type: 'uint256', - }, - { - internalType: 'address', - name: '_to', - type: 'address', - }, - ], - name: 'transferTicket', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_ticketId', - type: 'uint256', - }, - { - internalType: 'address', - name: '_to', - type: 'address', - }, - ], - name: 'transferTicketFrom', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_cents', - type: 'uint256', - }, - ], - name: 'centsToFlare', - outputs: [ - { - internalType: 'uint256', - name: '_flr', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'eventCounter', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - name: 'events', - outputs: [ - { - internalType: 'string', - name: 'name', - type: 'string', - }, - { - internalType: 'string', - name: 'description', - type: 'string', - }, - { - internalType: 'uint256', - name: 'capacity', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'ticketsSold', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'ticketPrice', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'eventDate', - type: 'uint256', - }, - { - internalType: 'address payable', - name: 'eventHost', - type: 'address', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - name: 'feedIds', - outputs: [ - { - internalType: 'bytes21', - name: '', - type: 'bytes21', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_eventId', - type: 'uint256', - }, - ], - name: 'getEventImages', - outputs: [ - { - internalType: 'string[]', - name: '', - type: 'string[]', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_eventId', - type: 'uint256', - }, - ], - name: 'getEventPriceFlare', - outputs: [ - { - internalType: 'uint256', - name: '_flr', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '_eventId', - type: 'uint256', - }, - ], - name: 'getEventTickets', - outputs: [ - { - internalType: 'uint256[]', - name: '', - type: 'uint256[]', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getFlareFeed', - outputs: [ - { - internalType: 'uint256', - name: '_feedValue', - type: 'uint256', - }, - { - internalType: 'int8', - name: '_decimals', - type: 'int8', - }, - { - internalType: 'uint64', - name: '_timestamp', - type: 'uint64', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'getFtsoV2CurrentFeedValues', - outputs: [ - { - internalType: 'uint256[]', - name: '_feedValues', - type: 'uint256[]', - }, - { - internalType: 'int8[]', - name: '_decimals', - type: 'int8[]', - }, - { - internalType: 'uint64', - name: '_timestamp', - type: 'uint64', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'ticketCounter', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - name: 'tickets', - outputs: [ - { - internalType: 'address', - name: 'holder', - type: 'address', - }, - { - internalType: 'uint256', - name: 'boughtTime', - type: 'uint256', - }, - { - internalType: 'uint256', - name: 'eventId', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - internalType: 'address', - name: '', - type: 'address', - }, - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - name: 'userTickets', - outputs: [ - { - internalType: 'uint256', - name: '', - type: 'uint256', - }, - ], - stateMutability: 'view', - type: 'function', - }, - ]; + const contractABI = EventManagerABI; return new ethers.Contract(contractAddress!, contractABI, provider); }