diff --git a/soroban-client/README.md b/soroban-client/README.md index e215bc4..0a6e34a 100644 --- a/soroban-client/README.md +++ b/soroban-client/README.md @@ -2,7 +2,15 @@ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next- ## Getting Started -First, run the development server: +Before running the app, configure a few environment variables in `.env.local` (see example in the repo). At a minimum you should set: + +```env +NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org +NEXT_PUBLIC_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" +NEXT_PUBLIC_EVENT_MANAGER_CONTRACT=C... # address of deployed EventManager contract +``` + +Once your env file is populated, start the development server: ```bash npm run dev diff --git a/soroban-client/__tests__/components/Header.test.tsx b/soroban-client/__tests__/components/Header.test.tsx index 3f3b48b..1001990 100644 --- a/soroban-client/__tests__/components/Header.test.tsx +++ b/soroban-client/__tests__/components/Header.test.tsx @@ -21,10 +21,11 @@ describe('Header Component', () => { render(
); expect(screen.getByText('CrowdPass')).toBeInTheDocument(); - expect(screen.getByText('Events')).toBeInTheDocument(); - expect(screen.getByText('Marketplace')).toBeInTheDocument(); - expect(screen.getByText('Create Events')).toBeInTheDocument(); - expect(screen.getByText('Connect Wallet')).toBeInTheDocument(); + // Events, Marketplace, Create Events may appear multiple times (desktop + mobile) + expect(screen.getAllByText('Events').length).toBeGreaterThan(0); + expect(screen.getAllByText('Marketplace').length).toBeGreaterThan(0); + expect(screen.getAllByText('Create Events').length).toBeGreaterThan(0); + expect(screen.getAllByText('Connect Wallet').length).toBeGreaterThan(0); }); it('displays Install Freighter if wallet is not installed', () => { @@ -37,7 +38,8 @@ describe('Header Component', () => { }); render(
); - expect(screen.getByText('Install Freighter')).toBeInTheDocument(); + // Install Freighter may appear multiple times (desktop + mobile) + expect(screen.getAllByText('Install Freighter').length).toBeGreaterThan(0); }); it('renders connected wallet address prefix and disconnect button when connected', () => { @@ -54,11 +56,13 @@ describe('Header Component', () => { render(
); const formattedAddress = 'GBJ2...V2Y2'; - expect(screen.getByText(formattedAddress)).toBeInTheDocument(); + // There are multiple address displays (desktop and mobile), so verify they exist + expect(screen.getAllByText(formattedAddress).length).toBeGreaterThan(0); - const disconnectBtn = screen.getByText('Disconnect'); - expect(disconnectBtn).toBeInTheDocument(); - fireEvent.click(disconnectBtn); + // Disconnect buttons may appear multiple times (desktop + mobile) + const disconnectBtns = screen.getAllByText('Disconnect'); + expect(disconnectBtns.length).toBeGreaterThan(0); + fireEvent.click(disconnectBtns[0]); expect(disconnectMock).toHaveBeenCalledTimes(1); }); }); diff --git a/soroban-client/app/create-event/page.tsx b/soroban-client/app/create-event/page.tsx new file mode 100644 index 0000000..29ec8ed --- /dev/null +++ b/soroban-client/app/create-event/page.tsx @@ -0,0 +1,221 @@ +"use client"; + +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useWallet } from "@/contexts/WalletContext"; +import { createEvent } from "@/lib/soroban"; + +export default function CreateEventPage() { + const router = useRouter(); + const { address, isConnected, isInstalled, connect } = useWallet(); + + const [theme, setTheme] = useState(""); + const [description, setDescription] = useState(""); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [price, setPrice] = useState(""); + const [tickets, setTickets] = useState(""); + const [image, setImage] = useState(null); + + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + const [submitting, setSubmitting] = useState(false); + const [successMsg, setSuccessMsg] = useState(""); + const [errorMsg, setErrorMsg] = useState(""); + + const validate = () => { + const errs: { [key: string]: string } = {}; + const now = Date.now(); + + if (!theme.trim()) errs.theme = "Event name required"; + if (!startDate) errs.startDate = "Start date is required"; + if (!endDate) errs.endDate = "End date is required"; + if (startDate && new Date(startDate).getTime() <= now) + errs.startDate = "Start date must be in the future"; + if (startDate && endDate && new Date(endDate) <= new Date(startDate)) + errs.endDate = "End date must be after start date"; + if (!price) errs.price = "Price required"; + if (price && isNaN(Number(price))) errs.price = "Price must be a number"; + if (price && Number(price) < 0) errs.price = "Price cannot be negative"; + if (!tickets) errs.tickets = "Total tickets required"; + if (tickets && (!/^[0-9]+$/.test(tickets) || Number(tickets) <= 0)) + errs.tickets = "Must be a positive integer"; + + return errs; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!address) { + if (isInstalled) { + await connect(); + } else { + alert("Please install Freighter to create an event."); + return; + } + } + const errs = validate(); + if (Object.keys(errs).length) { + setErrors(errs); + return; + } + setErrors({}); + setSubmitting(true); + setErrorMsg(""); + setSuccessMsg(""); + + try { + const organizer = address!; + const startUnix = Math.floor(new Date(startDate).getTime() / 1000); + const endUnix = Math.floor(new Date(endDate).getTime() / 1000); + const ticketPrice = BigInt(Math.floor(parseFloat(price) * 1_000_000)); + const totalTickets = BigInt(tickets); + + // for simplicity we use zero address as payment token; replace with real + // token contract address or allow user selection later. + const paymentToken = "0000000000000000000000000000000000000000000000000000000000000000"; + + const res = await createEvent({ + organizer, + theme, + eventType: description, + startTimeUnix: startUnix, + endTimeUnix: endUnix, + ticketPrice, + totalTickets, + paymentToken, + }); + + console.log("transaction result", res); + setSuccessMsg("Event created (tx " + res.hash + ")"); + // Optionally redirect to dashboard or home after creation + setTimeout(() => router.push("/"), 3000); + } catch (err: any) { + console.error(err); + setErrorMsg(err.message || "unknown error"); + } finally { + setSubmitting(false); + } + }; + + return ( +
+

Create Event

+ + {successMsg && ( +
{successMsg}
+ )} + {errorMsg && ( +
{errorMsg}
+ )} + +
+
+ + setTheme(e.target.value)} + className="mt-1 block w-full border-gray-300 rounded-md shadow-sm" + /> + {errors.theme && ( +

{errors.theme}

+ )} +
+ +
+ +