Skip to content
Open
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
38 changes: 38 additions & 0 deletions app/(dashboard)/events/new/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react"
import { render, screen } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import NewEventPage from "./page"

// Mock next/navigation
jest.mock("next/navigation", () => ({
useRouter() {
return {
push: jest.fn(),
replace: jest.fn(),
prefetch: jest.fn(),
}
},
}))

describe("NewEventPage focus order", () => {
it("tabs through fields in the correct visual reading order", async () => {
const user = userEvent.setup()
render(<NewEventPage />)

// Start tabbing
await user.tab()
expect(screen.getByLabelText(/event title/i)).toHaveFocus()

await user.tab()
expect(screen.getByLabelText(/description/i)).toHaveFocus()

await user.tab()
// Select is tricky because Radix UI uses a hidden button.
// Let's just check the id or name if possible, or skip to the next
// The select trigger usually has role="combobox"
expect(screen.getByRole("combobox", { name: /category/i })).toHaveFocus()

// Add more expectations to see what currently happens
// By keeping this minimal first, we can run it and see the output!
})
})
162 changes: 83 additions & 79 deletions app/(dashboard)/events/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,86 +64,20 @@ export default function NewEventPage() {

<form onSubmit={handleSubmit} className="space-y-8">
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Event Title</Label>
<Input
id="title"
placeholder="Enter event title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>

<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Describe the prediction event"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="min-h-[120px]"
required
/>
</div>

<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<Select value={category} onValueChange={setCategory} required>
<SelectTrigger id="category">
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Sports">Sports</SelectItem>
<SelectItem value="Finance">Finance</SelectItem>
<SelectItem value="Entertainment">Entertainment</SelectItem>
<SelectItem value="Politics">Politics</SelectItem>
<SelectItem value="Technology">Technology</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
</div>

<div className="space-y-2">
<Label htmlFor="deadline">Deadline</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left font-normal" id="deadline">
<CalendarIcon className="mr-2 h-4 w-4" />
{deadline ? format(deadline, "PPP") : "Select deadline"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar mode="single" selected={deadline} onSelect={setDeadline} initialFocus />
</PopoverContent>
</Popover>
</div>
</div>

<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="visibility">Event Visibility</Label>
<Switch id="visibility" checked={isPublic} onCheckedChange={setIsPublic} />
</div>
<p className="text-sm text-muted-foreground">
{isPublic ? "Public - Visible to all users" : "Private - Visible to selected users only"}
</p>
</div>

<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="featured">Featured Event</Label>
<Switch id="featured" checked={featuredEvent} onCheckedChange={setFeaturedEvent} />
</div>
<p className="text-sm text-muted-foreground">
Featured events appear on the homepage and receive more visibility
</p>
</div>
{/* Row 1 Left: Title */}
<div className="space-y-2 md:col-start-1 md:row-start-1">
<Label htmlFor="title">Event Title</Label>
<Input
id="title"
placeholder="Enter event title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>

<div className="space-y-4">
{/* Row 1 Right: Outcomes (spans multiple rows) */}
<div className="space-y-4 md:col-start-2 md:row-start-1 md:row-span-6">
<div>
<Label>Prediction Options</Label>
<p className="text-sm text-muted-foreground mb-4">Add the possible outcomes for this prediction event</p>
Expand Down Expand Up @@ -214,9 +148,79 @@ export default function NewEventPage() {
</div>
</div>
</div>

{/* Row 2 Left: Description */}
<div className="space-y-2 md:col-start-1 md:row-start-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Describe the prediction event"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="min-h-[120px]"
required
/>
</div>

{/* Row 3 Left: Category and Deadline */}
<div className="grid gap-4 md:grid-cols-2 md:col-start-1 md:row-start-3">
<div className="space-y-2">
<Label htmlFor="category">Category</Label>
<Select value={category} onValueChange={setCategory} required>
<SelectTrigger id="category">
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Sports">Sports</SelectItem>
<SelectItem value="Finance">Finance</SelectItem>
<SelectItem value="Entertainment">Entertainment</SelectItem>
<SelectItem value="Politics">Politics</SelectItem>
<SelectItem value="Technology">Technology</SelectItem>
<SelectItem value="Other">Other</SelectItem>
</SelectContent>
</Select>
</div>

<div className="space-y-2">
<Label htmlFor="deadline">Deadline</Label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left font-normal" id="deadline">
<CalendarIcon className="mr-2 h-4 w-4" />
{deadline ? format(deadline, "PPP") : "Select deadline"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar mode="single" selected={deadline} onSelect={setDeadline} initialFocus />
</PopoverContent>
</Popover>
</div>
</div>

{/* Row 4 Left: Visibility */}
<div className="space-y-2 md:col-start-1 md:row-start-4">
<div className="flex items-center justify-between">
<Label htmlFor="visibility">Event Visibility</Label>
<Switch id="visibility" checked={isPublic} onCheckedChange={setIsPublic} />
</div>
<p className="text-sm text-muted-foreground">
{isPublic ? "Public - Visible to all users" : "Private - Visible to selected users only"}
</p>
</div>

{/* Row 5 Left: Featured */}
<div className="space-y-2 md:col-start-1 md:row-start-5">
<div className="flex items-center justify-between">
<Label htmlFor="featured">Featured Event</Label>
<Switch id="featured" checked={featuredEvent} onCheckedChange={setFeaturedEvent} />
</div>
<p className="text-sm text-muted-foreground">
Featured events appear on the homepage and receive more visibility
</p>
</div>
</div>

<div className="flex items-center justify-end gap-4">
<div className="flex items-center justify-end gap-4 mt-8">
<Button type="button" variant="outline" onClick={() => router.push("/events")}>
Cancel
</Button>
Expand Down
3 changes: 2 additions & 1 deletion app/(dashboard)/settings/ACCESSIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ Accessibility and responsive improvements
Summary
- Added a lightweight `Tabs` wrapper around the Settings page to provide keyboard-navigable grouping and ARIA roles.
- Introduced an `aria-live` polite region announcing save confirmations for assistive technologies.
- Ensured `Switch` and `Select` controls are programmatically labelled via `Label` + `htmlFor`.
- Ensure `Switch` and `Select` controls are programmatically labelled via `Label` + `htmlFor`.
- Interactive option buttons now use `aria-pressed` where appropriate.
- **Form Layouts and Focus Order**: Use explicit CSS Grid placements (`md:col-start-*`, `md:row-start-*`) instead of wrapping columns in separate `div` containers. This allows you to place form fields directly in the DOM in the exact logical left-to-right, top-to-bottom sequence so keyboard tab order naturally follows the visual reading order without relying on positive `tabIndex` values.

How to test locally

Expand Down
15 changes: 12 additions & 3 deletions components/error-boundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, { Component, ErrorInfo, ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { AlertTriangle } from "lucide-react";
import { CopyableText } from "@/components/ui/CopyableText";

interface Props {
children: ReactNode;
Expand All @@ -13,6 +14,7 @@ interface Props {
interface State {
hasError: boolean;
error: Error | null;
incidentId: string | null;
}

/**
Expand All @@ -22,11 +24,12 @@ interface State {
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
this.state = { hasError: false, error: null, incidentId: null };
}

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
const incidentId = Math.random().toString(36).substring(2, 10).toUpperCase();
return { hasError: true, error, incidentId };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
Expand All @@ -40,7 +43,7 @@ export class ErrorBoundary extends Component<Props, State> {
}

handleReset = () => {
this.setState({ hasError: false, error: null });
this.setState({ hasError: false, error: null, incidentId: null });
};

render() {
Expand All @@ -62,6 +65,12 @@ export class ErrorBoundary extends Component<Props, State> {
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{this.state.incidentId && (
<div className="flex flex-col gap-1.5 rounded-md bg-muted/50 p-3 border border-border/50">
<span className="text-xs font-medium text-muted-foreground">Incident ID</span>
<CopyableText text={this.state.incidentId} truncateMiddle={false} className="w-fit" />
</div>
)}
{process.env.NODE_ENV === 'development' && this.state.error && (
<div className="rounded-md bg-muted p-3">
<p className="text-sm font-mono text-destructive">
Expand Down
Loading