Skip to content

Commit 9ff9409

Browse files
authored
UX Enhancements: transitions, prediction detail and more (#87)
1 parent 6767fac commit 9ff9409

File tree

22 files changed

+665
-234
lines changed

22 files changed

+665
-234
lines changed

.cursorrules

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,63 @@ This is a Next.js prediction market application built by a **solo founder** that
2626
- Follow the existing file structure and naming conventions
2727
- **Keep it simple** - Avoid over-engineering for a solo founder project
2828

29+
### UI/UX Patterns
30+
- **Collapsible Content with Gradient Fade**: For long text content that needs to be collapsed, use gradient fade effects to indicate there's more content below:
31+
```jsx
32+
// Collapsed content with fade effect
33+
<div className={cn(
34+
"relative overflow-hidden",
35+
!expanded && "max-h-[5rem]"
36+
)}>
37+
{content}
38+
{!expanded && (
39+
<div className="absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-background to-transparent" />
40+
)}
41+
</div>
42+
```
43+
- **Show More/Less Controls**: Use chevron icons (ChevronDown/ChevronUp) with descriptive text for expand/collapse actions
44+
- **Consistent Collapsed Heights**: Match collapsed heights to related UI sections (e.g., prediction summary height)
45+
46+
### Layout & Spacing Standards
47+
Follow these consistent spacing patterns for professional, polished layouts:
48+
49+
**Page Sections:**
50+
- Use semantic `<section>` elements for major content areas
51+
- **Section spacing**: `mb-24` (6rem/96px) between major sections
52+
- **Hero sections**: `py-16` (4rem/64px) vertical padding, `mb-24` bottom margin
53+
- **Section dividers**: `my-16` (4rem/64px) margin for border separators
54+
55+
**Content Spacing:**
56+
- **Page titles (H1)**: `mb-6` (1.5rem/24px) bottom margin
57+
- **Section titles (H2)**: `mb-6` (1.5rem/24px) bottom margin
58+
- **Content blocks**: `mb-12` (3rem/48px) between content areas within sections
59+
- **Call-to-action elements**: `mt-8` (2rem/32px) top margin for secondary actions
60+
61+
**Container Structure:**
62+
```jsx
63+
// Standard page layout
64+
<main className="container mx-auto px-4 py-8">
65+
<section className="py-16 mb-24"> {/* Hero */}
66+
<h1 className="mb-6">Page Title</h1>
67+
<p>Description</p>
68+
</section>
69+
70+
<section className="mb-24"> {/* Content section */}
71+
<div className="text-center mb-12">
72+
<h2 className="mb-6">Section Title</h2>
73+
<p>Section description</p>
74+
</div>
75+
{/* Section content */}
76+
</section>
77+
</main>
78+
```
79+
80+
**Best Practices:**
81+
- **Consistency**: Always use the same spacing values across similar elements
82+
- **Breathing room**: Generous spacing prevents cramped layouts and improves readability
83+
- **Visual hierarchy**: Larger spacing between major sections, smaller spacing within sections
84+
- **Responsive**: Tailwind spacing classes automatically scale appropriately on mobile
85+
2986
### Running the development server
3087
- If the development server port localhost:3000 is already in use, please do not try to start another development server automatically. Ask first before starting another development server.
3188

@@ -55,6 +112,13 @@ curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/upda
55112
- Follow the existing schema patterns (snake_case for DB columns, camelCase for JS)
56113
- Try to use the migration commands in package.json such as "db:migrate:deploy:dev"
57114
- **Prefer simple queries** - Complex joins can be hard to debug for a solo developer
115+
116+
### Prisma & Serialization Best Practices
117+
- **Prisma JSON Protocol**: Schema uses `jsonProtocol = "true"` to return plain objects instead of Decimal instances
118+
- **Server-to-Client Serialization**: Always use `serializeDecimals()` from `lib/serialization.ts` when passing Prisma data to Client Components
119+
- **API Routes**: Serialize all Prisma responses with `serializeDecimals()` before returning JSON
120+
- **Migration Pattern**: Run `pnpm prisma generate` after schema changes to regenerate client with JSON protocol
121+
- **Error Pattern**: "Only plain objects can be passed to Client Components" = missing serialization
58122
### Database Operations
59123
- Use commands in package.json where possible for database migration in Dev and Preview builds.
60124

CLAUDE.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ See package.json for the most recent commands.
6868
- Use transactions for multi-step operations
6969
- Store raw API responses in separate `_raw` tables with metadata
7070

71+
### Prisma & Serialization Best Practices
72+
- **Prisma JSON Protocol**: Uses `jsonProtocol = "true"` in `schema.prisma` to return plain JSON objects instead of Decimal instances
73+
- **Server-to-Client Serialization**: Use `serializeDecimals()` from `lib/serialization.ts` when passing Prisma data to Client Components
74+
- **API Routes**: Always serialize Prisma responses using `serializeDecimals()` before returning JSON
75+
- **Migration Pattern**: After schema changes, run `pnpm prisma generate` to regenerate the client with JSON protocol
76+
- **Common Issue**: "Only plain objects can be passed to Client Components" errors indicate missing serialization
77+
7178

7279
## Code Style & Patterns
7380

@@ -80,6 +87,63 @@ Follow the project's `.cursorrules` for consistent development:
8087
- Use shadcn/ui components for consistency
8188
- **Keep it simple** - Avoid over-engineering for a solo founder project
8289

90+
### UI/UX Patterns
91+
- **Collapsible Content with Gradient Fade**: For long text content that needs to be collapsed, use gradient fade effects to indicate there's more content below:
92+
```jsx
93+
// Collapsed content with fade effect
94+
<div className={cn(
95+
"relative overflow-hidden",
96+
!expanded && "max-h-[5rem]"
97+
)}>
98+
{content}
99+
{!expanded && (
100+
<div className="absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t from-background to-transparent" />
101+
)}
102+
</div>
103+
```
104+
- **Show More/Less Controls**: Use chevron icons (ChevronDown/ChevronUp) with descriptive text for expand/collapse actions
105+
- **Consistent Collapsed Heights**: Match collapsed heights to related UI sections (e.g., prediction summary height)
106+
107+
### Layout & Spacing Standards
108+
Follow these consistent spacing patterns for professional, polished layouts:
109+
110+
**Page Sections:**
111+
- Use semantic `<section>` elements for major content areas
112+
- **Section spacing**: `mb-24` (6rem/96px) between major sections
113+
- **Hero sections**: `py-16` (4rem/64px) vertical padding, `mb-24` bottom margin
114+
- **Section dividers**: `my-16` (4rem/64px) margin for border separators
115+
116+
**Content Spacing:**
117+
- **Page titles (H1)**: `mb-6` (1.5rem/24px) bottom margin
118+
- **Section titles (H2)**: `mb-6` (1.5rem/24px) bottom margin
119+
- **Content blocks**: `mb-12` (3rem/48px) between content areas within sections
120+
- **Call-to-action elements**: `mt-8` (2rem/32px) top margin for secondary actions
121+
122+
**Container Structure:**
123+
```jsx
124+
// Standard page layout
125+
<main className="container mx-auto px-4 py-8">
126+
<section className="py-16 mb-24"> {/* Hero */}
127+
<h1 className="mb-6">Page Title</h1>
128+
<p>Description</p>
129+
</section>
130+
131+
<section className="mb-24"> {/* Content section */}
132+
<div className="text-center mb-12">
133+
<h2 className="mb-6">Section Title</h2>
134+
<p>Section description</p>
135+
</div>
136+
{/* Section content */}
137+
</section>
138+
</main>
139+
```
140+
141+
**Best Practices:**
142+
- **Consistency**: Always use the same spacing values across similar elements
143+
- **Breathing room**: Generous spacing prevents cramped layouts and improves readability
144+
- **Visual hierarchy**: Larger spacing between major sections, smaller spacing within sections
145+
- **Responsive**: Tailwind spacing classes automatically scale appropriately on mobile
146+
83147
### File Organization
84148
- **Components**: `components/` (reusable) and `app/` (page-specific)
85149
- **Services**: `lib/services/` for business logic

app/about/page.tsx

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,19 @@ export default function AboutPage() {
66
return (
77
<div className="min-h-screen bg-background">
88
<main className="container mx-auto px-4 py-8">
9-
{/* Hero Section with Background */}
10-
<div className="relative overflow-hidden rounded-xl mb-16">
11-
{/* Background Image */}
12-
<div
13-
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
14-
style={{
15-
backgroundImage: 'url("/betterai-background.png")'
16-
}}
17-
/>
18-
{/* Dark Overlay for Text Readability */}
19-
<div className="absolute inset-0 bg-black/40" />
20-
21-
{/* Content */}
22-
<div className="relative z-10 text-center py-24 px-8">
23-
<h1 className="text-4xl md:text-5xl font-bold text-white mb-4 flex items-center justify-center gap-2">
24-
<TrendingUp className="text-blue-300" />
25-
What is BetterAI?
26-
</h1>
27-
<p className="text-blue-100 text-lg max-w-3xl mx-auto">
28-
Democratizing access to world-class AI predictions for everyone
29-
</p>
30-
</div>
31-
</div>
9+
{/* Hero Section */}
10+
<section className="text-center py-16 mb-24">
11+
<h1 className="text-4xl md:text-5xl font-bold text-foreground mb-6 flex items-center justify-center gap-2">
12+
<TrendingUp className="text-primary" />
13+
What is BetterAI?
14+
</h1>
15+
<p className="text-muted-foreground text-lg max-w-3xl mx-auto">
16+
Democratizing access to world-class AI predictions for everyone
17+
</p>
18+
</section>
3219

3320
{/* Better Definition 1 */}
34-
<div className="mb-20" data-testid="better-definition-1">
21+
<section className="mb-24" data-testid="better-definition-1">
3522
<div className="text-center mb-12">
3623
<h2 className="mb-4 flex items-baseline justify-center text-foreground">
3724
<span className="text-sm sm:text-base font-medium mr-2">[bet-er]¹ (noun)</span>
@@ -78,24 +65,25 @@ export default function AboutPage() {
7865
</div>
7966
))}
8067
</div>
81-
</div>
82-
<div className="border-t border-border/40 my-12 max-w-4xl mx-auto" />
68+
</section>
69+
70+
<div className="border-t border-border/40 my-16 max-w-4xl mx-auto" />
8371

8472

85-
<div className="mb-20" data-testid="better-definition-2">
73+
<section className="mb-24" data-testid="better-definition-2">
8674
<div className="text-center mb-12">
87-
<h2 className="mb-4 flex items-baseline justify-center text-foreground">
75+
<h2 className="mb-6 flex items-baseline justify-center text-foreground">
8876
<span className="text-sm sm:text-base font-medium mr-2">[bet-er]<sup>2</sup> (adjective)</span>
8977
<span className="text-3xl sm:text-4xl font-bold">improved in accuracy or performance</span>
9078
</h2>
9179
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
9280
<a href="https://hai.stanford.edu/ai-index/2025-ai-index-report" className="hover:underline">Leading AI models are close to beating our most difficult benchmarks. The world needs better, infinitely difficult benchmarks, to properly measure the AI model's intelligence growth beyond super intelligence.</a>
9381
</p>
9482
</div>
95-
<div className="max-w-5xl mx-auto rounded-lg border bg-card p-4 shadow-sm">
83+
<div className="max-w-5xl mx-auto rounded-lg border bg-card p-6 shadow-sm">
9684
<AiVsHumanAccuracyChart />
9785
</div>
98-
</div>
86+
</section>
9987

10088

10189
{/* How it Works Section */}

app/api/predictions/recent/route.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,7 @@
11
import { NextResponse } from "next/server"
22
import { predictionQueries } from "@/lib/db/queries"
33
import { requireAuth, createAuthErrorResponse } from "@/lib/auth"
4-
5-
function serialize(value: any): any {
6-
if (value == null) return value
7-
if (Array.isArray(value)) return value.map(serialize)
8-
if (value instanceof Date) return value.toISOString()
9-
if (typeof value === 'object') {
10-
if (typeof (value as any)?.toNumber === 'function') {
11-
try { return Number((value as any).toNumber()) } catch {}
12-
}
13-
const out: Record<string, any> = {}
14-
for (const [k, v] of Object.entries(value)) out[k] = serialize(v)
15-
return out
16-
}
17-
return value
18-
}
4+
import { serializeDecimals } from "@/lib/serialization"
195

206
export async function GET(request: Request) {
217
try {
@@ -46,7 +32,7 @@ export async function GET(request: Request) {
4632
const { items, nextCursor } = result
4733

4834
return NextResponse.json({
49-
items: serialize(items),
35+
items: serializeDecimals(items),
5036
nextCursor,
5137
pageSize: limit,
5238
filteredByTags: tagIds || null,

app/market/[marketId]/page.tsx

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { formatVolume, generateMarketURL } from '@/lib/utils'
88
import type { PredictionResult } from '@/lib/types'
99
import MarketDetailsCard from '@/components/market-details-card'
1010
import { MarketEventHeader } from '@/components/market-event-header'
11+
import { PredictionReasoningCard } from '@/components/prediction-reasoning-card'
12+
import { PredictionHistoryList } from '@/components/prediction-history-list'
13+
import { serializePredictionData } from '@/lib/serialization'
1114

1215
interface MarketDetailPageProps {
1316
params: Promise<{
@@ -27,8 +30,11 @@ export default async function MarketDetailPage({ params }: MarketDetailPageProps
2730
// Fetch event data if market has an eventId
2831
const event = market.eventId ? await eventQueries.getEventById(market.eventId) : null
2932

30-
// Fetch most recent prediction
31-
const prediction = await predictionQueries.getMostRecentPredictionByMarketId(marketId)
33+
// Fetch most recent prediction and all predictions for history
34+
const [prediction, allPredictions] = await Promise.all([
35+
predictionQueries.getMostRecentPredictionByMarketId(marketId),
36+
predictionQueries.getPredictionsByMarketId(marketId)
37+
])
3238
const predictionResult = prediction?.predictionResult as PredictionResult | null
3339
const externalMarketUrl = await generateMarketURL(marketId)
3440

@@ -102,9 +108,12 @@ export default async function MarketDetailPage({ params }: MarketDetailPageProps
102108
{predictionResult.reasoning && (
103109
<div>
104110
<h4 className="font-medium mb-2">Reasoning</h4>
105-
<p className="text-sm text-muted-foreground">
106-
{predictionResult.reasoning}
107-
</p>
111+
<PredictionReasoningCard
112+
reasoning={predictionResult.reasoning}
113+
collapsedHeight="7rem"
114+
showHeader={false}
115+
className="border-0 shadow-none bg-transparent"
116+
/>
108117
</div>
109118
)}
110119

@@ -132,6 +141,17 @@ export default async function MarketDetailPage({ params }: MarketDetailPageProps
132141
)}
133142
</CardContent>
134143
</Card>
144+
145+
{/* Past Predictions */}
146+
{allPredictions.length > 1 && (
147+
<PredictionHistoryList
148+
predictions={serializePredictionData(allPredictions)}
149+
marketId={marketId}
150+
showChecks={false}
151+
showPredictions={true}
152+
className="mt-6"
153+
/>
154+
)}
135155
</div>
136156

137157

app/market/[marketId]/predictions/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { eventQueries, marketQueries, predictionQueries } from '@/lib/db/queries
33
import { MarketEventHeader } from '@/components/market-event-header'
44
import { PredictionSummaryCard } from '@/components/prediction-summary-card'
55
import type { PredictionResult } from '@/lib/types'
6+
import { serializeDecimals } from '@/lib/serialization'
67

78
type PageParams = { marketId: string }
89
type PageProps = { params: Promise<PageParams> }
@@ -42,14 +43,14 @@ export default async function MarketPredictionsPage({ params }: PageProps) {
4243
{predictions.map((p) => {
4344
const predictionResult = (p as any).predictionResult as PredictionResult | null
4445
const aiOutcomes = (p as any).outcomes ?? null
45-
const aiOutcomesProbabilities = (p as any).outcomesProbabilities ?? null
46+
const aiOutcomesProbabilities = serializeDecimals((p as any).outcomesProbabilities) ?? null
4647
const confidenceLevel = predictionResult?.confidence_level ?? null
4748

4849
return (
4950
<PredictionSummaryCard
5051
key={p.id}
5152
marketOutcomes={market.outcomes ?? null}
52-
marketOutcomePrices={(market as any).outcomePrices ?? null}
53+
marketOutcomePrices={serializeDecimals((market as any).outcomePrices) ?? null}
5354
aiOutcomes={aiOutcomes}
5455
aiOutcomesProbabilities={aiOutcomesProbabilities}
5556
confidenceLevel={confidenceLevel}

app/prediction/[predictionId]/page.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { predictionCheckQueries, predictionQueries as pq } from "@/lib/db/querie
88
import { PredictionHistoryList } from "@/components/prediction-history-list"
99
import { MarketEventHeader } from "@/components/market-event-header"
1010
import { PredictionUserMessageCard } from "@/components/prediction-user-message-card"
11+
import { serializePredictionData, serializePredictionChecks } from "@/lib/serialization"
1112

1213
type PageParams = { predictionId: string }
1314
type PageProps = { params: Promise<PageParams> }
@@ -78,18 +79,8 @@ export default async function PredictionDetailPage({ params }: PageProps) {
7879
{/* Then: Past predictions only */}
7980
<PredictionHistoryList
8081
className="mt-2"
81-
checks={checks?.map((c) => ({
82-
createdAt: c.createdAt as any,
83-
aiProbability: (c as any).aiProbability,
84-
marketProbability: (c as any).marketProbability,
85-
delta: (c as any).absDelta ?? (c as any).delta,
86-
marketClosed: c.marketClosed ?? null,
87-
}))}
88-
predictions={pastPredictions?.map((p) => ({
89-
createdAt: p.createdAt as any,
90-
modelName: p.modelName ?? null,
91-
outcomesProbabilities: (p as any).outcomesProbabilities ?? null,
92-
}))}
82+
checks={checks ? serializePredictionChecks(checks) : null}
83+
predictions={pastPredictions ? serializePredictionData(pastPredictions) : null}
9384
marketId={marketId ?? null}
9485
showChecks={false}
9586
showPredictions={true}

0 commit comments

Comments
 (0)