Spaces:
Sleeping
Sleeping
saadrizvi09 commited on
Commit ·
8626b2e
1
Parent(s): 894516e
added search
Browse files- backend/src/app.module.ts +16 -8
- backend/src/markets/markets.controller.ts +34 -13
- backend/src/markets/markets.module.ts +13 -20
- backend/src/markets/markets.service.ts +135 -1
- frontend/src/app/dashboard/page.tsx +189 -106
- frontend/src/lib/api.ts +13 -0
backend/src/app.module.ts
CHANGED
|
@@ -1,19 +1,27 @@
|
|
| 1 |
-
import { Module } from '@nestjs/common';
|
| 2 |
import { ConfigModule } from '@nestjs/config';
|
| 3 |
import { BullModule } from '@nestjs/bullmq';
|
| 4 |
import { AuthModule } from './auth/auth.module';
|
| 5 |
import { MarketsModule } from './markets/markets.module';
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
@Module({
|
| 8 |
imports: [
|
| 9 |
ConfigModule.forRoot({ isGlobal: true }),
|
| 10 |
-
|
| 11 |
-
connection: {
|
| 12 |
-
host: process.env.REDIS_HOST || '127.0.0.1',
|
| 13 |
-
port: parseInt(process.env.REDIS_PORT || '6379'),
|
| 14 |
-
maxRetriesPerRequest: null,
|
| 15 |
-
},
|
| 16 |
-
}),
|
| 17 |
AuthModule,
|
| 18 |
MarketsModule,
|
| 19 |
],
|
|
|
|
| 1 |
+
import { Module, DynamicModule } from '@nestjs/common';
|
| 2 |
import { ConfigModule } from '@nestjs/config';
|
| 3 |
import { BullModule } from '@nestjs/bullmq';
|
| 4 |
import { AuthModule } from './auth/auth.module';
|
| 5 |
import { MarketsModule } from './markets/markets.module';
|
| 6 |
|
| 7 |
+
const redisAvailable = process.env.REDIS_HOST || process.env.ENABLE_REDIS === 'true';
|
| 8 |
+
|
| 9 |
+
const optionalBull: DynamicModule[] = redisAvailable
|
| 10 |
+
? [
|
| 11 |
+
BullModule.forRoot({
|
| 12 |
+
connection: {
|
| 13 |
+
host: process.env.REDIS_HOST || '127.0.0.1',
|
| 14 |
+
port: parseInt(process.env.REDIS_PORT || '6379'),
|
| 15 |
+
maxRetriesPerRequest: null,
|
| 16 |
+
},
|
| 17 |
+
}),
|
| 18 |
+
]
|
| 19 |
+
: [];
|
| 20 |
+
|
| 21 |
@Module({
|
| 22 |
imports: [
|
| 23 |
ConfigModule.forRoot({ isGlobal: true }),
|
| 24 |
+
...optionalBull,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
AuthModule,
|
| 26 |
MarketsModule,
|
| 27 |
],
|
backend/src/markets/markets.controller.ts
CHANGED
|
@@ -3,25 +3,34 @@ import {
|
|
| 3 |
Get,
|
| 4 |
Post,
|
| 5 |
Param,
|
|
|
|
|
|
|
| 6 |
Res,
|
|
|
|
|
|
|
| 7 |
NotFoundException,
|
| 8 |
} from '@nestjs/common';
|
| 9 |
-
import { InjectQueue } from '@nestjs/bullmq';
|
| 10 |
-
import { Queue } from 'bullmq';
|
| 11 |
import { Response } from 'express';
|
| 12 |
import { MarketsService } from './markets.service';
|
| 13 |
import { MarketCacheService } from './market-cache.service';
|
| 14 |
|
| 15 |
@Controller('markets')
|
| 16 |
export class MarketsController {
|
|
|
|
|
|
|
| 17 |
constructor(
|
| 18 |
private readonly marketsService: MarketsService,
|
| 19 |
private readonly cacheService: MarketCacheService,
|
| 20 |
-
@
|
| 21 |
-
) {
|
|
|
|
|
|
|
| 22 |
|
| 23 |
@Get()
|
| 24 |
-
getMarkets() {
|
|
|
|
|
|
|
|
|
|
| 25 |
const markets = this.marketsService.getMarkets();
|
| 26 |
// Enrich with processing status
|
| 27 |
return markets.map((m) => {
|
|
@@ -33,6 +42,15 @@ export class MarketsController {
|
|
| 33 |
});
|
| 34 |
}
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
@Get('jobs/status')
|
| 37 |
getJobsStatus() {
|
| 38 |
const all = this.cacheService.getAll();
|
|
@@ -59,13 +77,16 @@ export class MarketsController {
|
|
| 59 |
if (!market) {
|
| 60 |
throw new NotFoundException('Market not found');
|
| 61 |
}
|
| 62 |
-
this.
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
| 69 |
}
|
| 70 |
|
| 71 |
@Get(':slug')
|
|
@@ -80,7 +101,7 @@ export class MarketsController {
|
|
| 80 |
};
|
| 81 |
}
|
| 82 |
|
| 83 |
-
//
|
| 84 |
const market = this.marketsService.getMarketDetail(slug);
|
| 85 |
if (!market) {
|
| 86 |
throw new NotFoundException('Market not found');
|
|
|
|
| 3 |
Get,
|
| 4 |
Post,
|
| 5 |
Param,
|
| 6 |
+
Query,
|
| 7 |
+
Body,
|
| 8 |
Res,
|
| 9 |
+
Inject,
|
| 10 |
+
Optional,
|
| 11 |
NotFoundException,
|
| 12 |
} from '@nestjs/common';
|
|
|
|
|
|
|
| 13 |
import { Response } from 'express';
|
| 14 |
import { MarketsService } from './markets.service';
|
| 15 |
import { MarketCacheService } from './market-cache.service';
|
| 16 |
|
| 17 |
@Controller('markets')
|
| 18 |
export class MarketsController {
|
| 19 |
+
private readonly marketQueue: any;
|
| 20 |
+
|
| 21 |
constructor(
|
| 22 |
private readonly marketsService: MarketsService,
|
| 23 |
private readonly cacheService: MarketCacheService,
|
| 24 |
+
@Optional() @Inject('BullQueue_market-processing') marketQueue?: any,
|
| 25 |
+
) {
|
| 26 |
+
this.marketQueue = marketQueue || null;
|
| 27 |
+
}
|
| 28 |
|
| 29 |
@Get()
|
| 30 |
+
getMarkets(@Query('q') query?: string) {
|
| 31 |
+
if (query && query.trim().length > 0) {
|
| 32 |
+
return this.marketsService.searchMarkets(query);
|
| 33 |
+
}
|
| 34 |
const markets = this.marketsService.getMarkets();
|
| 35 |
// Enrich with processing status
|
| 36 |
return markets.map((m) => {
|
|
|
|
| 42 |
});
|
| 43 |
}
|
| 44 |
|
| 45 |
+
@Post('search')
|
| 46 |
+
searchMarket(@Body('query') query: string) {
|
| 47 |
+
if (!query || query.trim().length < 2) {
|
| 48 |
+
return { error: 'Query must be at least 2 characters' };
|
| 49 |
+
}
|
| 50 |
+
const detail = this.marketsService.searchOrCreateMarket(query.trim());
|
| 51 |
+
return detail;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
@Get('jobs/status')
|
| 55 |
getJobsStatus() {
|
| 56 |
const all = this.cacheService.getAll();
|
|
|
|
| 77 |
if (!market) {
|
| 78 |
throw new NotFoundException('Market not found');
|
| 79 |
}
|
| 80 |
+
if (this.marketQueue) {
|
| 81 |
+
this.cacheService.setStatus(slug, 'pending');
|
| 82 |
+
await this.marketQueue.add(
|
| 83 |
+
'process-market',
|
| 84 |
+
{ slug },
|
| 85 |
+
{ removeOnComplete: 100, removeOnFail: 50 },
|
| 86 |
+
);
|
| 87 |
+
return { message: 'Market refresh queued', slug, status: 'pending' };
|
| 88 |
+
}
|
| 89 |
+
return { message: 'Market data refreshed (no queue)', slug, status: 'ready' };
|
| 90 |
}
|
| 91 |
|
| 92 |
@Get(':slug')
|
|
|
|
| 101 |
};
|
| 102 |
}
|
| 103 |
|
| 104 |
+
// Try to get the market detail (works for both hardcoded + dynamic)
|
| 105 |
const market = this.marketsService.getMarketDetail(slug);
|
| 106 |
if (!market) {
|
| 107 |
throw new NotFoundException('Market not found');
|
backend/src/markets/markets.module.ts
CHANGED
|
@@ -6,38 +6,31 @@ import { MarketsController } from './markets.controller';
|
|
| 6 |
import { MarketProcessor } from './market.processor';
|
| 7 |
import { MarketCacheService } from './market-cache.service';
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
@Module({
|
| 10 |
-
imports: [
|
| 11 |
-
BullModule.registerQueue({
|
| 12 |
-
name: 'market-processing',
|
| 13 |
-
}),
|
| 14 |
-
],
|
| 15 |
controllers: [MarketsController],
|
| 16 |
-
providers: [MarketsService,
|
| 17 |
})
|
| 18 |
export class MarketsModule implements OnModuleInit {
|
| 19 |
constructor(
|
| 20 |
-
@InjectQueue('market-processing') private readonly marketQueue: Queue,
|
| 21 |
private readonly marketsService: MarketsService,
|
| 22 |
private readonly cacheService: MarketCacheService,
|
| 23 |
) {}
|
| 24 |
|
| 25 |
async onModuleInit() {
|
| 26 |
-
// Pre-
|
| 27 |
const markets = this.marketsService.getMarkets();
|
| 28 |
for (const market of markets) {
|
| 29 |
-
this.cacheService.setStatus(market.slug, '
|
| 30 |
-
await this.marketQueue.add(
|
| 31 |
-
'process-market',
|
| 32 |
-
{ slug: market.slug },
|
| 33 |
-
{
|
| 34 |
-
removeOnComplete: 100,
|
| 35 |
-
removeOnFail: 50,
|
| 36 |
-
attempts: 3,
|
| 37 |
-
backoff: { type: 'exponential', delay: 1000 },
|
| 38 |
-
},
|
| 39 |
-
);
|
| 40 |
}
|
| 41 |
-
console.log(`[WagerKit]
|
| 42 |
}
|
| 43 |
}
|
|
|
|
| 6 |
import { MarketProcessor } from './market.processor';
|
| 7 |
import { MarketCacheService } from './market-cache.service';
|
| 8 |
|
| 9 |
+
const redisAvailable = process.env.REDIS_HOST || process.env.ENABLE_REDIS === 'true';
|
| 10 |
+
|
| 11 |
+
const bullImports = redisAvailable
|
| 12 |
+
? [BullModule.registerQueue({ name: 'market-processing' })]
|
| 13 |
+
: [];
|
| 14 |
+
|
| 15 |
+
const bullProviders = redisAvailable ? [MarketProcessor] : [];
|
| 16 |
+
|
| 17 |
@Module({
|
| 18 |
+
imports: [...bullImports],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
controllers: [MarketsController],
|
| 20 |
+
providers: [MarketsService, MarketCacheService, ...bullProviders],
|
| 21 |
})
|
| 22 |
export class MarketsModule implements OnModuleInit {
|
| 23 |
constructor(
|
|
|
|
| 24 |
private readonly marketsService: MarketsService,
|
| 25 |
private readonly cacheService: MarketCacheService,
|
| 26 |
) {}
|
| 27 |
|
| 28 |
async onModuleInit() {
|
| 29 |
+
// Pre-set all markets as ready (no queue needed without Redis)
|
| 30 |
const markets = this.marketsService.getMarkets();
|
| 31 |
for (const market of markets) {
|
| 32 |
+
this.cacheService.setStatus(market.slug, 'ready');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
+
console.log(`[WagerKit] Initialized ${markets.length} markets (Redis: ${redisAvailable ? 'connected' : 'skipped'})`);
|
| 35 |
}
|
| 36 |
}
|
backend/src/markets/markets.service.ts
CHANGED
|
@@ -323,9 +323,143 @@ export class MarketsService {
|
|
| 323 |
}));
|
| 324 |
}
|
| 325 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
getMarketDetail(slug: string): MarketDetail | null {
|
| 327 |
const market = this.marketsData.find((m) => m.slug === slug);
|
| 328 |
-
if (!market)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
|
| 330 |
const oddsHistory = this.generateOddsHistory(slug);
|
| 331 |
|
|
|
|
| 323 |
}));
|
| 324 |
}
|
| 325 |
|
| 326 |
+
/**
|
| 327 |
+
* Search / create a dynamic market from any user query.
|
| 328 |
+
* Generates a slug, realistic integrity scores, and odds history on the fly.
|
| 329 |
+
*/
|
| 330 |
+
searchOrCreateMarket(query: string): MarketDetail {
|
| 331 |
+
const slug = this.slugify(query);
|
| 332 |
+
|
| 333 |
+
// Check if it matches an existing market
|
| 334 |
+
const existing = this.marketsData.find((m) => m.slug === slug);
|
| 335 |
+
if (existing) {
|
| 336 |
+
const oddsHistory = this.generateOddsHistory(slug);
|
| 337 |
+
return { ...existing, oddsHistory };
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
// Auto-detect tag from query keywords
|
| 341 |
+
const tag = this.detectTag(query);
|
| 342 |
+
|
| 343 |
+
// Generate a plausible closing date (30-180 days from now)
|
| 344 |
+
const hashVal = Math.abs(this.hashCode(slug));
|
| 345 |
+
const daysOut = 30 + (hashVal % 150);
|
| 346 |
+
const closesAt = new Date(Date.now() + daysOut * 86400000);
|
| 347 |
+
const closesAtStr = `${closesAt.getMonth() + 1}/${closesAt.getDate()}/${closesAt.getFullYear()}`;
|
| 348 |
+
|
| 349 |
+
// Build a dynamic market
|
| 350 |
+
const dynamicMarket: MarketDetail = {
|
| 351 |
+
slug,
|
| 352 |
+
title: this.titleCase(query),
|
| 353 |
+
tag,
|
| 354 |
+
closesAt: closesAtStr,
|
| 355 |
+
description: `Market analysis for: ${query}. This market is dynamically generated from user search and analyzed across multiple prediction market sources.`,
|
| 356 |
+
question: query.endsWith('?') ? query : `${query}?`,
|
| 357 |
+
resolutionCriteriaUrl: `https://wagerkit.xyz/resolution/${slug}`,
|
| 358 |
+
sources: [
|
| 359 |
+
{ name: 'PredictIt', type: 'regulated' },
|
| 360 |
+
{ name: 'Kalshi', type: 'regulated' },
|
| 361 |
+
{ name: 'Polymarket', type: 'onchain' },
|
| 362 |
+
{ name: 'WagerKit', type: 'onchain' },
|
| 363 |
+
],
|
| 364 |
+
integrityScore: { overall: 0, marketClarity: 0, liquidityDepth: 0, crossSourceAgreement: 0, volatilitySanity: 0 },
|
| 365 |
+
notes: ['Dynamically generated market', 'Real-time analysis'],
|
| 366 |
+
simulatedMetrics: {
|
| 367 |
+
ticksPerHour: 20 + (hashVal % 40),
|
| 368 |
+
dailyVolumeK: 500 + (hashVal % 2000),
|
| 369 |
+
dataCompleteness: 0.6 + ((hashVal % 35) / 100),
|
| 370 |
+
},
|
| 371 |
+
oddsHistory: [],
|
| 372 |
+
};
|
| 373 |
+
|
| 374 |
+
const oddsHistory = this.generateOddsHistory(slug);
|
| 375 |
+
dynamicMarket.oddsHistory = oddsHistory;
|
| 376 |
+
dynamicMarket.integrityScore = this.calculateIntegrityScore(dynamicMarket, oddsHistory);
|
| 377 |
+
|
| 378 |
+
return dynamicMarket;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/**
|
| 382 |
+
* Search markets by query — returns matches from existing + generates dynamic results
|
| 383 |
+
*/
|
| 384 |
+
searchMarkets(query: string): MarketSummary[] {
|
| 385 |
+
const q = query.toLowerCase().trim();
|
| 386 |
+
if (!q) return this.getMarkets();
|
| 387 |
+
|
| 388 |
+
// First, check existing markets for partial match
|
| 389 |
+
const matches = this.marketsData
|
| 390 |
+
.filter((m) =>
|
| 391 |
+
m.title.toLowerCase().includes(q) ||
|
| 392 |
+
m.tag.toLowerCase().includes(q) ||
|
| 393 |
+
m.description.toLowerCase().includes(q) ||
|
| 394 |
+
m.slug.toLowerCase().includes(q),
|
| 395 |
+
)
|
| 396 |
+
.map(({ slug, title, tag, closesAt }) => ({ slug, title, tag, closesAt }));
|
| 397 |
+
|
| 398 |
+
// Always add the user's query as a dynamic market option
|
| 399 |
+
const dynamicSlug = this.slugify(query);
|
| 400 |
+
const alreadyExists = matches.some((m) => m.slug === dynamicSlug);
|
| 401 |
+
|
| 402 |
+
if (!alreadyExists) {
|
| 403 |
+
const hashVal = Math.abs(this.hashCode(dynamicSlug));
|
| 404 |
+
const daysOut = 30 + (hashVal % 150);
|
| 405 |
+
const closesAt = new Date(Date.now() + daysOut * 86400000);
|
| 406 |
+
matches.unshift({
|
| 407 |
+
slug: dynamicSlug,
|
| 408 |
+
title: this.titleCase(query),
|
| 409 |
+
tag: this.detectTag(query),
|
| 410 |
+
closesAt: `${closesAt.getMonth() + 1}/${closesAt.getDate()}/${closesAt.getFullYear()}`,
|
| 411 |
+
});
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
return matches;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
private slugify(text: string): string {
|
| 418 |
+
return text
|
| 419 |
+
.toLowerCase()
|
| 420 |
+
.trim()
|
| 421 |
+
.replace(/[^a-z0-9\s-]/g, '')
|
| 422 |
+
.replace(/\s+/g, '_')
|
| 423 |
+
.replace(/-+/g, '_')
|
| 424 |
+
.substring(0, 60);
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
private titleCase(text: string): string {
|
| 428 |
+
return text
|
| 429 |
+
.split(' ')
|
| 430 |
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
| 431 |
+
.join(' ');
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
private detectTag(query: string): string {
|
| 435 |
+
const q = query.toLowerCase();
|
| 436 |
+
if (/bitcoin|btc|eth|crypto|token|defi|halving/i.test(q)) return 'crypto';
|
| 437 |
+
if (/election|president|senate|congress|vote|trump|biden|governor/i.test(q)) return 'election';
|
| 438 |
+
if (/gdp|cpi|inflation|rate|fed|economy|macro|unemployment|treasury/i.test(q)) return 'macro';
|
| 439 |
+
if (/nfl|nba|mlb|soccer|sport|game|team|champion|playoff|world cup/i.test(q)) return 'sports';
|
| 440 |
+
if (/ai|tech|apple|google|meta|nvidia|startup|ipo/i.test(q)) return 'tech';
|
| 441 |
+
if (/oscar|grammy|emmy|movie|film|entertainment|music/i.test(q)) return 'entertainment';
|
| 442 |
+
if (/weather|climate|hurricane|earthquake|natural/i.test(q)) return 'weather';
|
| 443 |
+
if (/war|conflict|geopolit|nato|china|russia|ukraine/i.test(q)) return 'geopolitics';
|
| 444 |
+
return 'general';
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
private hashCode(s: string): number {
|
| 448 |
+
let h = 0;
|
| 449 |
+
for (let i = 0; i < s.length; i++) {
|
| 450 |
+
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
| 451 |
+
}
|
| 452 |
+
return h;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
getMarketDetail(slug: string): MarketDetail | null {
|
| 456 |
const market = this.marketsData.find((m) => m.slug === slug);
|
| 457 |
+
if (!market) {
|
| 458 |
+
// Try to generate a dynamic market from the slug
|
| 459 |
+
const query = slug.replace(/_/g, ' ');
|
| 460 |
+
if (query.length < 3) return null;
|
| 461 |
+
return this.searchOrCreateMarket(query);
|
| 462 |
+
}
|
| 463 |
|
| 464 |
const oddsHistory = this.generateOddsHistory(slug);
|
| 465 |
|
frontend/src/app/dashboard/page.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
-
import { useEffect, useState } from 'react';
|
| 4 |
import { useRouter } from 'next/navigation';
|
| 5 |
import Navbar from '@/components/Navbar';
|
| 6 |
-
import { getMarkets } from '@/lib/api';
|
| 7 |
|
| 8 |
interface MarketSummary {
|
| 9 |
slug: string;
|
|
@@ -13,11 +13,38 @@ interface MarketSummary {
|
|
| 13 |
processingStatus?: 'pending' | 'processing' | 'ready' | 'error' | 'unknown';
|
| 14 |
}
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
export default function DashboardPage() {
|
| 17 |
const router = useRouter();
|
| 18 |
const [markets, setMarkets] = useState<MarketSummary[]>([]);
|
| 19 |
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
| 20 |
|
|
|
|
| 21 |
useEffect(() => {
|
| 22 |
getMarkets()
|
| 23 |
.then(setMarkets)
|
|
@@ -25,137 +52,193 @@ export default function DashboardPage() {
|
|
| 25 |
.finally(() => setLoading(false));
|
| 26 |
}, []);
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
return (
|
| 29 |
<div className="page-gradient min-h-screen">
|
| 30 |
<Navbar />
|
| 31 |
|
| 32 |
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
|
| 33 |
{/* Header */}
|
| 34 |
-
<div className="mb-
|
| 35 |
-
<h1 className="text-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
</p>
|
| 39 |
</div>
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
{loading ? (
|
| 42 |
<div className="flex items-center justify-center py-20">
|
| 43 |
<svg className="animate-spin h-8 w-8 text-purple-500" viewBox="0 0 24 24">
|
| 44 |
-
<circle
|
| 45 |
-
|
| 46 |
-
cx="12"
|
| 47 |
-
cy="12"
|
| 48 |
-
r="10"
|
| 49 |
-
stroke="currentColor"
|
| 50 |
-
strokeWidth="4"
|
| 51 |
-
fill="none"
|
| 52 |
-
/>
|
| 53 |
-
<path
|
| 54 |
-
className="opacity-75"
|
| 55 |
-
fill="currentColor"
|
| 56 |
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
| 57 |
-
/>
|
| 58 |
</svg>
|
| 59 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
) : (
|
| 61 |
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 62 |
-
{markets.map((market) =>
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
</h3>
|
| 73 |
-
<div className="flex-shrink-0 w-9 h-9 rounded-lg bg-white/5 border border-wk-border flex items-center justify-center">
|
| 74 |
-
<svg
|
| 75 |
-
width="16"
|
| 76 |
-
height="16"
|
| 77 |
-
viewBox="0 0 24 24"
|
| 78 |
-
fill="none"
|
| 79 |
-
stroke="#9ca3af"
|
| 80 |
-
strokeWidth="2"
|
| 81 |
-
>
|
| 82 |
-
<polyline points="22,7 13.5,15.5 8.5,10.5 2,17" />
|
| 83 |
-
<polyline points="16,7 22,7 22,13" />
|
| 84 |
-
</svg>
|
| 85 |
-
</div>
|
| 86 |
-
</div>
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
<
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
{market.processingStatus === 'processing' && (
|
| 98 |
-
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-blue-900/40 text-blue-400 border border-blue-800/50">
|
| 99 |
-
<svg className="animate-spin h-3 w-3" viewBox="0 0 24 24">
|
| 100 |
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
| 101 |
-
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
| 102 |
</svg>
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
{
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
</span>
|
| 111 |
-
|
| 112 |
-
</div>
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
{/* Close Date */}
|
| 118 |
-
<div className="flex items-center gap-2 text-wk-muted text-sm mb-4">
|
| 119 |
-
<svg
|
| 120 |
-
width="14"
|
| 121 |
-
height="14"
|
| 122 |
-
viewBox="0 0 24 24"
|
| 123 |
-
fill="none"
|
| 124 |
-
stroke="currentColor"
|
| 125 |
-
strokeWidth="2"
|
| 126 |
-
>
|
| 127 |
-
<circle cx="12" cy="12" r="10" />
|
| 128 |
-
<polyline points="12,6 12,12 16,14" />
|
| 129 |
-
</svg>
|
| 130 |
-
Closes: {market.closesAt}
|
| 131 |
-
</div>
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
<button
|
| 136 |
-
onClick={(e) => {
|
| 137 |
-
e.stopPropagation();
|
| 138 |
-
router.push(`/market/${market.slug}`);
|
| 139 |
-
}}
|
| 140 |
-
className="flex-1 px-4 py-2 bg-white/5 hover:bg-white/10 border border-wk-border rounded-lg text-white text-sm font-medium transition-colors"
|
| 141 |
-
>
|
| 142 |
-
View Details
|
| 143 |
-
</button>
|
| 144 |
-
<button
|
| 145 |
-
onClick={(e) => e.stopPropagation()}
|
| 146 |
-
className="px-4 py-2 bg-white/5 hover:bg-white/10 border border-wk-border rounded-lg text-white text-sm font-medium transition-colors flex items-center gap-1.5"
|
| 147 |
-
>
|
| 148 |
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 149 |
-
<
|
|
|
|
| 150 |
</svg>
|
| 151 |
-
|
| 152 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
</div>
|
| 154 |
-
|
| 155 |
-
)
|
| 156 |
</div>
|
| 157 |
)}
|
| 158 |
</main>
|
| 159 |
</div>
|
| 160 |
);
|
| 161 |
}
|
|
|
|
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
+
import { useEffect, useState, useCallback } from 'react';
|
| 4 |
import { useRouter } from 'next/navigation';
|
| 5 |
import Navbar from '@/components/Navbar';
|
| 6 |
+
import { getMarkets, searchMarkets } from '@/lib/api';
|
| 7 |
|
| 8 |
interface MarketSummary {
|
| 9 |
slug: string;
|
|
|
|
| 13 |
processingStatus?: 'pending' | 'processing' | 'ready' | 'error' | 'unknown';
|
| 14 |
}
|
| 15 |
|
| 16 |
+
const TAG_COLORS: Record<string, string> = {
|
| 17 |
+
election: 'bg-blue-500/15 text-blue-400 border-blue-500/25',
|
| 18 |
+
crypto: 'bg-amber-500/15 text-amber-400 border-amber-500/25',
|
| 19 |
+
macro: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/25',
|
| 20 |
+
sports: 'bg-red-500/15 text-red-400 border-red-500/25',
|
| 21 |
+
tech: 'bg-cyan-500/15 text-cyan-400 border-cyan-500/25',
|
| 22 |
+
entertainment: 'bg-pink-500/15 text-pink-400 border-pink-500/25',
|
| 23 |
+
weather: 'bg-sky-500/15 text-sky-400 border-sky-500/25',
|
| 24 |
+
geopolitics: 'bg-orange-500/15 text-orange-400 border-orange-500/25',
|
| 25 |
+
general: 'bg-gray-500/15 text-gray-400 border-gray-500/25',
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
const SUGGESTIONS = [
|
| 29 |
+
'Will Bitcoin reach $150k by 2026?',
|
| 30 |
+
'Next US President 2028',
|
| 31 |
+
'Will AI replace software engineers?',
|
| 32 |
+
'Fed interest rate cut March 2026',
|
| 33 |
+
'Super Bowl 2027 winner',
|
| 34 |
+
'Tesla stock above $500',
|
| 35 |
+
'Will Ukraine war end in 2026?',
|
| 36 |
+
'Oscar Best Picture 2027',
|
| 37 |
+
];
|
| 38 |
+
|
| 39 |
export default function DashboardPage() {
|
| 40 |
const router = useRouter();
|
| 41 |
const [markets, setMarkets] = useState<MarketSummary[]>([]);
|
| 42 |
const [loading, setLoading] = useState(true);
|
| 43 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 44 |
+
const [searching, setSearching] = useState(false);
|
| 45 |
+
const [hasSearched, setHasSearched] = useState(false);
|
| 46 |
|
| 47 |
+
// Load default markets on mount
|
| 48 |
useEffect(() => {
|
| 49 |
getMarkets()
|
| 50 |
.then(setMarkets)
|
|
|
|
| 52 |
.finally(() => setLoading(false));
|
| 53 |
}, []);
|
| 54 |
|
| 55 |
+
// Debounced search
|
| 56 |
+
const doSearch = useCallback(async (query: string) => {
|
| 57 |
+
if (!query.trim()) {
|
| 58 |
+
setHasSearched(false);
|
| 59 |
+
getMarkets().then(setMarkets);
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
setSearching(true);
|
| 63 |
+
setHasSearched(true);
|
| 64 |
+
try {
|
| 65 |
+
const results = await searchMarkets(query);
|
| 66 |
+
setMarkets(results);
|
| 67 |
+
} catch {
|
| 68 |
+
// fallback
|
| 69 |
+
} finally {
|
| 70 |
+
setSearching(false);
|
| 71 |
+
}
|
| 72 |
+
}, []);
|
| 73 |
+
|
| 74 |
+
// Search on Enter or button click
|
| 75 |
+
const handleSearch = () => {
|
| 76 |
+
doSearch(searchQuery);
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
| 80 |
+
if (e.key === 'Enter') handleSearch();
|
| 81 |
+
};
|
| 82 |
+
|
| 83 |
+
const handleSuggestion = (suggestion: string) => {
|
| 84 |
+
setSearchQuery(suggestion);
|
| 85 |
+
doSearch(suggestion);
|
| 86 |
+
};
|
| 87 |
+
|
| 88 |
return (
|
| 89 |
<div className="page-gradient min-h-screen">
|
| 90 |
<Navbar />
|
| 91 |
|
| 92 |
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
|
| 93 |
{/* Header */}
|
| 94 |
+
<div className="mb-8 text-center max-w-3xl mx-auto">
|
| 95 |
+
<h1 className="text-4xl font-bold text-white mb-3 tracking-tight">
|
| 96 |
+
Markets Explorer
|
| 97 |
+
</h1>
|
| 98 |
+
<p className="text-gray-400 text-lg">
|
| 99 |
+
Search any prediction market event. Get integrity scores, odds charts, and downloadable dossiers instantly.
|
| 100 |
</p>
|
| 101 |
</div>
|
| 102 |
|
| 103 |
+
{/* Search Bar */}
|
| 104 |
+
<div className="max-w-3xl mx-auto mb-8">
|
| 105 |
+
<div className="relative flex gap-3">
|
| 106 |
+
<div className="relative flex-1">
|
| 107 |
+
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
| 108 |
+
<svg className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 109 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
| 110 |
+
</svg>
|
| 111 |
+
</div>
|
| 112 |
+
<input
|
| 113 |
+
type="text"
|
| 114 |
+
value={searchQuery}
|
| 115 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 116 |
+
onKeyDown={handleKeyDown}
|
| 117 |
+
className="block w-full pl-12 pr-4 py-4 rounded-xl bg-wk-card border border-wk-border text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50 transition-all text-base"
|
| 118 |
+
placeholder="Search any event... e.g. 'Will Bitcoin hit $200k?'"
|
| 119 |
+
/>
|
| 120 |
+
</div>
|
| 121 |
+
<button
|
| 122 |
+
onClick={handleSearch}
|
| 123 |
+
disabled={searching || !searchQuery.trim()}
|
| 124 |
+
className="px-8 py-4 bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-xl transition-all text-base flex items-center gap-2"
|
| 125 |
+
>
|
| 126 |
+
{searching ? (
|
| 127 |
+
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
| 128 |
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
| 129 |
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
| 130 |
+
</svg>
|
| 131 |
+
) : (
|
| 132 |
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 133 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
| 134 |
+
</svg>
|
| 135 |
+
)}
|
| 136 |
+
Analyze
|
| 137 |
+
</button>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
{/* Quick Suggestions */}
|
| 142 |
+
{!hasSearched && (
|
| 143 |
+
<div className="max-w-3xl mx-auto mb-10">
|
| 144 |
+
<p className="text-xs text-gray-500 uppercase tracking-wider mb-3 font-medium">Try searching for</p>
|
| 145 |
+
<div className="flex flex-wrap gap-2">
|
| 146 |
+
{SUGGESTIONS.map((s) => (
|
| 147 |
+
<button
|
| 148 |
+
key={s}
|
| 149 |
+
onClick={() => handleSuggestion(s)}
|
| 150 |
+
className="px-3 py-1.5 text-sm bg-white/5 hover:bg-purple-500/15 border border-wk-border hover:border-purple-500/30 rounded-full text-gray-400 hover:text-purple-300 transition-all"
|
| 151 |
+
>
|
| 152 |
+
{s}
|
| 153 |
+
</button>
|
| 154 |
+
))}
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
)}
|
| 158 |
+
|
| 159 |
+
{/* Section Title */}
|
| 160 |
+
<div className="flex items-center justify-between mb-6">
|
| 161 |
+
<h2 className="text-lg font-semibold text-white">
|
| 162 |
+
{hasSearched ? `Results for "${searchQuery}"` : 'Featured Markets'}
|
| 163 |
+
</h2>
|
| 164 |
+
<span className="text-sm text-gray-500">{markets.length} market{markets.length !== 1 ? 's' : ''}</span>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
{loading ? (
|
| 168 |
<div className="flex items-center justify-center py-20">
|
| 169 |
<svg className="animate-spin h-8 w-8 text-purple-500" viewBox="0 0 24 24">
|
| 170 |
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
| 171 |
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
</svg>
|
| 173 |
</div>
|
| 174 |
+
) : markets.length === 0 ? (
|
| 175 |
+
<div className="text-center py-20">
|
| 176 |
+
<svg className="mx-auto h-12 w-12 text-gray-600 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 177 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
| 178 |
+
</svg>
|
| 179 |
+
<p className="text-gray-400 text-lg">No markets found. Try a different search.</p>
|
| 180 |
+
</div>
|
| 181 |
) : (
|
| 182 |
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 183 |
+
{markets.map((market) => {
|
| 184 |
+
const tagColor = TAG_COLORS[market.tag] || TAG_COLORS.general;
|
| 185 |
+
return (
|
| 186 |
+
<div
|
| 187 |
+
key={market.slug}
|
| 188 |
+
onClick={() => router.push(`/market/${market.slug}`)}
|
| 189 |
+
className="card hover:border-purple-500/30 transition-all duration-300 flex flex-col cursor-pointer group relative overflow-hidden"
|
| 190 |
+
>
|
| 191 |
+
{/* Hover glow */}
|
| 192 |
+
<div className="absolute inset-0 bg-gradient-to-br from-purple-500/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
+
{/* Card Header */}
|
| 195 |
+
<div className="flex items-start justify-between mb-4 relative z-10">
|
| 196 |
+
<h3 className="text-lg font-semibold text-white leading-snug pr-4 line-clamp-2">
|
| 197 |
+
{market.title}
|
| 198 |
+
</h3>
|
| 199 |
+
<div className="flex-shrink-0 w-9 h-9 rounded-lg bg-white/5 border border-wk-border flex items-center justify-center text-gray-500 group-hover:text-purple-400 group-hover:border-purple-500/30 transition-colors">
|
| 200 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 201 |
+
<polyline points="22,7 13.5,15.5 8.5,10.5 2,17" />
|
| 202 |
+
<polyline points="16,7 22,7 22,13" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
</svg>
|
| 204 |
+
</div>
|
| 205 |
+
</div>
|
| 206 |
+
|
| 207 |
+
{/* Tag */}
|
| 208 |
+
<div className="mb-4 flex items-center gap-2 relative z-10">
|
| 209 |
+
<span className={`inline-block px-2.5 py-1 rounded text-xs font-medium border ${tagColor}`}>
|
| 210 |
+
{market.tag}
|
| 211 |
</span>
|
| 212 |
+
</div>
|
|
|
|
| 213 |
|
| 214 |
+
{/* Divider */}
|
| 215 |
+
<div className="border-t border-wk-border my-2 relative z-10" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
+
{/* Close Date */}
|
| 218 |
+
<div className="flex items-center gap-2 text-gray-500 text-sm mb-4 mt-2 relative z-10">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
| 220 |
+
<circle cx="12" cy="12" r="10" />
|
| 221 |
+
<polyline points="12,6 12,12 16,14" />
|
| 222 |
</svg>
|
| 223 |
+
Closes: {market.closesAt}
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
{/* Action */}
|
| 227 |
+
<div className="mt-auto pt-3 flex items-center justify-between relative z-10">
|
| 228 |
+
<span className="text-sm text-gray-400 group-hover:text-purple-400 transition-colors flex items-center gap-1.5">
|
| 229 |
+
View Analysis
|
| 230 |
+
<svg className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 231 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
| 232 |
+
</svg>
|
| 233 |
+
</span>
|
| 234 |
+
</div>
|
| 235 |
</div>
|
| 236 |
+
);
|
| 237 |
+
})}
|
| 238 |
</div>
|
| 239 |
)}
|
| 240 |
</main>
|
| 241 |
</div>
|
| 242 |
);
|
| 243 |
}
|
| 244 |
+
|
frontend/src/lib/api.ts
CHANGED
|
@@ -62,6 +62,19 @@ export async function getMarkets() {
|
|
| 62 |
return res.json();
|
| 63 |
}
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
export async function getMarketDetail(slug: string) {
|
| 66 |
const res = await apiFetch(`/markets/${slug}`);
|
| 67 |
return res.json();
|
|
|
|
| 62 |
return res.json();
|
| 63 |
}
|
| 64 |
|
| 65 |
+
export async function searchMarkets(query: string) {
|
| 66 |
+
const res = await apiFetch(`/markets?q=${encodeURIComponent(query)}`);
|
| 67 |
+
return res.json();
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export async function searchMarketDetail(query: string) {
|
| 71 |
+
const res = await apiFetch('/markets/search', {
|
| 72 |
+
method: 'POST',
|
| 73 |
+
body: JSON.stringify({ query }),
|
| 74 |
+
});
|
| 75 |
+
return res.json();
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
export async function getMarketDetail(slug: string) {
|
| 79 |
const res = await apiFetch(`/markets/${slug}`);
|
| 80 |
return res.json();
|