mirror of
https://github.com/0xShay/ticketchain.git
synced 2026-01-10 20:53:24 +00:00
finished conflicts in future merges after last components branch commit
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_RPC_URL=
|
||||
NEXT_PUBLIC_CONTRACT_ADDRESS=
|
||||
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
||||
"rules": {
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -26,7 +26,8 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
.env
|
||||
.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -34,3 +35,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
artifacts
|
||||
typechain-types
|
||||
cache
|
||||
44
README.md
44
README.md
@@ -1,36 +1,22 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
# TicketChain
|
||||
|
||||
## Getting Started
|
||||
## Problem
|
||||
|
||||
First, run the development server:
|
||||
Many popular ticket sites exist, that allow customers to browse different upcoming events in their area and book tickets, however we identified several issues with these centralised service model:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
- Tickets cannot be easily transferred
|
||||
- Many existing platforms make it difficult if not impossible to transfer tickets between different users. This means people don't have _true_ ownership over their purchased tickets.
|
||||
- Complex user interfaces
|
||||
- User interfaces on many of these websites can be quite complex and complicated, which can make it hard to book the right ticket and can often lead to customers making costly mistakes.
|
||||
- Scalability issues
|
||||
- When popular events are released, many of these websites can slow down or sometimes even crash, worsening the experience for the end user.
|
||||
- Single point of failure
|
||||
- These central services consolidate all of the data and compute for their services in one spot. One possible risk to consider is a cyberattack - if the central organisation is compromised, all customer data is at risk of being stolen and leaked. Another possible risk is the central server going down, meaning ticket services would go down, which is not ideal if thousands of attendees are trying to verify themselves at an event.
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
## Proposal
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
We propose to build a solution which tackles these issues head-on. TicketChain is a decentralised website which allows users to browse upcoming events, and book tickets for these events through verifiable, immutable blockchain transactions. This allows users to purchase tickets. Functionality also exists for users to transfer their tickets to other users.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
This is made possible through the use of smart contracts deployed on the blockchain which expose several public read and write methods, which can be invoked through our front-end user interface. A record of transactions are kept secure on the blockchain, and no central entity can tamper with these transactions.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
Because the main application logic is on the blockchain, this means we can exploit the blockchains' scalability and durability during high levels of demand. The workload of execution is split between nodes and transaction gas fees pay for the compute required during levels of high demand for the service, solving the issue of central services being unscalable and avoiding us from having a single point of failure.
|
||||
|
||||
19
app/contact/page.tsx
Normal file
19
app/contact/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Header from '../../components/custom/header';
|
||||
import Footer from '../../components/custom/footer';
|
||||
|
||||
const ContactPage: React.FC = () => {
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden bg-black">
|
||||
<Header />
|
||||
<div className="relative z-20 container mx-auto p-4 pt-16">
|
||||
{/* implement contact page here */}
|
||||
<p className="text-white">Page to be implemented</p>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactPage;
|
||||
49
app/events/[...eventId]/page.tsx
Normal file
49
app/events/[...eventId]/page.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Header from '../../../components/custom/header';
|
||||
import Footer from '../../../components/custom/footer';
|
||||
import EventDescription from '../../../components/custom/EventDescription';
|
||||
|
||||
// Dummy function to simulate a GET request
|
||||
const fetchEventDetails = (eventID: number) => {
|
||||
alert(`Fetching details for event ID: ${eventID}`);
|
||||
// Simulated JSON response for the event
|
||||
return {
|
||||
EventID: eventID,
|
||||
name: 'Example Event Name',
|
||||
date: '2023-12-01',
|
||||
location: 'Example Location',
|
||||
ticketPrice: 100,
|
||||
description: 'Detailed description of the event.',
|
||||
capacity: 300,
|
||||
ticketsSold: 150,
|
||||
imageUrl: [
|
||||
'https://via.placeholder.com/150',
|
||||
'https://via.placeholder.com/150',
|
||||
],
|
||||
host: 'Example Host',
|
||||
tickets: [1, 2, 3, 4],
|
||||
};
|
||||
};
|
||||
|
||||
const ListingPage: React.FC = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const eventID = searchParams.get('eventID');
|
||||
|
||||
// Simulate fetching data from backend
|
||||
if (eventID) {
|
||||
const eventDetails = fetchEventDetails(Number(eventID));
|
||||
console.log('Event Details:', eventDetails);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<EventDescription eventId={eventID!} />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListingPage;
|
||||
370
app/events/page.tsx
Normal file
370
app/events/page.tsx
Normal file
@@ -0,0 +1,370 @@
|
||||
'use client';
|
||||
import React, { useEffect, useState, useRef, Suspense } from 'react';
|
||||
import { useRouter } from 'next/navigation'; // Import useRouter for routing
|
||||
import Header from '../../components/custom/header';
|
||||
import Footer from '../../components/custom/footer';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
interface Event {
|
||||
EventID: number;
|
||||
name: string;
|
||||
date: string;
|
||||
location: string;
|
||||
ticketPrice: number;
|
||||
description: string;
|
||||
capacity: number;
|
||||
ticketsSold: number;
|
||||
imageUrl: string;
|
||||
host: string;
|
||||
}
|
||||
|
||||
// Dummy function to fetch events
|
||||
const fetchEvents = (): Event[] => {
|
||||
return [
|
||||
{
|
||||
EventID: 1,
|
||||
name: 'Rock Concert',
|
||||
date: '2023-12-01',
|
||||
location: 'New York City',
|
||||
ticketPrice: 99,
|
||||
description: 'An exhilarating rock concert featuring famous bands.',
|
||||
capacity: 5000,
|
||||
ticketsSold: 4500,
|
||||
imageUrl: 'https://via.placeholder.com/150',
|
||||
host: 'Music Mania',
|
||||
},
|
||||
{
|
||||
EventID: 2,
|
||||
name: 'Art Expo',
|
||||
date: '2023-11-15',
|
||||
location: 'San Francisco',
|
||||
ticketPrice: 55,
|
||||
description: 'A showcase of modern art from around the world.',
|
||||
capacity: 300,
|
||||
ticketsSold: 260,
|
||||
imageUrl: 'https://via.placeholder.com/150',
|
||||
host: 'Art Lovers',
|
||||
},
|
||||
{
|
||||
EventID: 3,
|
||||
name: 'Tech Summit 2023',
|
||||
date: '2023-12-10',
|
||||
location: 'Chicago',
|
||||
ticketPrice: 250,
|
||||
description: 'The leading tech summit with top industry speakers.',
|
||||
capacity: 2000,
|
||||
ticketsSold: 1800,
|
||||
imageUrl: 'https://via.placeholder.com/150',
|
||||
host: 'Tech Alliance',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const EventsPage: React.FC = () => {
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [filteredEvents, setFilteredEvents] = useState<Event[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [hoveredEventId, setHoveredEventId] = useState<number | null>(null);
|
||||
const [sortOption, setSortOption] = useState<string>('');
|
||||
const [filterOptions, setFilterOptions] = useState<string[]>([]);
|
||||
const [selectedHost, setSelectedHost] = useState<string>('');
|
||||
const [showSortMenu, setShowSortMenu] = useState<boolean>(false);
|
||||
const [showFilterMenu, setShowFilterMenu] = useState<boolean>(false);
|
||||
|
||||
const sortRef = useRef<HTMLDivElement>(null);
|
||||
const filterRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const eventsData = fetchEvents();
|
||||
setEvents(eventsData);
|
||||
setFilteredEvents(eventsData);
|
||||
}, []);
|
||||
|
||||
const SearchBox = () => {
|
||||
setSearchQuery(useSearchParams().get('q') || '');
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search events..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-bar mt-4 p-2 border border-gray-300 rounded w-full max-w-md"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let filtered = events.filter((event) =>
|
||||
['name', 'date', 'location', 'description', 'host'].some((key) =>
|
||||
event[key as keyof Event]
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
)
|
||||
);
|
||||
|
||||
if (filterOptions.includes('limited')) {
|
||||
filtered = filtered.filter(
|
||||
(event) => event.ticketsSold / event.capacity >= 0.9
|
||||
);
|
||||
}
|
||||
|
||||
if (filterOptions.includes('future')) {
|
||||
const now = new Date();
|
||||
filtered = filtered.filter((event) => new Date(event.date) > now);
|
||||
}
|
||||
|
||||
if (filterOptions.includes('past')) {
|
||||
const now = new Date();
|
||||
filtered = filtered.filter((event) => new Date(event.date) < now);
|
||||
}
|
||||
|
||||
if (selectedHost) {
|
||||
filtered = filtered.filter((event) => event.host === selectedHost);
|
||||
}
|
||||
|
||||
if (sortOption === 'price-asc') {
|
||||
filtered = filtered.sort((a, b) => a.ticketPrice - b.ticketPrice);
|
||||
} else if (sortOption === 'price-desc') {
|
||||
filtered = filtered.sort((a, b) => b.ticketPrice - a.ticketPrice);
|
||||
} else if (sortOption === 'date-asc') {
|
||||
filtered = filtered.sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
);
|
||||
} else if (sortOption === 'date-desc') {
|
||||
filtered = filtered.sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
setFilteredEvents(filtered);
|
||||
}, [searchQuery, sortOption, filterOptions, selectedHost, events]);
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (sortRef.current && !sortRef.current.contains(event.target as Node)) {
|
||||
setShowSortMenu(false);
|
||||
}
|
||||
if (
|
||||
filterRef.current &&
|
||||
!filterRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowFilterMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleEventClick = (eventID: number) => {
|
||||
router.push(`/events/${eventID}`); // You may replace this with a Link from Next.js
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
<Header />
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
className="absolute inset-0 w-full h-full object-cover z-0"
|
||||
src="BGVid1.mp4"
|
||||
>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className="absolute inset-0 bg-black opacity-50 z-10"></div>
|
||||
|
||||
<div className="relative z-20 container mx-auto p-4 pt-16">
|
||||
<div className="mb-6">
|
||||
<Suspense
|
||||
fallback={
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search events..."
|
||||
disabled={true}
|
||||
value="loading..."
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-bar mt-4 p-2 border border-gray-300 rounded w-full max-w-md"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SearchBox />
|
||||
</Suspense>
|
||||
<div className="flex mt-4 space-x-4">
|
||||
{/* Sort Button and Dropdown */}
|
||||
<div className="relative" ref={sortRef}>
|
||||
<button
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
||||
onClick={() => setShowSortMenu(!showSortMenu)}
|
||||
>
|
||||
Sort
|
||||
</button>
|
||||
{showSortMenu && (
|
||||
<div className="absolute left-0 mt-2 p-2 bg-white shadow-lg rounded border border-gray-300 z-30">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSortOption('price-asc');
|
||||
setShowSortMenu(false);
|
||||
}}
|
||||
>
|
||||
Price Ascending
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSortOption('price-desc');
|
||||
setShowSortMenu(false);
|
||||
}}
|
||||
>
|
||||
Price Descending
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSortOption('date-asc');
|
||||
setShowSortMenu(false);
|
||||
}}
|
||||
>
|
||||
Date Ascending
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSortOption('date-desc');
|
||||
setShowSortMenu(false);
|
||||
}}
|
||||
>
|
||||
Date Descending
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filter Button and Dropdown */}
|
||||
<div className="relative" ref={filterRef}>
|
||||
<button
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
||||
onClick={() => setShowFilterMenu(!showFilterMenu)}
|
||||
>
|
||||
Filters
|
||||
</button>
|
||||
{showFilterMenu && (
|
||||
<div className="absolute left-0 mt-2 p-2 bg-white shadow-lg rounded border border-gray-300 z-30">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filterOptions.includes('limited')}
|
||||
onChange={(e) => {
|
||||
const newFilters = e.target.checked
|
||||
? [...filterOptions, 'limited']
|
||||
: filterOptions.filter(
|
||||
(filter) => filter !== 'limited'
|
||||
);
|
||||
setFilterOptions(newFilters);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">Limited Tickets</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filterOptions.includes('future')}
|
||||
onChange={(e) => {
|
||||
const newFilters = e.target.checked
|
||||
? [...filterOptions, 'future']
|
||||
: filterOptions.filter(
|
||||
(filter) => filter !== 'future'
|
||||
);
|
||||
setFilterOptions(newFilters);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">Future Events</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filterOptions.includes('past')}
|
||||
onChange={(e) => {
|
||||
const newFilters = e.target.checked
|
||||
? [...filterOptions, 'past']
|
||||
: filterOptions.filter((filter) => filter !== 'past');
|
||||
setFilterOptions(newFilters);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">Past Events</span>
|
||||
</label>
|
||||
|
||||
{/* Filter by Host */}
|
||||
<select
|
||||
value={selectedHost}
|
||||
onChange={(e) => setSelectedHost(e.target.value)}
|
||||
className="mt-1 block w-full border-gray-300 rounded"
|
||||
>
|
||||
<option value="">All Hosts</option>
|
||||
{Array.from(new Set(events.map((event) => event.host))).map(
|
||||
(host) => (
|
||||
<option key={host} value={host}>
|
||||
{host}
|
||||
</option>
|
||||
)
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold text-white mb-4">
|
||||
Available Events
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{filteredEvents.map((event) => (
|
||||
<button
|
||||
key={event.EventID}
|
||||
className="relative flex bg-white p-4 rounded-lg shadow-lg text-left"
|
||||
onClick={() => handleEventClick(event.EventID)}
|
||||
onMouseEnter={() => setHoveredEventId(event.EventID)}
|
||||
onMouseLeave={() => setHoveredEventId(null)}
|
||||
>
|
||||
<img
|
||||
src={event.imageUrl}
|
||||
alt={event.name}
|
||||
className="w-1/4 rounded-lg"
|
||||
/>
|
||||
<div className="ml-4 relative">
|
||||
<h3 className="text-xl font-bold">{event.name}</h3>
|
||||
<p className="text-gray-600">{event.date}</p>
|
||||
<p className="text-gray-600">{event.location}</p>
|
||||
<p className="text-gray-800 font-semibold">
|
||||
${event.ticketPrice.toFixed(2)}
|
||||
</p>
|
||||
<p className="text-gray-600">Host: {event.host}</p>
|
||||
{event.ticketsSold / event.capacity >= 0.9 && (
|
||||
<div className="mt-2 p-2 bg-yellow-300 text-black rounded">
|
||||
Limited Tickets Remaining!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 flex items-center space-x-2">
|
||||
{hoveredEventId === event.EventID && (
|
||||
<div className="top-0 left-4 w-full bg-white p-4 shadow-lg rounded-lg z-10">
|
||||
<p>{event.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventsPage;
|
||||
@@ -4,6 +4,7 @@ import './globals.css';
|
||||
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
@@ -30,7 +31,10 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
<body className={inter.className}>
|
||||
<main>{children}</main>
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
125
app/page.tsx
125
app/page.tsx
@@ -1,21 +1,124 @@
|
||||
'use client';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Header from '../components/custom/header';
|
||||
import Footer from '../components/custom/footer';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import FeaturedEvent from '@/components/custom/FeaturedEvent';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import React from 'react';
|
||||
import EventDescription from '@/components/custom/EventDescription';
|
||||
import Home from '../components/Home';
|
||||
import EventForm from '@/components/custom/EventForm';
|
||||
import PreviousTickets from '@/components/PreviousTickets';
|
||||
import { FlipWords } from '@/components/ui/flip-words';
|
||||
|
||||
export default function Page() {
|
||||
// Define the handleSubmit function
|
||||
export default function Home() {
|
||||
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 (
|
||||
<>
|
||||
{/* <Home /> */}
|
||||
{/* <EventForm onSubmit={(data) => handleSubmit(data)} /> */}
|
||||
{/* <PreviousTickets/> */}
|
||||
<Header />
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
{/* Video Background */}
|
||||
{isClient && (
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
className="absolute inset-0 w-full h-full object-cover z-0"
|
||||
src="BGVid2.mp4"
|
||||
>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)}
|
||||
|
||||
{/* Dark Overlay for Enhanced Readability */}
|
||||
<div className="absolute inset-0 bg-black opacity-50 z-10"></div>
|
||||
|
||||
{/* Page Content Over the Video */}
|
||||
<div className="relative z-20 min-h-screen bg-gradient-to-b from-transparent to-gray-900 pt-20">
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="container mx-auto justify-center items-center p-4">
|
||||
<div className="text-4xl font-bold text-white text-shadow-lg">
|
||||
Book your next
|
||||
<FlipWords
|
||||
words={words}
|
||||
className="text-light-purple text-opacity-75"
|
||||
/>
|
||||
on the Flare blockchain.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center mt-6 flex-col gap-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search for your next experience..."
|
||||
className="flex w-full rounded-md border border-input bg-white bg-opacity-50 placeholder:text-black px-4 py-6 text-lg shadow-sm"
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Button
|
||||
className="bg-pink-600 bg-opacity-50 text-white px-4 py-6 text-lg w-full hover:bg-pink-500"
|
||||
onClick={searchForEvents}
|
||||
>
|
||||
Search for events
|
||||
</Button>
|
||||
</div>
|
||||
<main>
|
||||
<section className="mb-8 mt-4 mx-auto grid grid-cols-4 gap-4">
|
||||
<FeaturedEvent
|
||||
name="FAB XO Halloween"
|
||||
description="Halloween is upon us and is one of the biggest nights of the FAB XO calendar. Fancy dress is encouraged! So have your fancy dress ready and we look forward to seeing who have the best fancy dress on the night! As a special treat we will be serving our very own witches brew!!!"
|
||||
location="Birmingham, UK"
|
||||
eventStartDate={1729980000}
|
||||
eventHost="0x225C73C8c536C4F5335a2C1abECa95b0f221eeF6"
|
||||
imageURL={
|
||||
'https://www.guildofstudents.com/asset/Event/7572/Halloween-Fab-XO-Web-Event.jpg'
|
||||
}
|
||||
/>
|
||||
<FeaturedEvent
|
||||
name="Halls Halloween Spooktacular"
|
||||
description="Put on your spookiest costume and head on down to Pritchatts Park and join your Event Activators for our ResLife SPOOKTACULAR on Wednesday 30th October 5-8pm."
|
||||
location="Birmingham, UK"
|
||||
eventStartDate={1730307600}
|
||||
eventHost="0x225C73C8c536C4F5335a2C1abECa95b0f221eeF6"
|
||||
imageURL={
|
||||
'https://www.guildofstudents.com/asset/Event/41187/Spooktacular-Web-Event-2024.png'
|
||||
}
|
||||
/>
|
||||
<FeaturedEvent
|
||||
name="Housing Fair"
|
||||
description="We’re hosting a Housing Fair, so make sure you save the date! Whether you’re living in student accommodation or the local community, this will be a great place to start as you begin thinking about where you’ll be living next year."
|
||||
location="Birmingham, UK"
|
||||
eventStartDate={1730804400}
|
||||
eventHost="0x225C73C8c536C4F5335a2C1abECa95b0f221eeF6"
|
||||
imageURL={
|
||||
'https://www.guildofstudents.com/asset/Event/41111/Housing-Fair-Web-Event.png'
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import Header from '../components/custom/header';
|
||||
import Footer from '../components/custom/footer';
|
||||
import Test from '../components/scripts/Test';
|
||||
import MetaMask from '../components/scripts/MetaMask';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div>
|
||||
<Header />
|
||||
{/* Other page content */}
|
||||
</div>
|
||||
<main>
|
||||
<section className="mb-8">
|
||||
<h2 className="text-2xl font-semibold mb-4">Featured Events</h2>
|
||||
<p className="text-gray-600">No events available at the moment.</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="text-2xl font-semibold mb-4">Upcoming Events</h2>
|
||||
<ul className="list-disc list-inside">
|
||||
<li>Event 1 - Date</li>
|
||||
<li>Event 2 - Date</li>
|
||||
<li>Event 3 - Date</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section className="mb-8">
|
||||
<Test />
|
||||
</section>
|
||||
<section className="mb-8">
|
||||
<MetaMask />
|
||||
</section>
|
||||
<Footer />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,20 +4,12 @@ import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
// Dark theme and animation setup
|
||||
|
||||
7
components/WalletAdapter.tsx
Normal file
7
components/WalletAdapter.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const WalletAdapter = () => {
|
||||
return <div>WalletAdapter</div>;
|
||||
};
|
||||
|
||||
export default WalletAdapter;
|
||||
@@ -1,4 +1,3 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
@@ -10,10 +9,16 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import ImageCarousel from './ImageCarousel';
|
||||
import BuyTicket from './BuyTicket';
|
||||
import TicketButton from './TicketButton';
|
||||
import { buyHandler } from '@/lib/buyHandler';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
const EventDescription = ({ eventId }: { eventId: string }) => {
|
||||
const { toast } = useToast();
|
||||
const handleBuyNow = () => {
|
||||
buyHandler(Number(eventId), toast);
|
||||
};
|
||||
|
||||
const EventDescription = () => {
|
||||
return (
|
||||
<Card className="pt-10 pb-16 px-6 bg-gradient-to-r from-blue-50 to-gray-50 rounded-xl shadow-lg max-w-4xl mx-auto">
|
||||
<CardHeader className="flex flex-col items-start space-y-4">
|
||||
@@ -48,6 +53,7 @@ const EventDescription = () => {
|
||||
<Button
|
||||
variant="default"
|
||||
className="w-full md:w-auto bg-gradient-to-r from-blue-500 to-purple-600 text-white font-semibold rounded-lg hover:from-blue-600 hover:to-purple-700"
|
||||
onClick={handleBuyNow}
|
||||
>
|
||||
Buy now Using MetaMask
|
||||
</Button>
|
||||
|
||||
51
components/custom/FeaturedEvent.tsx
Normal file
51
components/custom/FeaturedEvent.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
} from '@/components/ui/card';
|
||||
|
||||
interface props {
|
||||
name: string;
|
||||
description: string;
|
||||
location: string;
|
||||
eventStartDate: number;
|
||||
eventHost: string;
|
||||
imageURL: string | null;
|
||||
}
|
||||
|
||||
const FeaturedEvent = ({
|
||||
name,
|
||||
description,
|
||||
location,
|
||||
eventStartDate,
|
||||
eventHost,
|
||||
imageURL,
|
||||
}: props) => {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{imageURL && <img src={imageURL} alt={name}></img>}
|
||||
<CardTitle>{name}</CardTitle>
|
||||
<CardDescription>
|
||||
{location}
|
||||
<br />
|
||||
{new Date(eventStartDate * 1000).toLocaleString()}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>{description}</CardContent>
|
||||
<CardFooter>
|
||||
<i>
|
||||
Host: {eventHost.substring(0, 8)}...
|
||||
{eventHost.substring(eventHost.length - 3)}
|
||||
</i>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturedEvent;
|
||||
@@ -3,8 +3,8 @@ import React from 'react';
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer className="text-center mt-8">
|
||||
<p className="text-gray-500">
|
||||
© 2024 Ticket Chain. All rights reserved.
|
||||
<p className="text-light-purple text-opacity-75">
|
||||
© 2024 TicketChain. All rights reserved.
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
|
||||
@@ -4,48 +4,70 @@ import Link from 'next/link';
|
||||
import MetaMask from '../scripts/MetaMask';
|
||||
|
||||
const Header = () => {
|
||||
const [mouseX, setMouseX] = useState(0);
|
||||
const [mouseY, setMouseY] = useState(0);
|
||||
const [mouseX, setMouseX] = useState(-1);
|
||||
const [mouseY, setMouseY] = useState(-1);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
setMouseX(e.clientX);
|
||||
setMouseY(e.clientY);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setMouseX(-1);
|
||||
setMouseY(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 backdrop-blur-md bg-opacity-60 z-50"
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{
|
||||
border: '1px solid transparent',
|
||||
background: `radial-gradient(circle at ${mouseX}px ${mouseY}px, rgba(255, 255, 255, 0.4), transparent 20%)`,
|
||||
background:
|
||||
mouseX >= 0 && mouseY >= 0
|
||||
? `radial-gradient(circle at ${mouseX}px ${mouseY}px, rgba(255, 255, 255, 0.4), transparent 20%)`
|
||||
: 'none',
|
||||
backgroundClip: 'padding-box, border-box',
|
||||
}}
|
||||
></div>
|
||||
<div className="container mx-auto px-6 py-5 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold text-white">Ticket Chain</h1>
|
||||
<div className="container mx-auto px-6 py-4 flex justify-between items-center">
|
||||
<Link href="/" legacyBehavior>
|
||||
<a className="text-2xl font-semibold text-white hover:text-light-purple hover:text-opacity-75 transition-colors duration-300">
|
||||
TicketChain
|
||||
</a>
|
||||
</Link>
|
||||
<nav className="nav">
|
||||
<ul className="flex space-x-6">
|
||||
<li>
|
||||
<Link href="/" legacyBehavior>
|
||||
<a className="text-white hover:text-blue-500 transition-colors duration-300">
|
||||
<a
|
||||
className="text-white hover:text-light-purple hover:text-opacity-75 transition-colors duration-300"
|
||||
style={{ textShadow: '1px 1px 2px rgba(0, 0, 0, 0.5)' }}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/events" legacyBehavior>
|
||||
<a className="text-white hover:text-blue-500 transition-colors duration-300">
|
||||
<a
|
||||
className="text-white hover:text-light-purple hover:text-opacity-75 transition-colors duration-300"
|
||||
style={{ textShadow: '1px 1px 2px rgba(0, 0, 0, 0.5)' }}
|
||||
>
|
||||
Events
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/contact" legacyBehavior>
|
||||
<a className="text-white hover:text-blue-500 transition-colors duration-300">
|
||||
<a
|
||||
className="text-white hover:text-light-purple hover:text-opacity-75 transition-colors duration-300"
|
||||
style={{ textShadow: '1px 1px 2px rgba(0, 0, 0, 0.5)' }}
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
@@ -1,76 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ethers } from 'ethers';
|
||||
import { useSDK, MetaMaskProvider } from '@metamask/sdk-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ethereum: ethers.providers.ExternalProvider;
|
||||
}
|
||||
function WalletIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4" />
|
||||
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5" />
|
||||
<path d="M18 12a2 2 0 0 0 0 4h4v-4Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const MetaMask = () => {
|
||||
const [metaMaskInstalled, setMetaMaskInstalled] = useState(false);
|
||||
const [account, setAccount] = useState<string | null>(null);
|
||||
function formatAddress(address: string | undefined): string {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
const isMetaMaskInstalled = () =>
|
||||
typeof window !== 'undefined' &&
|
||||
typeof (window as { ethereum?: unknown }).ethereum !== 'undefined';
|
||||
function MetaMaskConnect() {
|
||||
const { sdk, connected, connecting, account } = useSDK();
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMetaMaskInstalled()) {
|
||||
setMetaMaskInstalled(true);
|
||||
}
|
||||
}, []);
|
||||
setIsConnected(connected);
|
||||
}, [connected]);
|
||||
|
||||
const handleConnectWallet = async () => {
|
||||
if (window.ethereum && window.ethereum.request) {
|
||||
try {
|
||||
// Request account access
|
||||
const accounts = await window.ethereum.request({
|
||||
method: 'eth_requestAccounts',
|
||||
});
|
||||
setAccount(accounts[0]); // Set the first account
|
||||
} catch (error) {
|
||||
console.error('Error connecting to MetaMask:', error);
|
||||
}
|
||||
} else {
|
||||
alert(
|
||||
'MetaMask is not installed. Please install MetaMask and try again.'
|
||||
);
|
||||
const connect = async () => {
|
||||
try {
|
||||
await sdk?.connect();
|
||||
setIsConnected(true);
|
||||
} catch (err) {
|
||||
console.warn(`No accounts found`, err);
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = () => {
|
||||
if (sdk) {
|
||||
sdk.terminate();
|
||||
setIsConnected(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
{metaMaskInstalled ? (
|
||||
<div>
|
||||
{account ? (
|
||||
<p className="text-green-500">
|
||||
Connected: 0x{account.slice(2, 5)}...{account.slice(-3)}
|
||||
</p>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleConnectWallet}
|
||||
className="bg-gradient-to-r from-blue-500 to-indigo-700 text-white px-4 py-1 rounded-full transform transition-transform duration-300 hover:scale-105 shadow-lg hover:shadow-2xl"
|
||||
<div className="relative">
|
||||
{isConnected ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="link" className="text-white">
|
||||
{formatAddress(account)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-44">
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={disconnect}
|
||||
className="w-full px-4 py-2 text-left hover:bg-muted hover:text-destructive"
|
||||
>
|
||||
Connect Wallet
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
Disconnect
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<button
|
||||
// Install Metamask extension if not already installed
|
||||
onClick={() =>
|
||||
window.open('https://metamask.io/download.html', '_blank')
|
||||
}
|
||||
className="bg-gradient-to-r from-blue-500 to-indigo-700 text-white px-4 py-1 rounded-full transform transition-transform duration-300 hover:scale-105 shadow-lg hover:shadow-2xl"
|
||||
<Button
|
||||
disabled={connecting}
|
||||
onClick={connect}
|
||||
className="bg-light-purple bg-opacity-75 hover:bg-purple border-0 hover:border-0"
|
||||
>
|
||||
Install MetaMask to connect wallet
|
||||
</button>
|
||||
<WalletIcon className="mr-2 h-4 w-4" /> Connect Wallet
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default MetaMask;
|
||||
export default function MetaMaskConnectWrapper() {
|
||||
return (
|
||||
<MetaMaskProvider
|
||||
debug={false}
|
||||
sdkOptions={{
|
||||
dappMetadata: {
|
||||
name: 'My dApp',
|
||||
url: typeof window !== 'undefined' ? window.location.href : '',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MetaMaskConnect />
|
||||
</MetaMaskProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
'use client';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
const Test = () => {
|
||||
useEffect(() => {
|
||||
console.log('Print some shit');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Hellao!</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Test;
|
||||
98
components/ui/flip-words.tsx
Normal file
98
components/ui/flip-words.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const FlipWords = ({
|
||||
words,
|
||||
duration = 3000,
|
||||
className,
|
||||
}: {
|
||||
words: string[];
|
||||
duration?: number;
|
||||
className?: string;
|
||||
}) => {
|
||||
const [currentWord, setCurrentWord] = useState(words[0]);
|
||||
const [isAnimating, setIsAnimating] = useState<boolean>(false);
|
||||
|
||||
// thanks for the fix Julian - https://github.com/Julian-AT
|
||||
const startAnimation = useCallback(() => {
|
||||
const word = words[words.indexOf(currentWord) + 1] || words[0];
|
||||
setCurrentWord(word);
|
||||
setIsAnimating(true);
|
||||
}, [currentWord, words]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAnimating)
|
||||
setTimeout(() => {
|
||||
startAnimation();
|
||||
}, duration);
|
||||
}, [isAnimating, duration, startAnimation]);
|
||||
|
||||
return (
|
||||
<AnimatePresence
|
||||
onExitComplete={() => {
|
||||
setIsAnimating(false);
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
}}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 100,
|
||||
damping: 10,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: -40,
|
||||
x: 40,
|
||||
filter: 'blur(8px)',
|
||||
scale: 2,
|
||||
position: 'absolute',
|
||||
}}
|
||||
className={cn(
|
||||
'z-10 inline-block relative text-left text-neutral-900 dark:text-neutral-100 px-2',
|
||||
className
|
||||
)}
|
||||
key={currentWord}
|
||||
>
|
||||
{/* edit suggested by Sajal: https://x.com/DewanganSajal */}
|
||||
{currentWord.split(' ').map((word, wordIndex) => (
|
||||
<motion.span
|
||||
key={word + wordIndex}
|
||||
initial={{ opacity: 0, y: 10, filter: 'blur(8px)' }}
|
||||
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
|
||||
transition={{
|
||||
delay: wordIndex * 0.3,
|
||||
duration: 0.3,
|
||||
}}
|
||||
className="inline-block whitespace-nowrap"
|
||||
>
|
||||
{word.split('').map((letter, letterIndex) => (
|
||||
<motion.span
|
||||
key={word + letterIndex}
|
||||
initial={{ opacity: 0, y: 10, filter: 'blur(8px)' }}
|
||||
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
|
||||
transition={{
|
||||
delay: wordIndex * 0.3 + letterIndex * 0.05,
|
||||
duration: 0.2,
|
||||
}}
|
||||
className="inline-block"
|
||||
>
|
||||
{letter}
|
||||
</motion.span>
|
||||
))}
|
||||
<span className="inline-block"> </span>
|
||||
</motion.span>
|
||||
))}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
const Input = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
React.InputHTMLAttributes<HTMLInputElement>
|
||||
>(({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
|
||||
33
components/ui/popover.tsx
Normal file
33
components/ui/popover.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
129
components/ui/toast.tsx
Normal file
129
components/ui/toast.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Cross2Icon } from '@radix-ui/react-icons';
|
||||
import * as ToastPrimitives from '@radix-ui/react-toast';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border bg-background text-foreground',
|
||||
destructive:
|
||||
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm opacity-90', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
};
|
||||
35
components/ui/toaster.tsx
Normal file
35
components/ui/toaster.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from '@/components/ui/toast';
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast();
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
);
|
||||
}
|
||||
197
contracts/EventManager.sol
Normal file
197
contracts/EventManager.sol
Normal file
@@ -0,0 +1,197 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
pragma solidity >=0.8.2 <0.9.0;
|
||||
|
||||
import {ContractRegistry} from "@flarenetwork/flare-periphery-contracts/coston2/ContractRegistry.sol";
|
||||
/* THIS IS A TEST IMPORT, in production use: import {FtsoV2Interface} from "@flarenetwork/flare-periphery-contracts/coston2/FtsoV2Interface.sol"; */
|
||||
import {TestFtsoV2Interface} from "@flarenetwork/flare-periphery-contracts/coston2/TestFtsoV2Interface.sol";
|
||||
|
||||
contract EventManager {
|
||||
|
||||
TestFtsoV2Interface internal ftsoV2;
|
||||
bytes21[] public feedIds = [
|
||||
bytes21(0x01464c522f55534400000000000000000000000000) // FLR/USD
|
||||
// bytes21(0x014254432f55534400000000000000000000000000), // BTC/USD
|
||||
// bytes21(0x014554482f55534400000000000000000000000000) // ETH/USD
|
||||
];
|
||||
|
||||
constructor() {
|
||||
/* THIS IS A TEST METHOD, in production use: ftsoV2 = ContractRegistry.getFtsoV2(); */
|
||||
ftsoV2 = ContractRegistry.getTestFtsoV2();
|
||||
}
|
||||
|
||||
struct Event {
|
||||
string name;
|
||||
string description;
|
||||
string location;
|
||||
uint256 capacity;
|
||||
uint256 ticketsSold;
|
||||
uint256 ticketPrice; // in USD cents
|
||||
uint256 eventStartDate;
|
||||
uint256 eventEndDate;
|
||||
string[] images; // array of image URLs
|
||||
uint256[] tickets;
|
||||
address payable eventHost;
|
||||
}
|
||||
|
||||
struct Ticket {
|
||||
address holder;
|
||||
uint256 boughtTime;
|
||||
uint256 eventId;
|
||||
}
|
||||
|
||||
event EventCreated(uint256 eventId, string name, uint256 eventStartDate);
|
||||
event TicketPurchased(uint256 ticketId, uint256 eventId, address buyer, uint256 price);
|
||||
event TicketTransferred(uint256 ticketId, address from, address to);
|
||||
event TicketTransferApproved(uint256 ticketId, address owner, address trustee);
|
||||
|
||||
mapping(uint256 => Event) public events;
|
||||
mapping(uint256 => Ticket) public tickets;
|
||||
|
||||
mapping(uint256 => mapping(address => bool)) ticketAllowance;
|
||||
|
||||
mapping(address => uint256[]) public userTickets;
|
||||
|
||||
uint256 public eventCounter;
|
||||
uint256 public ticketCounter;
|
||||
|
||||
function getFtsoV2CurrentFeedValues() public view returns (
|
||||
uint256[] memory _feedValues,
|
||||
int8[] memory _decimals,
|
||||
uint64 _timestamp
|
||||
) {
|
||||
return ftsoV2.getFeedsById(feedIds);
|
||||
}
|
||||
|
||||
function getFlareFeed() public view returns (uint256 _feedValue, int8 _decimals, uint64 _timestamp) {
|
||||
uint256[] memory feedValues;
|
||||
int8[] memory decimals;
|
||||
uint64 timestamp;
|
||||
(feedValues, decimals, timestamp) = ftsoV2.getFeedsById(feedIds);
|
||||
return (feedValues[0], decimals[0], timestamp);
|
||||
}
|
||||
|
||||
function centsToFlare(uint256 _cents) public view returns (uint256 _flr) {
|
||||
uint256 feedValue;
|
||||
int8 decimals;
|
||||
(feedValue, decimals, ) = getFlareFeed();
|
||||
return _cents * power(10, decimals) * 1 ether / 100 / feedValue;
|
||||
}
|
||||
|
||||
function power(uint base, int8 exponent) private pure returns (uint) {
|
||||
require(exponent >= 0, "Exponent must be non-negative");
|
||||
uint result = 1;
|
||||
for (int8 i = 0; i < exponent; i++) {
|
||||
result *= base;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getEventPriceFlare(uint256 _eventId) public view returns (uint256 _flr) {
|
||||
require(_eventId < eventCounter, "Invalid event ID");
|
||||
return centsToFlare(events[_eventId].ticketPrice);
|
||||
}
|
||||
|
||||
function createEvent(string memory _name, string memory _description, string memory _location, uint256 _capacity, uint256 _ticketPrice, uint256 _eventStartDate, uint256 _eventEndDate, string[] memory _images) public returns (uint256 _eventId) {
|
||||
events[eventCounter] = Event(_name, _description, _location, _capacity, 0, _ticketPrice, _eventStartDate, _eventEndDate, _images, new uint256[](0), payable(msg.sender));
|
||||
eventCounter++;
|
||||
emit EventCreated(eventCounter - 1, _name, _eventStartDate);
|
||||
return eventCounter - 1;
|
||||
}
|
||||
|
||||
function getEventImages(uint256 _eventId) public view returns (string[] memory) {
|
||||
require(_eventId < eventCounter, "Invalid event ID");
|
||||
return events[_eventId].images;
|
||||
}
|
||||
|
||||
function getEventTickets(uint256 _eventId) public view returns (uint256[] memory) {
|
||||
require(_eventId < eventCounter, "Invalid event ID");
|
||||
return events[_eventId].tickets;
|
||||
}
|
||||
|
||||
function buyTicket(uint256 _eventId) public payable returns (uint256 _ticketId) {
|
||||
require(_eventId < eventCounter, "Invalid event ID");
|
||||
require(events[_eventId].eventStartDate > block.timestamp, "Event has already passed");
|
||||
require(events[_eventId].tickets.length < events[_eventId].capacity, "Event is full");
|
||||
|
||||
uint256 ticketCost = getEventPriceFlare(_eventId); // Get ticket price in FLR
|
||||
require(msg.value >= ticketCost, "Insufficient value provided"); // Ensure user has paid >= ticket price
|
||||
if (msg.value > ticketCost) {
|
||||
// Pay any excess the user paid
|
||||
(bool sentExcess, ) = msg.sender.call{value: msg.value - ticketCost}("");
|
||||
require(sentExcess, "Failed to send FLR excess back to buyer");
|
||||
}
|
||||
|
||||
// Create new ticket
|
||||
tickets[ticketCounter] = Ticket(msg.sender, block.timestamp, _eventId);
|
||||
|
||||
// Add ticket to user
|
||||
userTickets[msg.sender].push(ticketCounter);
|
||||
|
||||
ticketCounter++;
|
||||
|
||||
// Update number of tickets sold
|
||||
events[_eventId].tickets.push(ticketCounter - 1);
|
||||
events[_eventId].ticketsSold++;
|
||||
|
||||
// Transfer FLR to event host
|
||||
(bool sentToHost, ) = events[_eventId].eventHost.call{value: ticketCost}("");
|
||||
require(sentToHost, "Failed to send FLR to event host");
|
||||
|
||||
emit TicketPurchased(ticketCounter - 1, _eventId, msg.sender, ticketCost);
|
||||
return ticketCounter - 1;
|
||||
}
|
||||
|
||||
function transferTicketForce(uint256 _ticketId, address _to) private {
|
||||
require(_ticketId < ticketCounter, "Invalid ticket ID");
|
||||
require(events[tickets[_ticketId].eventId].eventStartDate > block.timestamp, "Event has already passed");
|
||||
|
||||
address prevHolder = tickets[_ticketId].holder;
|
||||
|
||||
// Get index of ticket in holder's array
|
||||
bool found = false;
|
||||
uint256 i = 0;
|
||||
for (; i < userTickets[prevHolder].length; i++) {
|
||||
if (userTickets[prevHolder][i] == _ticketId) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
require(found, "Ticket not found in sender's inventory");
|
||||
|
||||
// Remove ticket from holder's array
|
||||
for (; i < userTickets[prevHolder].length-1; i++) {
|
||||
userTickets[prevHolder][i] = userTickets[prevHolder][i+1];
|
||||
}
|
||||
userTickets[prevHolder].pop();
|
||||
|
||||
// Add ticket to _to's array
|
||||
userTickets[_to].push(_ticketId);
|
||||
|
||||
tickets[_ticketId].holder = _to;
|
||||
|
||||
emit TicketTransferred(_ticketId, prevHolder, _to);
|
||||
}
|
||||
|
||||
function approveTicket(uint256 _ticketId, address _to, bool _allowed) public {
|
||||
require(_ticketId < ticketCounter, "Invalid ticket ID");
|
||||
require(tickets[_ticketId].holder == msg.sender, "You do not own this ticket");
|
||||
ticketAllowance[_ticketId][_to] = _allowed;
|
||||
|
||||
emit TicketTransferApproved(_ticketId, msg.sender, _to);
|
||||
}
|
||||
|
||||
function transferTicketFrom(uint256 _ticketId, address _to) public {
|
||||
require(ticketAllowance[_ticketId][msg.sender], "You are not allowed to transfer this ticket");
|
||||
ticketAllowance[_ticketId][msg.sender] = false;
|
||||
transferTicketForce(_ticketId, _to);
|
||||
}
|
||||
|
||||
function transferTicket(uint256 _ticketId, address _to) public {
|
||||
require(_ticketId < ticketCounter, "Invalid ticket ID");
|
||||
require(tickets[_ticketId].holder == msg.sender, "You do not own this ticket");
|
||||
transferTicketForce(_ticketId, _to);
|
||||
}
|
||||
|
||||
}
|
||||
14
hardhat.config.ts
Normal file
14
hardhat.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { HardhatUserConfig } from 'hardhat/config';
|
||||
import '@nomiclabs/hardhat-waffle';
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
|
||||
const config: HardhatUserConfig = {
|
||||
solidity: '0.8.17',
|
||||
networks: {
|
||||
hardhat: {
|
||||
chainId: 1337,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
191
hooks/use-toast.ts
Normal file
191
hooks/use-toast.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
'use client';
|
||||
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
|
||||
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
UPDATE_TOAST: 'UPDATE_TOAST',
|
||||
DISMISS_TOAST: 'DISMISS_TOAST',
|
||||
REMOVE_TOAST: 'REMOVE_TOAST',
|
||||
} as const;
|
||||
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType['ADD_TOAST'];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType['UPDATE_TOAST'];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType['DISMISS_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
}
|
||||
| {
|
||||
type: ActionType['REMOVE_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: 'REMOVE_TOAST',
|
||||
toastId: toastId,
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'ADD_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
};
|
||||
|
||||
case 'UPDATE_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
};
|
||||
|
||||
case 'DISMISS_TOAST': {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
};
|
||||
}
|
||||
case 'REMOVE_TOAST':
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, 'id'>;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: 'UPDATE_TOAST',
|
||||
toast: { ...props, id },
|
||||
});
|
||||
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: 'ADD_TOAST',
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast };
|
||||
66
lib/buyHandler.ts
Normal file
66
lib/buyHandler.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ethers } from 'ethers';
|
||||
import { getContract } from '@/lib/ethers';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ethereumProvider?: ethers.providers.ExternalProvider & {
|
||||
isMetaMask?: boolean;
|
||||
request?: (method: string, params?: unknown[]) => Promise<unknown>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type ToastFunction = (options: {
|
||||
title: string;
|
||||
variant?: 'default' | 'destructive' | null | undefined;
|
||||
}) => void;
|
||||
|
||||
export const buyHandler = async (
|
||||
eventId: number,
|
||||
toast: ToastFunction
|
||||
): Promise<void> => {
|
||||
if (eventId < 0) {
|
||||
toast({ title: 'Please enter a valid Event ID.', variant: 'destructive' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof window.ethereum === 'undefined') {
|
||||
toast({
|
||||
title: 'Please install MetaMask or another Ethereum wallet',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error: window.ethereum might not match ExternalProvider exactly
|
||||
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();
|
||||
|
||||
toast({
|
||||
title: `Ticket purchased successfully! Transaction Hash: ${receipt.transactionHash}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error buying ticket:', error);
|
||||
toast({
|
||||
title: 'Transaction failed. Please try again.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
520
lib/ethers.ts
Normal file
520
lib/ethers.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
const FLARE_TESTNET_RPC_URL = process.env.NEXT_PUBLIC_RPC_URL;
|
||||
const CONTRACT_ADDRESS = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS;
|
||||
|
||||
export function getFlareProvider() {
|
||||
const flareRpcUrl = FLARE_TESTNET_RPC_URL;
|
||||
const provider = new ethers.providers.JsonRpcProvider(flareRpcUrl);
|
||||
return provider;
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
return new ethers.Contract(contractAddress!, contractABI, provider);
|
||||
}
|
||||
16849
package-lock.json
generated
16849
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -7,15 +7,21 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "npx hardhat --tsconfig tsconfig.hardhat.json test",
|
||||
"compile": "hardhat compile",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@flarenetwork/flare-periphery-contracts": "^0.1.16",
|
||||
"@metamask/sdk-react": "^0.30.0",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-popover": "^1.1.2",
|
||||
"@radix-ui/react-select": "^2.1.2",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
@@ -26,22 +32,34 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.23.8"
|
||||
"zod": "^3.23.8",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flarenetwork/flare-periphery-contracts": "^0.1.16",
|
||||
"@nomicfoundation/hardhat-toolbox": "^2.0.2",
|
||||
"@nomiclabs/hardhat-ethers": "^2.2.3",
|
||||
"@nomiclabs/hardhat-waffle": "^2.0.0",
|
||||
"@typechain/hardhat": "^6.1.6",
|
||||
"@types/ethereum-protocol": "^1.0.5",
|
||||
"@types/mocha": "^10.0.9",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/tailwindcss": "^3.0.11",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.13",
|
||||
"ethereum-waffle": "^4.0.10",
|
||||
"ethers": "^5.7.2",
|
||||
"hardhat": "^2.22.15",
|
||||
"husky": "^9.1.6",
|
||||
"lint-staged": "^15.2.10",
|
||||
"postcss": "^8",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
BIN
public/BGVid1.mp4
Normal file
BIN
public/BGVid1.mp4
Normal file
Binary file not shown.
BIN
public/BGVid2.mp4
Normal file
BIN
public/BGVid2.mp4
Normal file
Binary file not shown.
1
remappings.txt
Normal file
1
remappings.txt
Normal file
@@ -0,0 +1 @@
|
||||
@flarenetwork/flare-periphery-contracts/=node_modules/@flarenetwork/flare-periphery-contracts/
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
import tailwindcssAnimate from 'tailwindcss-animate';
|
||||
|
||||
type ColorValue = string | ColorDictionary;
|
||||
interface ColorDictionary {
|
||||
[colorName: string]: ColorValue;
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ['class'],
|
||||
@@ -14,6 +20,12 @@ const config: Config = {
|
||||
DEFAULT: '#000',
|
||||
100: '#000319',
|
||||
},
|
||||
'darkest-purple': '#240046',
|
||||
'darker-purple': '#3C096C',
|
||||
'dark-purple': '#5A189A',
|
||||
purple: '#7B2CBF',
|
||||
'light-purple': '#9D4EDD',
|
||||
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
@@ -70,6 +82,44 @@ const config: Config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
plugins: [tailwindcssAnimate, addVariablesForColors],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
// Plugin to add each Tailwind color as a CSS variable
|
||||
function addVariablesForColors({
|
||||
addBase,
|
||||
theme,
|
||||
}: {
|
||||
addBase: (styles: Record<string, Record<string, string>>) => void;
|
||||
theme: (path: string) => unknown;
|
||||
}) {
|
||||
const colors = theme('colors') as ColorDictionary;
|
||||
const flattenedColors = flattenColors(colors);
|
||||
const newVars = Object.fromEntries(
|
||||
Object.entries(flattenedColors).map(([key, val]) => [`--${key}`, val])
|
||||
);
|
||||
|
||||
addBase({
|
||||
':root': newVars,
|
||||
});
|
||||
}
|
||||
|
||||
// Recursive function to flatten nested color objects
|
||||
function flattenColors(
|
||||
colors: ColorDictionary,
|
||||
prefix = ''
|
||||
): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
if (typeof value === 'string') {
|
||||
// If the value is a string, add it to the result
|
||||
result[prefix + key] = value;
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// If the value is an object, recursively flatten it
|
||||
Object.assign(result, flattenColors(value, `${prefix}${key}-`));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
149
test/EventManager.test.ts
Normal file
149
test/EventManager.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { expect } from 'chai';
|
||||
import { ethers } from 'hardhat';
|
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
|
||||
import { EventManager } from '../typechain-types/EventManager';
|
||||
|
||||
describe('EventManager', function () {
|
||||
let eventManager: EventManager;
|
||||
let owner: SignerWithAddress;
|
||||
let addr1: SignerWithAddress;
|
||||
let addr2: SignerWithAddress;
|
||||
|
||||
const EVENT_NAME = 'Test Event';
|
||||
const EVENT_DESCRIPTION = 'This is a test event';
|
||||
const EVENT_LOCATION = 'London, UK';
|
||||
const EVENT_CAPACITY = 100;
|
||||
const EVENT_TICKET_PRICE = 1000; // 10 USD in cents
|
||||
const EVENT_START_DATE = Math.floor(Date.now() / 1000) + 86400; // 1 day from now
|
||||
const EVENT_END_DATE = Math.floor(Date.now() / 1000) + 172800; // 2 days from now
|
||||
const EVENT_IMAGES = ['image1.jpg', 'image2.jpg'];
|
||||
|
||||
beforeEach(async function () {
|
||||
[owner, addr1, addr2] = await ethers.getSigners();
|
||||
|
||||
const EventManager = await ethers.getContractFactory('EventManager');
|
||||
eventManager = await EventManager.deploy();
|
||||
await eventManager.deployed();
|
||||
});
|
||||
|
||||
async function createTestEvent() {
|
||||
await eventManager.createEvent(
|
||||
EVENT_NAME,
|
||||
EVENT_DESCRIPTION,
|
||||
EVENT_LOCATION,
|
||||
EVENT_CAPACITY,
|
||||
EVENT_TICKET_PRICE,
|
||||
EVENT_START_DATE,
|
||||
EVENT_END_DATE,
|
||||
EVENT_IMAGES
|
||||
);
|
||||
}
|
||||
|
||||
describe('Event Creation', function () {
|
||||
it('Should create an event with correct details', async function () {
|
||||
await createTestEvent();
|
||||
|
||||
const event = await eventManager.events(0);
|
||||
expect(event.name).to.equal(EVENT_NAME);
|
||||
expect(event.description).to.equal(EVENT_DESCRIPTION);
|
||||
expect(event.location).to.equal(EVENT_LOCATION);
|
||||
expect(event.capacity).to.equal(EVENT_CAPACITY);
|
||||
expect(event.ticketPrice).to.equal(EVENT_TICKET_PRICE);
|
||||
expect(event.eventStartDate).to.equal(EVENT_START_DATE);
|
||||
expect(event.eventEndDate).to.equal(EVENT_END_DATE);
|
||||
expect(event.eventHost).to.equal(owner.address);
|
||||
});
|
||||
|
||||
// it("Should emit EventCreated event", async function () {
|
||||
// await expect(await createTestEvent())
|
||||
// .to.emit(eventManager, "EventCreated")
|
||||
// .withArgs(0, EVENT_NAME, EVENT_DATE);
|
||||
// });
|
||||
});
|
||||
|
||||
describe('Ticket Purchase', function () {
|
||||
beforeEach(async function () {
|
||||
await createTestEvent();
|
||||
});
|
||||
|
||||
it('Should allow buying a ticket', async function () {
|
||||
const ticketPriceFlare = await eventManager.getEventPriceFlare(0);
|
||||
await expect(
|
||||
eventManager.connect(addr1).buyTicket(0, { value: ticketPriceFlare })
|
||||
)
|
||||
.to.emit(eventManager, 'TicketPurchased')
|
||||
.withArgs(0, 0, addr1.address, ticketPriceFlare);
|
||||
|
||||
const ticket = await eventManager.tickets(0);
|
||||
expect(ticket.holder).to.equal(addr1.address);
|
||||
expect(ticket.eventId).to.equal(0);
|
||||
});
|
||||
|
||||
it('Should fail if insufficient funds are provided', async function () {
|
||||
const ticketPriceFlare = await eventManager.getEventPriceFlare(0);
|
||||
await expect(
|
||||
eventManager
|
||||
.connect(addr1)
|
||||
.buyTicket(0, { value: ticketPriceFlare.sub(1) })
|
||||
).to.be.revertedWith('Insufficient value provided');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ticket Transfer', function () {
|
||||
beforeEach(async function () {
|
||||
await createTestEvent();
|
||||
const ticketPriceFlare = await eventManager.getEventPriceFlare(0);
|
||||
await eventManager
|
||||
.connect(addr1)
|
||||
.buyTicket(0, { value: ticketPriceFlare });
|
||||
});
|
||||
|
||||
it('Should allow transferring a ticket', async function () {
|
||||
await expect(eventManager.connect(addr1).transferTicket(0, addr2.address))
|
||||
.to.emit(eventManager, 'TicketTransferred')
|
||||
.withArgs(0, addr1.address, addr2.address);
|
||||
|
||||
const ticket = await eventManager.tickets(0);
|
||||
expect(ticket.holder).to.equal(addr2.address);
|
||||
});
|
||||
|
||||
it('Should fail if non-owner tries to transfer', async function () {
|
||||
await expect(
|
||||
eventManager.connect(addr2).transferTicket(0, addr2.address)
|
||||
).to.be.revertedWith('You do not own this ticket');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ticket Approval and Transfer', function () {
|
||||
beforeEach(async function () {
|
||||
await createTestEvent();
|
||||
const ticketPriceFlare = await eventManager.getEventPriceFlare(0);
|
||||
await eventManager
|
||||
.connect(addr1)
|
||||
.buyTicket(0, { value: ticketPriceFlare });
|
||||
});
|
||||
|
||||
it('Should allow approving and transferring a ticket', async function () {
|
||||
await expect(
|
||||
eventManager.connect(addr1).approveTicket(0, addr2.address, true)
|
||||
)
|
||||
.to.emit(eventManager, 'TicketTransferApproved')
|
||||
.withArgs(0, addr1.address, addr2.address);
|
||||
|
||||
await expect(
|
||||
eventManager.connect(addr2).transferTicketFrom(0, addr2.address)
|
||||
)
|
||||
.to.emit(eventManager, 'TicketTransferred')
|
||||
.withArgs(0, addr1.address, addr2.address);
|
||||
|
||||
const ticket = await eventManager.tickets(0);
|
||||
expect(ticket.holder).to.equal(addr2.address);
|
||||
});
|
||||
|
||||
it('Should fail if transferring without approval', async function () {
|
||||
await expect(
|
||||
eventManager.connect(addr2).transferTicketFrom(0, addr2.address)
|
||||
).to.be.revertedWith('You are not allowed to transfer this ticket');
|
||||
});
|
||||
});
|
||||
});
|
||||
13
tsconfig.hardhat.json
Normal file
13
tsconfig.hardhat.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "./",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./scripts", "./test", "./typechain-types"],
|
||||
"files": ["./hardhat.config.ts"]
|
||||
}
|
||||
@@ -22,5 +22,5 @@
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "test", "scripts", "hardhat.config.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user