finished conflicts in future merges after last components branch commit

This commit is contained in:
ashprit
2024-10-26 23:50:42 +01:00
37 changed files with 18779 additions and 570 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
NEXT_PUBLIC_RPC_URL=
NEXT_PUBLIC_CONTRACT_ADDRESS=

View File

@@ -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
View File

@@ -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

View File

@@ -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
View 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;

View 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
View 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;

View File

@@ -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>
);
}

View File

@@ -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="Were hosting a Housing Fair, so make sure you save the date! Whether youre living in student accommodation or the local community, this will be a great place to start as you begin thinking about where youll 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>
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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

View File

@@ -0,0 +1,7 @@
import React from 'react';
const WalletAdapter = () => {
return <div>WalletAdapter</div>;
};
export default WalletAdapter;

View File

@@ -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>

View 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;

View File

@@ -3,8 +3,8 @@ import React from 'react';
const Footer = () => {
return (
<footer className="text-center mt-8">
<p className="text-gray-500">
&copy; 2024 Ticket Chain. All rights reserved.
<p className="text-light-purple text-opacity-75">
&copy; 2024 TicketChain. All rights reserved.
</p>
</footer>
);

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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;

View 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">&nbsp;</span>
</motion.span>
))}
</motion.div>
</AnimatePresence>
);
};

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

Binary file not shown.

BIN
public/BGVid2.mp4 Normal file

Binary file not shown.

1
remappings.txt Normal file
View File

@@ -0,0 +1 @@
@flarenetwork/flare-periphery-contracts/=node_modules/@flarenetwork/flare-periphery-contracts/

View File

@@ -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
View 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
View 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"]
}

View File

@@ -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"]
}