mirror of
https://github.com/0xShay/ticketchain.git
synced 2026-01-11 13:13:25 +00:00
Merge branch 'main' of https://github.com/Ayush272002/Event-Chain into add_buy_button_handler
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
"extends": ["next/core-web-vitals", "next/typescript"],
|
||||
"rules": {
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
"@typescript-eslint/no-unused-vars":"off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# TicketChain
|
||||
|
||||
## Problem
|
||||
|
||||
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:
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
@@ -12,6 +14,7 @@ Many popular ticket sites exist, that allow customers to browse different upcomi
|
||||
- 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.
|
||||
|
||||
## Proposal
|
||||
|
||||
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 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.
|
||||
|
||||
@@ -62,7 +62,7 @@ const fetchEvents = (): Event[] => {
|
||||
const EventsPage: React.FC = () => {
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [filteredEvents, setFilteredEvents] = useState<Event[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [hoveredEventId, setHoveredEventId] = useState<number | null>(null);
|
||||
const [sortOption, setSortOption] = useState<string>('');
|
||||
const [filterOptions, setFilterOptions] = useState<string[]>([]);
|
||||
@@ -81,14 +81,16 @@ const EventsPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const SearchBox = () => {
|
||||
setSearchQuery(useSearchParams().get("q") || "");
|
||||
return <input
|
||||
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(() => {
|
||||
@@ -177,7 +179,8 @@ const EventsPage: React.FC = () => {
|
||||
|
||||
<div className="relative z-20 container mx-auto p-4 pt-16">
|
||||
<div className="mb-6">
|
||||
<Suspense fallback={
|
||||
<Suspense
|
||||
fallback={
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search events..."
|
||||
@@ -186,7 +189,8 @@ const EventsPage: React.FC = () => {
|
||||
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">
|
||||
|
||||
@@ -57,11 +57,11 @@ export default function Home() {
|
||||
<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">
|
||||
<div className="text-6xl font-bold text-white text-center text-shadow-lg">
|
||||
Book your next
|
||||
<FlipWords
|
||||
words={words}
|
||||
className="text-light-purple text-opacity-75"
|
||||
className="text-pink-500 text-opacity-75 pl-3.5"
|
||||
/>
|
||||
on the Flare blockchain.
|
||||
</div>
|
||||
|
||||
30
components/PreviousTickets.tsx
Normal file
30
components/PreviousTickets.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { PreviousTicketComponent } from '@/components/custom/previousTicket';
|
||||
|
||||
const PreviousTickets = () => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<PreviousTicketComponent
|
||||
name="Concert Ticket"
|
||||
status={true}
|
||||
description="This is a previous ticket for a concert event."
|
||||
capacity={500}
|
||||
ticketPrice={50}
|
||||
eventStartDate={new Date('2023-09-15T19:00:00')}
|
||||
eventHost="0x1234567890abcdef1234567890abcdef12345678"
|
||||
/>
|
||||
<PreviousTicketComponent
|
||||
name="Festival Ticket"
|
||||
status={false}
|
||||
description="This is a previous ticket for a festival event."
|
||||
capacity={1000}
|
||||
ticketPrice={100}
|
||||
eventStartDate={new Date('2023-07-10T12:00:00')}
|
||||
eventEndDate={new Date('2023-07-12T23:59:00')} // Optional, passed here
|
||||
eventHost="0xabcdef1234567890abcdef1234567890abcdef12"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviousTickets;
|
||||
0
components/TicketListings.tsx
Normal file
0
components/TicketListings.tsx
Normal file
223
components/custom/EventForm.tsx
Normal file
223
components/custom/EventForm.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
'use client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
// Define the schema using Zod
|
||||
const eventSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, { message: 'Event name is required' }),
|
||||
description: z.string().min(1, { message: 'Description is required' }),
|
||||
capacity: z
|
||||
.number({ invalid_type_error: 'Capacity must be a number' })
|
||||
.min(1, { message: 'Capacity must be at least 1' })
|
||||
.refine(Number.isInteger, { message: 'Capacity must be an integer' }),
|
||||
ticketPrice: z
|
||||
.number({ invalid_type_error: 'Ticket price must be a number' })
|
||||
.min(0, { message: 'Ticket price must be at least 0' })
|
||||
.refine(Number.isInteger, { message: 'Ticket price must be in cents' }),
|
||||
location: z.string().min(1, { message: 'Location is required' }),
|
||||
eventStartTime: z.preprocess(
|
||||
(val) =>
|
||||
typeof val === 'string' && val !== '' ? new Date(val) : undefined,
|
||||
z
|
||||
.date({ required_error: 'Event start time is required' })
|
||||
.min(new Date(), { message: 'Event start time must be in the future' })
|
||||
),
|
||||
eventEndTime: z.preprocess(
|
||||
(val) =>
|
||||
typeof val === 'string' && val !== '' ? new Date(val) : undefined,
|
||||
z.date().optional()
|
||||
),
|
||||
images: z.preprocess(
|
||||
(val) => {
|
||||
if (Array.isArray(val)) {
|
||||
// Filter out empty strings
|
||||
return val.filter((v) => v !== '');
|
||||
}
|
||||
return [];
|
||||
},
|
||||
z
|
||||
.array(z.string().url({ message: 'Invalid image URL format' }))
|
||||
.optional()
|
||||
),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.eventEndTime && data.eventEndTime <= data.eventStartTime) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Event end time must be after the start time',
|
||||
path: ['eventEndTime'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Define the TypeScript type based on the Zod schema
|
||||
type EventFormData = z.infer<typeof eventSchema>;
|
||||
|
||||
interface EventFormProps {
|
||||
onSubmit: (data: EventFormData) => void;
|
||||
}
|
||||
|
||||
const EventForm = ({ onSubmit }: EventFormProps) => {
|
||||
const [imageFields, setImageFields] = useState<string[]>(['']);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
watch,
|
||||
} = useForm<EventFormData>({
|
||||
resolver: zodResolver(eventSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
images: [''],
|
||||
},
|
||||
});
|
||||
|
||||
const images = watch('images') || [];
|
||||
|
||||
const handleAddImageField = () => {
|
||||
setImageFields((prev) => [...prev, '']);
|
||||
};
|
||||
|
||||
const handleRemoveImageField = (index: number) => {
|
||||
const updatedImages = [...imageFields];
|
||||
updatedImages.splice(index, 1);
|
||||
setImageFields(updatedImages);
|
||||
// Also update the form values
|
||||
setValue('images', updatedImages);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* Name Field */}
|
||||
<div>
|
||||
<Label htmlFor="name">Event Name</Label>
|
||||
<Input id="name" {...register('name')} />
|
||||
{errors.name && <p className="text-red-500">{errors.name.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Description Field */}
|
||||
<div>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Textarea id="description" {...register('description')} />
|
||||
{errors.description && (
|
||||
<p className="text-red-500">{errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Capacity Field */}
|
||||
<div>
|
||||
<Label htmlFor="capacity">Capacity</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="capacity"
|
||||
{...register('capacity', { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.capacity && (
|
||||
<p className="text-red-500">{errors.capacity.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Ticket Price Field */}
|
||||
<div>
|
||||
<Label htmlFor="ticketPrice">Ticket Price (in USD cents)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
id="ticketPrice"
|
||||
{...register('ticketPrice', { valueAsNumber: true })}
|
||||
/>
|
||||
{errors.ticketPrice && (
|
||||
<p className="text-red-500">{errors.ticketPrice.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Location Field */}
|
||||
<div>
|
||||
<Label htmlFor="description">Location</Label>
|
||||
<Textarea id="description" {...register('location')} />
|
||||
{errors.description && (
|
||||
<p className="text-red-500">{errors.description.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Event Start Time Field */}
|
||||
<div>
|
||||
<Label htmlFor="eventStartTime">Event Start Date & Time</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
id="eventStartTime"
|
||||
{...register('eventStartTime')}
|
||||
/>
|
||||
{errors.eventStartTime && (
|
||||
<p className="text-red-500">{errors.eventStartTime.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Event End Time Field */}
|
||||
<div>
|
||||
<Label htmlFor="eventEndTime">Event End Date & Time (Optional)</Label>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
id="eventEndTime"
|
||||
{...register('eventEndTime')}
|
||||
/>
|
||||
{errors.eventEndTime && (
|
||||
<p className="text-red-500">{errors.eventEndTime.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Images Field */}
|
||||
<div>
|
||||
<Label>Event Images (Optional)</Label>
|
||||
{imageFields.map((_, index) => (
|
||||
<div key={index} className="flex items-center space-x-2 mt-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Enter image URL"
|
||||
{...register(`images.${index}`)}
|
||||
/>
|
||||
{imageFields.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => handleRemoveImageField(index)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" onClick={handleAddImageField}>
|
||||
Add URL
|
||||
</Button>
|
||||
{/* Display individual image URL errors */}
|
||||
{errors.images &&
|
||||
Array.isArray(errors.images) &&
|
||||
errors.images.map((imgError, index) => {
|
||||
if (imgError) {
|
||||
const message = imgError.message || 'Invalid image URL';
|
||||
return (
|
||||
<p key={index} className="text-red-500">
|
||||
Image {index + 1}: {message}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<Button type="submit">Create Event</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventForm;
|
||||
@@ -19,26 +19,33 @@ interface props {
|
||||
}
|
||||
|
||||
const FeaturedEvent = ({
|
||||
name, description, location, eventStartDate, eventHost, imageURL
|
||||
name,
|
||||
description,
|
||||
location,
|
||||
eventStartDate,
|
||||
eventHost,
|
||||
imageURL,
|
||||
}: props) => {
|
||||
return <Card>
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{imageURL && <img src={imageURL} alt={name}></img>}
|
||||
<CardTitle>
|
||||
{name}
|
||||
</CardTitle>
|
||||
<CardTitle>{name}</CardTitle>
|
||||
<CardDescription>
|
||||
{location}<br />
|
||||
{location}
|
||||
<br />
|
||||
{new Date(eventStartDate * 1000).toLocaleString()}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{description}
|
||||
</CardContent>
|
||||
<CardContent>{description}</CardContent>
|
||||
<CardFooter>
|
||||
<i>Host: {eventHost.substring(0, 8)}...{eventHost.substring(eventHost.length-3)}</i>
|
||||
<i>
|
||||
Host: {eventHost.substring(0, 8)}...
|
||||
{eventHost.substring(eventHost.length - 3)}
|
||||
</i>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturedEvent;
|
||||
104
components/custom/previousTicket.tsx
Normal file
104
components/custom/previousTicket.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
|
||||
export interface PreviousTicket {
|
||||
name: string;
|
||||
status: boolean;
|
||||
description: string;
|
||||
capacity: number;
|
||||
ticketPrice: number;
|
||||
eventStartDate: Date;
|
||||
eventEndDate?: Date;
|
||||
eventHost: string; // metamask address
|
||||
}
|
||||
|
||||
const cardVariants = {
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
visible: { opacity: 1, y: 0 },
|
||||
};
|
||||
|
||||
export const PreviousTicketComponent = ({
|
||||
name,
|
||||
status,
|
||||
description,
|
||||
capacity,
|
||||
ticketPrice,
|
||||
eventStartDate,
|
||||
eventEndDate,
|
||||
eventHost,
|
||||
}: PreviousTicket) => {
|
||||
return (
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={cardVariants}
|
||||
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||
>
|
||||
<Card className="w-[350px] bg-[#1e2a3a] text-white shadow-lg">
|
||||
<CardHeader>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1, duration: 0.5 }}
|
||||
>
|
||||
<CardTitle className="text-lg font-semibold">{name}</CardTitle>
|
||||
<CardDescription className="text-gray-400">
|
||||
Status: {status ? 'Active' : 'Inactive'}
|
||||
</CardDescription>
|
||||
</motion.div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 wrap">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.4 }}
|
||||
>
|
||||
<p className="text-gray-300 mb-2 whitespace-normal break-words">
|
||||
{description}
|
||||
</p>
|
||||
<p className="text-gray-400">
|
||||
<strong>Capacity:</strong> {capacity}
|
||||
</p>
|
||||
<p className="text-gray-400">
|
||||
<strong>Ticket Price:</strong> ${ticketPrice.toFixed(2)}
|
||||
</p>
|
||||
<p className="text-gray-400">
|
||||
<strong>Event Start:</strong> {eventStartDate.toLocaleString()}
|
||||
</p>
|
||||
{eventEndDate && (
|
||||
<p className="text-gray-400">
|
||||
<strong>Event End:</strong> {eventEndDate.toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-gray-400 whitespace-normal break-all">
|
||||
<strong>Host:</strong> {eventHost}
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4, duration: 0.4 }}
|
||||
>
|
||||
<Button className="bg-[#365b85] text-white hover:bg-[#2b4a70]">
|
||||
View Details
|
||||
</Button>
|
||||
</motion.div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSDK, MetaMaskProvider } from '@metamask/sdk-react';
|
||||
import { WalletIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Popover,
|
||||
@@ -9,54 +9,87 @@ import {
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
|
||||
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>
|
||||
);
|
||||
declare global {
|
||||
interface Window {
|
||||
ethereum?: {
|
||||
isMetaMask?: boolean;
|
||||
request: (request: {
|
||||
method: string;
|
||||
params?: Array<unknown>; // Use `unknown` instead of `any`
|
||||
}) => Promise<unknown>; // Specify a more accurate return type if possible
|
||||
};
|
||||
}
|
||||
|
||||
function formatAddress(address: string | undefined): string {
|
||||
if (!address) return '';
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
}
|
||||
|
||||
function MetaMaskConnect() {
|
||||
const { sdk, connected, connecting, account } = useSDK();
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||
const [account, setAccount] = useState<string | null>(null);
|
||||
|
||||
// Initial check on load
|
||||
useEffect(() => {
|
||||
setIsConnected(connected);
|
||||
}, [connected]);
|
||||
|
||||
const connect = async () => {
|
||||
const checkConnection = async () => {
|
||||
if (typeof window !== 'undefined' && window.ethereum) {
|
||||
try {
|
||||
await sdk?.connect();
|
||||
// Check if there are any accounts already connected
|
||||
const accounts = (await window.ethereum.request({
|
||||
method: 'eth_accounts',
|
||||
})) as string[];
|
||||
if (accounts && accounts.length > 0) {
|
||||
setIsConnected(true);
|
||||
} catch (err) {
|
||||
console.warn(`No accounts found`, err);
|
||||
setAccount(accounts[0]);
|
||||
localStorage.setItem('isConnected', JSON.stringify(true));
|
||||
localStorage.setItem('account', accounts[0]);
|
||||
} else {
|
||||
// No connected accounts found; check `localStorage`
|
||||
const storedIsConnected = JSON.parse(
|
||||
localStorage.getItem('isConnected') || 'false'
|
||||
);
|
||||
const storedAccount = localStorage.getItem('account') || null;
|
||||
setIsConnected(storedIsConnected);
|
||||
setAccount(storedAccount);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking MetaMask connection:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = () => {
|
||||
if (sdk) {
|
||||
sdk.terminate();
|
||||
setIsConnected(false);
|
||||
checkConnection();
|
||||
}, []);
|
||||
|
||||
// Update localStorage whenever connection state changes
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('isConnected', JSON.stringify(isConnected));
|
||||
localStorage.setItem('account', account || '');
|
||||
}
|
||||
}, [isConnected, account]);
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
const accounts = (await window.ethereum?.request({
|
||||
method: 'eth_requestAccounts',
|
||||
})) as string[];
|
||||
if (accounts && accounts.length > 0) {
|
||||
setIsConnected(true);
|
||||
setAccount(accounts[0]);
|
||||
localStorage.setItem('isConnected', JSON.stringify(true));
|
||||
localStorage.setItem('account', accounts[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MetaMask connection failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
setIsConnected(false);
|
||||
setAccount(null);
|
||||
localStorage.setItem('isConnected', JSON.stringify(false));
|
||||
localStorage.removeItem('account');
|
||||
await window.ethereum?.request({
|
||||
method: 'wallet_revokePermissions',
|
||||
params: [{ eth_accounts: {} }],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -65,7 +98,7 @@ function MetaMaskConnect() {
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="link" className="text-white">
|
||||
{formatAddress(account)}
|
||||
{account && `${account.slice(0, 6)}...${account.slice(-4)}`}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-44">
|
||||
@@ -80,7 +113,6 @@ function MetaMaskConnect() {
|
||||
</Popover>
|
||||
) : (
|
||||
<Button
|
||||
disabled={connecting}
|
||||
onClick={connect}
|
||||
className="bg-light-purple bg-opacity-75 hover:bg-purple border-0 hover:border-0"
|
||||
>
|
||||
@@ -91,18 +123,4 @@ function MetaMaskConnect() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function MetaMaskConnectWrapper() {
|
||||
return (
|
||||
<MetaMaskProvider
|
||||
debug={false}
|
||||
sdkOptions={{
|
||||
dappMetadata: {
|
||||
name: 'My dApp',
|
||||
url: typeof window !== 'undefined' ? window.location.href : '',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MetaMaskConnect />
|
||||
</MetaMaskProvider>
|
||||
);
|
||||
}
|
||||
export default MetaMaskConnect;
|
||||
|
||||
24
components/ui/textarea.tsx
Normal file
24
components/ui/textarea.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export { Textarea };
|
||||
@@ -42,7 +42,6 @@ export const buyHandler = async (
|
||||
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);
|
||||
|
||||
3497
package-lock.json
generated
3497
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
||||
"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",
|
||||
@@ -24,11 +25,14 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"ethers": "^6.13.4",
|
||||
"framer-motion": "^11.11.10",
|
||||
"lucide-react": "^0.446.0",
|
||||
"next": "14.2.13",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.1",
|
||||
"zod": "^3.23.8",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
|
||||
@@ -72,6 +72,14 @@ const config: Config = {
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
typography: {
|
||||
wrap: {
|
||||
'*': {
|
||||
wordBreak: 'break-word',
|
||||
overflowWrap: 'break-word',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [tailwindcssAnimate, addVariablesForColors],
|
||||
|
||||
@@ -39,8 +39,8 @@ describe('EventManager', function () {
|
||||
);
|
||||
}
|
||||
|
||||
describe("Event Creation", function () {
|
||||
it("Should create an event with correct details", async function () {
|
||||
describe('Event Creation', function () {
|
||||
it('Should create an event with correct details', async function () {
|
||||
await createTestEvent();
|
||||
|
||||
const event = await eventManager.events(0);
|
||||
|
||||
Reference in New Issue
Block a user