Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 25 additions & 33 deletions src/app/dashboard/events/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { StatusBadge } from '@/components/status-badge';
import { Breadcrumbs } from '@/components/breadcrumbs';
import { EventDetails } from '@/components/event-details';
import { buildPublishSummary } from '@/lib/event-details';
import { FieldHint, FormField, Input, Select } from '@/components/form-field';

export default function EventPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params);
Expand Down Expand Up @@ -232,38 +233,35 @@ export default function EventPage({ params }: { params: Promise<{ id: string }>
)}

<form onSubmit={handleAddTicketType} className="mt-4 flex flex-wrap items-end gap-3">
<label className="flex flex-col gap-1 text-sm">
Name
<input
<FormField label="Name">
<Input
required
placeholder="GA"
value={ttName}
onChange={(e) => setTtName(e.target.value)}
className="w-28 rounded-md border border-border bg-surface px-3 py-2"
className="w-28"
/>
</label>
<label className="flex flex-col gap-1 text-sm">
Price
<input
</FormField>
<FormField label="Price">
<Input
required
inputMode="numeric"
placeholder="1000"
value={ttPrice}
onChange={(e) => setTtPrice(e.target.value)}
className="w-28 rounded-md border border-border bg-surface px-3 py-2"
className="w-28"
/>
</label>
<label className="flex flex-col gap-1 text-sm">
Quantity
<input
</FormField>
<FormField label="Quantity">
<Input
required
inputMode="numeric"
placeholder="100"
value={ttQuantity}
onChange={(e) => setTtQuantity(e.target.value)}
className="w-28 rounded-md border border-border bg-surface px-3 py-2"
className="w-28"
/>
</label>
</FormField>
<Button
type="submit"
loading={savingTicketType}
Expand All @@ -277,41 +275,35 @@ export default function EventPage({ params }: { params: Promise<{ id: string }>
<>
<h2 className="mt-10 font-heading text-xl font-bold">Issue a ticket</h2>
<form onSubmit={handleIssue} className="mt-4 flex flex-col gap-4">
<label className="flex flex-col gap-1 text-sm">
Recipient email
<input
<FormField label="Recipient email">
<Input
type="email"
required
value={issueEmail}
onChange={(e) => setIssueEmail(e.target.value)}
className="rounded-md border border-border bg-surface px-3 py-2"
/>
<span className="text-xs text-muted">
<FieldHint>
The recipient must already have a StellarTickets account with a connected wallet.
</span>
</label>
<label className="flex flex-col gap-1 text-sm">
Ticket type
<select
</FieldHint>
</FormField>
<FormField label="Ticket type">
<Select
value={issueTicketTypeId}
onChange={(e) => setIssueTicketTypeId(e.target.value)}
className="rounded-md border border-border bg-surface px-3 py-2"
>
{event.ticketTypes.map((tt) => (
<option key={tt.id} value={tt.id}>
{tt.name}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
Seat (optional)
<input
</Select>
</FormField>
<FormField label="Seat (optional)">
<Input
value={issueSeat}
onChange={(e) => setIssueSeat(e.target.value)}
className="rounded-md border border-border bg-surface px-3 py-2"
/>
</label>
</FormField>
<Button
type="submit"
loading={issuing}
Expand Down
67 changes: 27 additions & 40 deletions src/app/dashboard/organizations/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { FormError } from "@/components/form-error";
import { Button } from "@/components/button";
import { StatusBadge } from "@/components/status-badge";
import { Breadcrumbs } from "@/components/breadcrumbs";
import { FieldHint, FormField, Input, Select } from "@/components/form-field";

// Resale cap is a multiplier of face value (100% = no markup allowed); royalty is a
// straightforward percentage of the resale price. Bounds are UI guardrails, not on-chain limits.
Expand Down Expand Up @@ -169,62 +170,51 @@ export default function OrganizationPage({
<h2 className="mt-12 font-heading text-xl font-bold">Create an event</h2>
<form onSubmit={handleCreate} className="mt-4 flex flex-col gap-4">
<FormError message={error} />
<label className="flex flex-col gap-1 text-sm">
Event name
<input
<FormField label="Event name">
<Input
required
value={name}
onChange={(e) => setName(e.target.value)}
className="rounded-md border border-border bg-surface px-3 py-2"
/>
</label>
<label className="flex flex-col gap-1 text-sm">
Category
<select
</FormField>
<FormField label="Category">
<Select
value={category}
onChange={(e) =>
setCategory(e.target.value as (typeof INDUSTRIES)[number])
}
className="rounded-md border border-border bg-surface px-3 py-2"
>
{INDUSTRIES.map((i) => (
<option key={i} value={i}>
{INDUSTRY_LABELS[i]}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
Venue
<input
</Select>
</FormField>
<FormField label="Venue">
<Input
required
value={venue}
onChange={(e) => setVenue(e.target.value)}
className="rounded-md border border-border bg-surface px-3 py-2"
/>
</label>
<label className="flex flex-col gap-1 text-sm">
Starts at
<input
</FormField>
<FormField label="Starts at">
<Input
required
type="datetime-local"
value={startsAt}
onChange={(e) => setStartsAt(e.target.value)}
className="rounded-md border border-border bg-surface px-3 py-2"
/>
</label>
<label className="flex flex-col gap-1 text-sm">
Ends at
<input
</FormField>
<FormField label="Ends at">
<Input
type="datetime-local"
value={endsAt}
onChange={(e) => setEndsAt(e.target.value)}
className="rounded-md border border-border bg-surface px-3 py-2"
/>
</label>
<label className="flex flex-col gap-1 text-sm">
Resale cap (% of face value)
<input
</FormField>
<FormField label="Resale cap (% of face value)">
<Input
required
type="number"
inputMode="decimal"
Expand All @@ -233,17 +223,15 @@ export default function OrganizationPage({
step="1"
value={resaleCapPercent}
onChange={(e) => setResaleCapPercent(e.target.value)}
className="rounded-md border border-border bg-surface px-3 py-2"
/>
<span className="text-xs text-muted">
<FieldHint>
The most a ticket can resell for, as a percentage of face value. 120
means resales are capped at 1.2× face value; 100 means no markup is
allowed.
</span>
</label>
<label className="flex flex-col gap-1 text-sm">
Organizer royalty on resale (%)
<input
</FieldHint>
</FormField>
<FormField label="Organizer royalty on resale (%)">
<Input
required
type="number"
inputMode="decimal"
Expand All @@ -252,13 +240,12 @@ export default function OrganizationPage({
step="0.1"
value={royaltyPercent}
onChange={(e) => setRoyaltyPercent(e.target.value)}
className="rounded-md border border-border bg-surface px-3 py-2"
/>
<span className="text-xs text-muted">
<FieldHint>
Percentage of each resale that&apos;s paid to you automatically
on-chain. Set to 0 for no royalty.
</span>
</label>
</FieldHint>
</FormField>
<Button type="submit" loading={submitting} className="self-start">
{submitting ? "Creating…" : "Create event"}
</Button>
Expand Down
44 changes: 19 additions & 25 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { INDUSTRIES, INDUSTRY_LABELS, type Organization } from '@/lib/types';
import { FormError } from '@/components/form-error';
import { WalletConnectButton } from '@/components/wallet-connect-button';
import { Button } from '@/components/button';
import { FieldHint, FormField, Input, Select } from '@/components/form-field';

export default function DashboardPage() {
const { user, loading } = useAuth();
Expand Down Expand Up @@ -124,18 +125,15 @@ export default function DashboardPage() {
<h2 className="mt-12 font-heading text-xl font-bold">Create an organization</h2>
<form onSubmit={handleCreate} className="mt-4 flex flex-col gap-4">
<FormError message={error} />
<label className="flex flex-col gap-1 text-sm">
Organization name
<input
<FormField label="Organization name">
<Input
required
value={name}
onChange={handleNameChange}
className="rounded-md border border-border bg-surface px-3 py-2"
/>
</label>
<label className="flex flex-col gap-1 text-sm">
Slug
<input
</FormField>
<FormField label="Slug">
<Input
required
pattern="[a-z0-9]+(-[a-z0-9]+)*"
placeholder="my-organization"
Expand All @@ -144,44 +142,40 @@ export default function DashboardPage() {
onInvalid={(e) => e.currentTarget.setCustomValidity(SLUG_VALIDATION_MESSAGE)}
onInput={(e) => e.currentTarget.setCustomValidity('')}
aria-describedby="slug-help"
className="rounded-md border border-border bg-surface px-3 py-2"
/>
<span id="slug-help" className="text-xs text-muted">
<FieldHint id="slug-help">
{slug ? `stellartickets.netlify.app/${slug}` : 'This will form your organization’s URL.'}
</span>
</FieldHint>
{slugError && <span className="text-xs text-danger">{slugError}</span>}
</label>
<label className="flex flex-col gap-1 text-sm">
Industry
<select
</FormField>
<FormField label="Industry">
<Select
value={industry}
onChange={(e) => setIndustry(e.target.value as (typeof INDUSTRIES)[number])}
className="rounded-md border border-border bg-surface px-3 py-2"
>
{INDUSTRIES.map((i) => (
<option key={i} value={i}>
{INDUSTRY_LABELS[i]}
</option>
))}
</select>
</label>
<label className="flex flex-col gap-1 text-sm">
Stellar account (G…)
<input
</Select>
</FormField>
<FormField label="Stellar account (G…)">
<Input
required
pattern="G[A-Z0-9]{55}"
value={stellarAccount}
onChange={(e) => {
stellarAccountTouched.current = true;
setStellarAccount(e.target.value);
}}
className="rounded-md border border-border bg-surface px-3 py-2 font-mono text-xs"
className="font-mono text-xs"
/>
<span className="text-xs text-muted">
<FieldHint>
The Stellar account that will sign on-chain writes for this organization — connect
your wallet above to prefill your own address.
</span>
</label>
</FieldHint>
</FormField>
<Button
type="submit"
loading={submitting}
Expand Down
34 changes: 34 additions & 0 deletions src/components/form-field.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { InputHTMLAttributes, ReactNode, SelectHTMLAttributes } from 'react';

/** Shared control styling for dashboard form inputs and selects (#129, #302). */
export const FIELD_CONTROL_CLASS = 'rounded-md border border-border bg-surface px-3 py-2';

/**
* A labelled form field: wraps its control in a <label> so clicking the text
* focuses it, with the label text above and any hints/errors below.
*/
export function FormField({ label, children }: { label: string; children: ReactNode }) {
return (
<label className="flex flex-col gap-1 text-sm">
{label}
{children}
</label>
);
}

/** Muted helper text under a field; pass `id` to reference it via aria-describedby. */
export function FieldHint({ id, children }: { id?: string; children: ReactNode }) {
return (
<span id={id} className="text-xs text-muted">
{children}
</span>
);
}

export function Input({ className, ...props }: InputHTMLAttributes<HTMLInputElement>) {
return <input {...props} className={`${FIELD_CONTROL_CLASS} ${className ?? ''}`.trim()} />;
}

export function Select({ className, ...props }: SelectHTMLAttributes<HTMLSelectElement>) {
return <select {...props} className={`${FIELD_CONTROL_CLASS} ${className ?? ''}`.trim()} />;
}
Loading