dubs-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +280 -0
- package/CLAUDE.md +46 -0
- package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
- package/CURRENT_SESSION.md +171 -0
- package/CURRENT_SESSION_DRAW.md +516 -0
- package/MARCH_MADNESS_SURVIVOR.md +254 -0
- package/PANDA.md +166 -0
- package/Procfile +4 -0
- package/README.md +476 -0
- package/controllers/livescoresController.js +376 -0
- package/controllers/pickemController.js +554 -0
- package/controllers/survivorAdminController.js +887 -0
- package/controllers/survivorController.js +623 -0
- package/cron/oracleMonitor.js +77 -0
- package/cron/pickemOracleMonitor.js +73 -0
- package/data/jackpot-history.json +952 -0
- package/data/ncaaTeams.js +406 -0
- package/documentation/API_SECURITY_GUIDE.md +327 -0
- package/documentation/ARCADE_API.md +593 -0
- package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
- package/documentation/ARCADE_QUICKSTART.md +242 -0
- package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
- package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
- package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
- package/documentation/CLAIM_STATUS_FIX.md +67 -0
- package/documentation/CLI_TOOL_GUIDE.md +372 -0
- package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
- package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
- package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
- package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
- package/documentation/DATABASE_QUERIES.md +269 -0
- package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
- package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
- package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
- package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
- package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
- package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
- package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
- package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
- package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
- package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
- package/documentation/HEROKU_DEPLOYMENT.md +134 -0
- package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
- package/documentation/JACKPOT_API.md +521 -0
- package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
- package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
- package/documentation/JWT_QUICK_SETUP.md +268 -0
- package/documentation/JWT_TESTING_GUIDE.md +404 -0
- package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
- package/documentation/KEEPER_SETUP.md +206 -0
- package/documentation/KEEPER_STATE_MACHINE.md +423 -0
- package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
- package/documentation/LOCAL_VOTING_TEST.md +279 -0
- package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
- package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
- package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
- package/documentation/PNL_TRACKING_SETUP.md +189 -0
- package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
- package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
- package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
- package/documentation/QUICK_AUTH_SETUP.md +99 -0
- package/documentation/QUICK_DEPLOY.md +224 -0
- package/documentation/QUICK_FIX.md +114 -0
- package/documentation/QUICK_START.md +152 -0
- package/documentation/REFEREE_MODE_GUIDE.md +392 -0
- package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
- package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
- package/documentation/RUN_MIGRATION_NOW.md +39 -0
- package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
- package/documentation/SETUP_GUIDE.md +184 -0
- package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
- package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
- package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
- package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
- package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
- package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
- package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
- package/ecosystem.config.js +65 -0
- package/env.template +125 -0
- package/middleware/apiKeyAuth.js +136 -0
- package/middleware/authenticate.js +214 -0
- package/middleware/developerUserAuth.js +76 -0
- package/middleware/socketAuth.js +69 -0
- package/package.json +49 -0
- package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
- package/postman/Dubs-API-v1.postman_collection.json +205 -0
- package/postman/Dubs_Developer_API.postman_collection.json +662 -0
- package/postman/QUICKSTART.md +118 -0
- package/postman/QUICK_REFERENCE.md +246 -0
- package/postman/README.md +71 -0
- package/postman/VOTING_API_GUIDE.md +426 -0
- package/refactor/Animations.md +148 -0
- package/refactor/Chat.md +252 -0
- package/routes/actionsRoutes.js +699 -0
- package/routes/adminRoutes.js +370 -0
- package/routes/analyticsRoutes.js +1262 -0
- package/routes/arcadeRoutes.js +557 -0
- package/routes/authRoutes.js +2310 -0
- package/routes/avatarRoutes.js +85 -0
- package/routes/botRoutes.js +211 -0
- package/routes/chatRoutes.js +377 -0
- package/routes/cryptoPriceRoutes.js +105 -0
- package/routes/developerRoutes.js +4201 -0
- package/routes/deviceRoutes.js +214 -0
- package/routes/dmRoutes.js +167 -0
- package/routes/esportsRoutes.js +806 -0
- package/routes/exchangeRateRoutes.js +233 -0
- package/routes/gamesRoutes.js +3028 -0
- package/routes/jackpotRoutes.js +754 -0
- package/routes/keeperMonitoringRoutes.js +156 -0
- package/routes/keeperWebhookRoutes.js +466 -0
- package/routes/livescoresRoutes.js +31 -0
- package/routes/pickemAdminRoutes.js +199 -0
- package/routes/pickemRoutes.js +231 -0
- package/routes/playerStatsRoutes.js +147 -0
- package/routes/portfolioRoutes.js +217 -0
- package/routes/promoRoutes.js +418 -0
- package/routes/referralEarningsRoutes.js +392 -0
- package/routes/socialRoutes.js +459 -0
- package/routes/sportsRoutes.js +1271 -0
- package/routes/survivorAdminRoutes.js +345 -0
- package/routes/survivorRoutes.js +756 -0
- package/routes/uploadRoutes.js +256 -0
- package/routes/userProfileRoutes.js +244 -0
- package/routes/whatsNewRoutes.js +331 -0
- package/scripts/.claude/settings.local.json +15 -0
- package/scripts/README.md +170 -0
- package/scripts/RESTART_EVERYTHING.sh +104 -0
- package/scripts/add-claim-columns.sql +48 -0
- package/scripts/add-crypto-prices-cache.sql +27 -0
- package/scripts/add-exchange-rates-cache.sql +40 -0
- package/scripts/add-game-invite-column.sql +23 -0
- package/scripts/add-game-invite-notification.sql +33 -0
- package/scripts/add-game-invite-telegram-pref.sql +16 -0
- package/scripts/add-game-joined-notification.sql +16 -0
- package/scripts/add-game-joined-pref.js +40 -0
- package/scripts/add-game-joined-preference.sql +6 -0
- package/scripts/add-game-start-notifications.sql +41 -0
- package/scripts/add-notification-flags-to-games.sql +55 -0
- package/scripts/add-pending-game-dismissals.sql +19 -0
- package/scripts/add-preferred-currency.sql +34 -0
- package/scripts/add-winner-columns.js +61 -0
- package/scripts/add_mention_system.sql +53 -0
- package/scripts/add_payment_system.sql +96 -0
- package/scripts/add_sports_event_id_column.sql +22 -0
- package/scripts/analyze-cohort-data-heroku.js +276 -0
- package/scripts/analyze-cohort-data.js +295 -0
- package/scripts/analyze-prod-cohorts.sh +10 -0
- package/scripts/backfill-matchup-images.js +245 -0
- package/scripts/backfill-missing-signatures.js +175 -0
- package/scripts/backfill-referral-earnings.js +202 -0
- package/scripts/check-chat-schema.js +130 -0
- package/scripts/check-db.sh +14 -0
- package/scripts/check_oracle_in_game.js +54 -0
- package/scripts/cleanup-database.js +193 -0
- package/scripts/clear-notification-cache.js +85 -0
- package/scripts/convert-mnemonic.js +50 -0
- package/scripts/create-users-table.sql +44 -0
- package/scripts/debug-cohort-counts.js +248 -0
- package/scripts/debug-winner-calc.js +84 -0
- package/scripts/deploy-payment-system.sh +118 -0
- package/scripts/deploy-to-heroku.sh +63 -0
- package/scripts/diagnose-locked-round.js +143 -0
- package/scripts/dubs-cli.js +720 -0
- package/scripts/dump-account.js +65 -0
- package/scripts/find-vrf-offset.js +48 -0
- package/scripts/fix-chat-notifications-constraint.sql +122 -0
- package/scripts/fix-claim-columns.js +124 -0
- package/scripts/fix-constraint-now.js +44 -0
- package/scripts/fix-lock-timestamps.js +96 -0
- package/scripts/fix-locked-round.sh +126 -0
- package/scripts/fix-missing-badges.sql +91 -0
- package/scripts/fix-payment-notifications.sql +41 -0
- package/scripts/force-new-round.js +55 -0
- package/scripts/force-resolve-and-claim.js +278 -0
- package/scripts/important/README.md +115 -0
- package/scripts/important/authority-force-lock.js +197 -0
- package/scripts/important/authority-resolve-game.js +267 -0
- package/scripts/important/check-game-status.js +373 -0
- package/scripts/important/list-pending-games-by-version.js +270 -0
- package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
- package/scripts/initialize-jackpot.js +111 -0
- package/scripts/jackpot/.claude/settings.local.json +10 -0
- package/scripts/jackpot/force-reset.js +84 -0
- package/scripts/jackpot/initialize-mainnet.js +100 -0
- package/scripts/jackpot/keeper.js +742 -0
- package/scripts/jackpot/status.js +107 -0
- package/scripts/jackpot/update-round-duration.js +143 -0
- package/scripts/keeper-bot.js +112 -0
- package/scripts/list-pending-games.js +131 -0
- package/scripts/migrate-chat-v2.js +127 -0
- package/scripts/migrate-chat-winners.js +84 -0
- package/scripts/migrate-chat.sh +17 -0
- package/scripts/migrate-game-invite.js +83 -0
- package/scripts/migrate-heroku-game-notifications.sh +159 -0
- package/scripts/migrations/001_analytics_tables.sql +422 -0
- package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
- package/scripts/migrations/003_referral_earnings.sql +208 -0
- package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
- package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
- package/scripts/migrations/005_push_notifications.sql +55 -0
- package/scripts/migrations/006_add_draw_team_players.sql +28 -0
- package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
- package/scripts/migrations/007_add_gif_url.sql +8 -0
- package/scripts/migrations/008_add_connect4_columns.sql +139 -0
- package/scripts/migrations/008_add_pool_tracking.sql +22 -0
- package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
- package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
- package/scripts/migrations/011_create_developer_tables.sql +67 -0
- package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
- package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
- package/scripts/migrations/013_add_network_mode.sql +18 -0
- package/scripts/migrations/014_create_developer_app_users.sql +19 -0
- package/scripts/migrations/015_add_ui_config.sql +4 -0
- package/scripts/migrations/016_add_resolution_secret.sql +4 -0
- package/scripts/migrations/017_add_external_game_id.sql +3 -0
- package/scripts/migrations/018_create_pickem_tables.sql +115 -0
- package/scripts/migrations/019_expo_push_tokens.sql +19 -0
- package/scripts/migrations/create_whats_new_tables.sql +88 -0
- package/scripts/migrations/drop_live_games_tables.sql +34 -0
- package/scripts/open-jackpot-round.js +85 -0
- package/scripts/purge-all-data.sh +329 -0
- package/scripts/purge-all-data.sql +142 -0
- package/scripts/purge-heroku-data.sh +149 -0
- package/scripts/purge-heroku-data.sql +62 -0
- package/scripts/rebuild-heroku-database.sh +113 -0
- package/scripts/recover-funds.js +357 -0
- package/scripts/regenerate-epl-images.js +278 -0
- package/scripts/resize-s3-matchup-images.js +374 -0
- package/scripts/resolve-direct.js +88 -0
- package/scripts/resolve-mock-game.js +124 -0
- package/scripts/resolve-pickem-game.js +55 -0
- package/scripts/resolve-round-manual.js +83 -0
- package/scripts/resolve-stuck-game.js +382 -0
- package/scripts/resolve-stuck-round.js +42 -0
- package/scripts/run-connect4-migration.sh +16 -0
- package/scripts/run-mention-migration.sh +32 -0
- package/scripts/run-payment-migration.sh +51 -0
- package/scripts/run-preferred-currency-migration.sh +31 -0
- package/scripts/run-referral-earnings-migration.sh +32 -0
- package/scripts/run-survivor-outcome-migration.sh +16 -0
- package/scripts/seed-test-users.js +346 -0
- package/scripts/setup-auth-tables.js +78 -0
- package/scripts/setup-complete-database.sql +992 -0
- package/scripts/setup-database-fresh.sh +359 -0
- package/scripts/setup-heroku-keeper.sh +48 -0
- package/scripts/setup-keeper-database.js +83 -0
- package/scripts/setup-keeper-state-db.sql +110 -0
- package/scripts/setup-oracle.sh +39 -0
- package/scripts/setup-pnl-tracking.js +111 -0
- package/scripts/start-devnet.sh +14 -0
- package/scripts/test-arcade-devnet.sh +160 -0
- package/scripts/test-arcade-match.sh +109 -0
- package/scripts/test-automatic-mode.sh +239 -0
- package/scripts/test-connect4-cancel-claim.js +370 -0
- package/scripts/test-connect4-e2e.js +369 -0
- package/scripts/test-connect4-resolve.js +369 -0
- package/scripts/test-game-state-endpoint.js +136 -0
- package/scripts/test-invite-notification.js +86 -0
- package/scripts/test-jackpot-api.sh +71 -0
- package/scripts/test-poll-confirmation.js +267 -0
- package/scripts/test-resolve-game.js +271 -0
- package/scripts/test-resolve-signature.js +223 -0
- package/scripts/test-signature-preservation.js +124 -0
- package/scripts/test-state-machine.js +291 -0
- package/scripts/test-webhook-receiver.js +60 -0
- package/scripts/update-notification-constraint.js +52 -0
- package/scripts/verify-account-layout.js +145 -0
- package/scripts/verify-winner-algorithm.js +278 -0
- package/server.js +5259 -0
- package/services/arcadeMatchService.js +763 -0
- package/services/automaticGameOracle.js +1596 -0
- package/services/chatService.js +1612 -0
- package/services/connect4GameService.js +1049 -0
- package/services/connect4NotificationService.js +374 -0
- package/services/cryptoPriceService.js +223 -0
- package/services/customGameResolver.js +260 -0
- package/services/db.js +79 -0
- package/services/directMessageService.js +389 -0
- package/services/discordNotifications.js +160 -0
- package/services/exchangeRateService.js +289 -0
- package/services/expoPushService.js +314 -0
- package/services/gamesCacheService.js +539 -0
- package/services/jackpotHistory.js +331 -0
- package/services/jackpotService.js +856 -0
- package/services/keeperStateService.js +355 -0
- package/services/matchupImageService.js +591 -0
- package/services/notificationCacheService.js +407 -0
- package/services/pickemOracle.js +440 -0
- package/services/playerStatsService.js +389 -0
- package/services/portfolioService.js +555 -0
- package/services/promoService.js +757 -0
- package/services/promoTreasuryService.js +239 -0
- package/services/pushNotifications.js +353 -0
- package/services/redisService.js +422 -0
- package/services/referralEarningsService.js +728 -0
- package/services/s3Service.js +396 -0
- package/services/socialService.js +1202 -0
- package/services/survivorOracle.js +469 -0
- package/services/survivorSimulator.js +475 -0
- package/services/telegramNotifications.js +461 -0
- package/services/userProfileStatsService.js +1185 -0
- package/services/whatsNewService.js +388 -0
- package/utils/urlHelper.js +95 -0
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const axios = require('axios');
|
|
4
|
+
const theSportsDB = require('thesportsdb');
|
|
5
|
+
|
|
6
|
+
// Set the API key
|
|
7
|
+
const API_KEY = '819154';
|
|
8
|
+
theSportsDB.setApiKey(API_KEY);
|
|
9
|
+
|
|
10
|
+
// League IDs for our sports
|
|
11
|
+
const LEAGUE_IDS = {
|
|
12
|
+
NBA: 4387,
|
|
13
|
+
NHL: 4380,
|
|
14
|
+
MLB: 4424,
|
|
15
|
+
NFL: 4391,
|
|
16
|
+
EPL: 4328,
|
|
17
|
+
UFC: 4443,
|
|
18
|
+
NCAAF: 4479, // NCAA Division 1 College Football
|
|
19
|
+
NCAAB: 4607 // NCAA Division I Basketball Mens
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ESPN Rankings URLs
|
|
23
|
+
const ESPN_RANKINGS_URLS = {
|
|
24
|
+
NCAAF: 'http://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings',
|
|
25
|
+
NCAAB: 'http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/rankings'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @route GET /api/sports/leagues
|
|
30
|
+
* @desc Get all available leagues
|
|
31
|
+
* @access Public
|
|
32
|
+
*/
|
|
33
|
+
router.get('/leagues', async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
const data = await theSportsDB.getLeagueList();
|
|
36
|
+
res.json({ success: true, data });
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Error fetching leagues:', error);
|
|
39
|
+
res.status(500).json({ success: false, error: 'Failed to fetch leagues' });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @route GET /api/sports/league/:id
|
|
45
|
+
* @desc Get details for a specific league
|
|
46
|
+
* @access Public
|
|
47
|
+
*/
|
|
48
|
+
router.get('/league/:id', async (req, res) => {
|
|
49
|
+
try {
|
|
50
|
+
const { id } = req.params;
|
|
51
|
+
const data = await theSportsDB.getLeagueDetailsById(id);
|
|
52
|
+
res.json({ success: true, data });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`Error fetching league ${req.params.id}:`, error);
|
|
55
|
+
res.status(500).json({ success: false, error: 'Failed to fetch league details' });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @route GET /api/sports/teams/:league
|
|
61
|
+
* @desc Get teams for a specific league (NBA, NHL, MLB)
|
|
62
|
+
* @access Public
|
|
63
|
+
*/
|
|
64
|
+
router.get('/teams/:league', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const { league } = req.params;
|
|
67
|
+
const leagueId = LEAGUE_IDS[league.toUpperCase()];
|
|
68
|
+
|
|
69
|
+
if (!leagueId) {
|
|
70
|
+
return res.status(400).json({
|
|
71
|
+
success: false,
|
|
72
|
+
error: 'Invalid league. Please use NBA, NHL, MLB, NFL, EPL, UFC, NCAAF, or NCAAB.'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Get league details first to get the league name
|
|
77
|
+
const leagueDetails = await theSportsDB.getLeagueDetailsById(leagueId);
|
|
78
|
+
if (!leagueDetails.leagues || leagueDetails.leagues.length === 0) {
|
|
79
|
+
return res.status(404).json({ success: false, error: 'League not found' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const leagueName = leagueDetails.leagues[0].strLeague;
|
|
83
|
+
const teams = await theSportsDB.getTeamsByLeagueName(leagueName);
|
|
84
|
+
|
|
85
|
+
res.json({ success: true, data: teams });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(`Error fetching teams for ${req.params.league}:`, error);
|
|
88
|
+
res.status(500).json({ success: false, error: 'Failed to fetch teams' });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
94
|
+
* 🥊 UFC STUB EVENTS - ISOLATED DATA SOURCE
|
|
95
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
96
|
+
* TheSportsDB has unreliable/empty UFC data, so we maintain curated stub data.
|
|
97
|
+
* This function is completely isolated from other sports data sources.
|
|
98
|
+
*
|
|
99
|
+
* To update UFC events:
|
|
100
|
+
* 1. Add new events to the array below
|
|
101
|
+
* 2. Upload fighter images to S3 (dubs-avatars-prod/UFC/)
|
|
102
|
+
* 3. Update strHomeTeamBadge and strAwayTeamBadge URLs
|
|
103
|
+
*
|
|
104
|
+
* Structure matches TheSportsDB format exactly for oracle compatibility.
|
|
105
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
106
|
+
*/
|
|
107
|
+
function getUFCStubEvents() {
|
|
108
|
+
// Filter out events that have already passed
|
|
109
|
+
const now = new Date();
|
|
110
|
+
|
|
111
|
+
const allEvents = [];
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
114
|
+
// 🧪 MOCK UFC EVENT - Development Testing (dynamic date, 5 min from now)
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
116
|
+
if (process.env.NODE_ENV === 'development') {
|
|
117
|
+
const startTime = new Date(now.getTime() + 5 * 60000); // 5 minutes from now
|
|
118
|
+
const formattedDate = startTime.toISOString().split('T')[0];
|
|
119
|
+
const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
|
|
120
|
+
const timestamp = `${formattedDate}T${formattedTime}`;
|
|
121
|
+
|
|
122
|
+
allEvents.push({
|
|
123
|
+
"idEvent": "mock-ufc-gaethje-pimblett",
|
|
124
|
+
"idAPIfootball": null,
|
|
125
|
+
"strEvent": "Justin Gaethje vs Paddy Pimblett",
|
|
126
|
+
"strEventAlternate": "[MOCK] UFC 324: Gaethje vs. Pimblett",
|
|
127
|
+
"strFilename": `UFC ${formattedDate} UFC 324 Gaethje vs Pimblett`,
|
|
128
|
+
"strSport": "Fighting",
|
|
129
|
+
"idLeague": "4443",
|
|
130
|
+
"strLeague": "UFC",
|
|
131
|
+
"strLeagueBadge": "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
|
|
132
|
+
"strSeason": "2026",
|
|
133
|
+
"strDescriptionEN": "[MOCK EVENT] UFC 324 Main Event - Lightweight bout",
|
|
134
|
+
"strHomeTeam": "Justin Gaethje",
|
|
135
|
+
"strAwayTeam": "Paddy Pimblett",
|
|
136
|
+
"intHomeScore": null,
|
|
137
|
+
"intRound": "1",
|
|
138
|
+
"intAwayScore": null,
|
|
139
|
+
"intSpectators": null,
|
|
140
|
+
"strOfficial": null,
|
|
141
|
+
"strTimestamp": timestamp,
|
|
142
|
+
"dateEvent": formattedDate,
|
|
143
|
+
"dateEventLocal": formattedDate,
|
|
144
|
+
"strTime": formattedTime,
|
|
145
|
+
"strTimeLocal": formattedTime,
|
|
146
|
+
"strGroup": "Main Card",
|
|
147
|
+
"idHomeTeam": "3022345",
|
|
148
|
+
"strHomeTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/justin_gaethje.png",
|
|
149
|
+
"idAwayTeam": "4008549",
|
|
150
|
+
"strAwayTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/paddy_pimblett.png",
|
|
151
|
+
"intScore": null,
|
|
152
|
+
"intScoreVotes": null,
|
|
153
|
+
"strResult": "",
|
|
154
|
+
"idVenue": "16132",
|
|
155
|
+
"strVenue": "T-Mobile Arena",
|
|
156
|
+
"strCountry": "United States",
|
|
157
|
+
"strCity": "Las Vegas, NV",
|
|
158
|
+
"strPoster": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/gaethje_pimblett.png",
|
|
159
|
+
"strSquare": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/gaethje_pimblett.png",
|
|
160
|
+
"strFanart": "",
|
|
161
|
+
"strThumb": "",
|
|
162
|
+
"strBanner": "",
|
|
163
|
+
"strMap": "",
|
|
164
|
+
"strTweet1": "",
|
|
165
|
+
"strVideo": "",
|
|
166
|
+
"strStatus": "Not Started",
|
|
167
|
+
"strPostponed": "no",
|
|
168
|
+
"strLocked": "unlocked"
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
console.log(`[UFC] Added mock event for ${formattedDate} at ${formattedTime}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Real UFC events (will be filtered out if in the past)
|
|
175
|
+
allEvents.push({
|
|
176
|
+
"idEvent": "2389036",
|
|
177
|
+
"idAPIfootball": null,
|
|
178
|
+
"strEvent": "Justin Gaethje vs Paddy Pimblett",
|
|
179
|
+
"strEventAlternate": "UFC 324: Gaethje vs. Pimblett",
|
|
180
|
+
"strFilename": "UFC 2026-01-24 UFC 324 Gaethje vs Pimblett",
|
|
181
|
+
"strSport": "Fighting",
|
|
182
|
+
"idLeague": "4443",
|
|
183
|
+
"strLeague": "UFC",
|
|
184
|
+
"strLeagueBadge": "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
|
|
185
|
+
"strSeason": "2026",
|
|
186
|
+
"strDescriptionEN": "UFC 324 Main Event - Lightweight bout",
|
|
187
|
+
"strHomeTeam": "Justin Gaethje",
|
|
188
|
+
"strAwayTeam": "Paddy Pimblett",
|
|
189
|
+
"intHomeScore": null,
|
|
190
|
+
"intRound": "1",
|
|
191
|
+
"intAwayScore": null,
|
|
192
|
+
"intSpectators": null,
|
|
193
|
+
"strOfficial": null,
|
|
194
|
+
"strTimestamp": "2026-01-25T02:00:00",
|
|
195
|
+
"dateEvent": "2026-01-25",
|
|
196
|
+
"dateEventLocal": "2026-01-24",
|
|
197
|
+
"strTime": "02:00:00",
|
|
198
|
+
"strTimeLocal": "21:00:00",
|
|
199
|
+
"strGroup": "Main Card",
|
|
200
|
+
"idHomeTeam": "3022345",
|
|
201
|
+
"strHomeTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/justin_gaethje.png",
|
|
202
|
+
"idAwayTeam": "4008549",
|
|
203
|
+
"strAwayTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/paddy_pimblett.png",
|
|
204
|
+
"intScore": null,
|
|
205
|
+
"intScoreVotes": null,
|
|
206
|
+
"strResult": "",
|
|
207
|
+
"idVenue": "16132",
|
|
208
|
+
"strVenue": "T-Mobile Arena",
|
|
209
|
+
"strCountry": "United States",
|
|
210
|
+
"strCity": "Las Vegas, NV",
|
|
211
|
+
"strPoster": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/gaethje_pimblett.png",
|
|
212
|
+
"strSquare": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/gaethje_pimblett.png",
|
|
213
|
+
"strFanart": "",
|
|
214
|
+
"strThumb": "",
|
|
215
|
+
"strBanner": "",
|
|
216
|
+
"strMap": "",
|
|
217
|
+
"strTweet1": "",
|
|
218
|
+
"strVideo": "",
|
|
219
|
+
"strStatus": "Not Started",
|
|
220
|
+
"strPostponed": "no",
|
|
221
|
+
"strLocked": "unlocked"
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
allEvents.push({
|
|
225
|
+
"idEvent": "2389037",
|
|
226
|
+
"idAPIfootball": null,
|
|
227
|
+
"strEvent": "Alexander Volkanovski vs Diego Lopes",
|
|
228
|
+
"strEventAlternate": "UFC 325: Volkanovski vs Lopes 2",
|
|
229
|
+
"strFilename": "UFC 2026-01-31 UFC 325 Volkanovski vs Lopes 2",
|
|
230
|
+
"strSport": "Fighting",
|
|
231
|
+
"idLeague": "4443",
|
|
232
|
+
"strLeague": "UFC",
|
|
233
|
+
"strLeagueBadge": "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
|
|
234
|
+
"strSeason": "2026",
|
|
235
|
+
"strDescriptionEN": "UFC 325 Main Event - Featherweight bout",
|
|
236
|
+
"strHomeTeam": "Alexander Volkanovski",
|
|
237
|
+
"strAwayTeam": "Diego Lopes",
|
|
238
|
+
"intHomeScore": null,
|
|
239
|
+
"intRound": "2",
|
|
240
|
+
"intAwayScore": null,
|
|
241
|
+
"intSpectators": null,
|
|
242
|
+
"strOfficial": null,
|
|
243
|
+
"strTimestamp": "2026-02-01T02:00:00",
|
|
244
|
+
"dateEvent": "2026-02-01",
|
|
245
|
+
"dateEventLocal": "2026-02-01",
|
|
246
|
+
"strTime": "02:00:00",
|
|
247
|
+
"strTimeLocal": "13:00:00",
|
|
248
|
+
"strGroup": "Main Card",
|
|
249
|
+
"idHomeTeam": null,
|
|
250
|
+
"strHomeTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/alexander_volkanovski.png",
|
|
251
|
+
"idAwayTeam": null,
|
|
252
|
+
"strAwayTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/deigo_lopes.png",
|
|
253
|
+
"intScore": null,
|
|
254
|
+
"intScoreVotes": null,
|
|
255
|
+
"strResult": "",
|
|
256
|
+
"idVenue": "29731",
|
|
257
|
+
"strVenue": "Qudos Bank Arena",
|
|
258
|
+
"strCountry": "Australia",
|
|
259
|
+
"strCity": "Sydney, NSW",
|
|
260
|
+
"strPoster": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/volkanovski_lopes.png",
|
|
261
|
+
"strSquare": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/volkanovski_lopes.png",
|
|
262
|
+
"strFanart": "",
|
|
263
|
+
"strThumb": "",
|
|
264
|
+
"strBanner": "",
|
|
265
|
+
"strMap": "",
|
|
266
|
+
"strTweet1": "",
|
|
267
|
+
"strVideo": "",
|
|
268
|
+
"strStatus": "Not Started",
|
|
269
|
+
"strPostponed": "no",
|
|
270
|
+
"strLocked": "unlocked"
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
allEvents.push({
|
|
274
|
+
"idEvent": "2391879",
|
|
275
|
+
"idAPIfootball": null,
|
|
276
|
+
"strEvent": "Mario Bautista vs Vinicius Oliveira",
|
|
277
|
+
"strEventAlternate": "UFC Fight Night: Bautista vs Oliveira",
|
|
278
|
+
"strFilename": "UFC 2026-02-07 UFC Fight Night 266 Bautista vs Oliveira",
|
|
279
|
+
"strSport": "Fighting",
|
|
280
|
+
"idLeague": "4443",
|
|
281
|
+
"strLeague": "UFC",
|
|
282
|
+
"strLeagueBadge": "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
|
|
283
|
+
"strSeason": "2026",
|
|
284
|
+
"strDescriptionEN": "UFC Fight Night Main Event - Bantamweight bout",
|
|
285
|
+
"strHomeTeam": "Mario Bautista",
|
|
286
|
+
"strAwayTeam": "Vinicius Oliveira",
|
|
287
|
+
"intHomeScore": null,
|
|
288
|
+
"intRound": "3",
|
|
289
|
+
"intAwayScore": null,
|
|
290
|
+
"intSpectators": null,
|
|
291
|
+
"strOfficial": null,
|
|
292
|
+
"strTimestamp": "2026-02-08T02:00:00",
|
|
293
|
+
"dateEvent": "2026-02-08",
|
|
294
|
+
"dateEventLocal": "2026-02-07",
|
|
295
|
+
"strTime": "02:00:00",
|
|
296
|
+
"strTimeLocal": "21:00:00",
|
|
297
|
+
"strGroup": "Main Card",
|
|
298
|
+
"idHomeTeam": null,
|
|
299
|
+
"strHomeTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/mario_bautista.png",
|
|
300
|
+
"idAwayTeam": null,
|
|
301
|
+
"strAwayTeamBadge": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/vinicius_oliveira.png",
|
|
302
|
+
"intScore": null,
|
|
303
|
+
"intScoreVotes": null,
|
|
304
|
+
"strResult": "",
|
|
305
|
+
"idVenue": "18567",
|
|
306
|
+
"strVenue": "UFC APEX",
|
|
307
|
+
"strCountry": "United States",
|
|
308
|
+
"strCity": "Las Vegas, Nevada",
|
|
309
|
+
"strPoster": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/bautista_oliveira.png",
|
|
310
|
+
"strSquare": "https://dubs-avatars-prod.s3.us-east-2.amazonaws.com/UFC/Cards/bautista_oliveira.png",
|
|
311
|
+
"strFanart": "",
|
|
312
|
+
"strThumb": "",
|
|
313
|
+
"strBanner": "",
|
|
314
|
+
"strMap": "",
|
|
315
|
+
"strTweet1": "",
|
|
316
|
+
"strVideo": "",
|
|
317
|
+
"strStatus": "Not Started",
|
|
318
|
+
"strPostponed": "no",
|
|
319
|
+
"strLocked": "unlocked"
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// Return only future events (filter out past events), sorted by date (soonest first)
|
|
323
|
+
return allEvents
|
|
324
|
+
.filter(event => {
|
|
325
|
+
const eventDate = new Date(event.strTimestamp + 'Z');
|
|
326
|
+
return eventDate > now;
|
|
327
|
+
})
|
|
328
|
+
.sort((a, b) => {
|
|
329
|
+
const dateA = new Date(a.strTimestamp + 'Z');
|
|
330
|
+
const dateB = new Date(b.strTimestamp + 'Z');
|
|
331
|
+
return dateA.getTime() - dateB.getTime();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
337
|
+
* 🏀 NCAAB EVENTS - ESPN DATA SOURCE
|
|
338
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
339
|
+
* TheSportsDB has unreliable NCAAB data (placeholder times, missing games),
|
|
340
|
+
* so we fetch from ESPN and transform to TheSportsDB format.
|
|
341
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
342
|
+
*/
|
|
343
|
+
async function getNCAABEventsFromESPN() {
|
|
344
|
+
const events = [];
|
|
345
|
+
const now = new Date();
|
|
346
|
+
|
|
347
|
+
// Query ESPN for the next 7 days
|
|
348
|
+
for (let i = 0; i < 7; i++) {
|
|
349
|
+
const date = new Date(now);
|
|
350
|
+
date.setDate(date.getDate() + i);
|
|
351
|
+
const dateStr = date.toISOString().split('T')[0].replace(/-/g, ''); // YYYYMMDD
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const response = await axios.get(
|
|
355
|
+
`https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard?dates=${dateStr}`
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (response.data?.events) {
|
|
359
|
+
for (const game of response.data.events) {
|
|
360
|
+
// Only include scheduled games (not finished or in progress)
|
|
361
|
+
if (game.status?.type?.name !== 'STATUS_SCHEDULED') continue;
|
|
362
|
+
|
|
363
|
+
// Extract home and away teams
|
|
364
|
+
const homeTeam = game.competitions?.[0]?.competitors?.find(c => c.homeAway === 'home');
|
|
365
|
+
const awayTeam = game.competitions?.[0]?.competitors?.find(c => c.homeAway === 'away');
|
|
366
|
+
|
|
367
|
+
if (!homeTeam || !awayTeam) continue;
|
|
368
|
+
|
|
369
|
+
// Parse ESPN date (already has Z) and convert to TheSportsDB format
|
|
370
|
+
const gameDate = new Date(game.date);
|
|
371
|
+
const dateEvent = gameDate.toISOString().split('T')[0];
|
|
372
|
+
const strTime = gameDate.toISOString().split('T')[1].substring(0, 8);
|
|
373
|
+
const strTimestamp = `${dateEvent}T${strTime}`;
|
|
374
|
+
|
|
375
|
+
// Transform to TheSportsDB format
|
|
376
|
+
events.push({
|
|
377
|
+
idEvent: `espn-ncaab-${game.id}`,
|
|
378
|
+
strEvent: `${homeTeam.team.displayName} vs ${awayTeam.team.displayName}`,
|
|
379
|
+
strEventAlternate: game.name,
|
|
380
|
+
strSport: "Basketball",
|
|
381
|
+
idLeague: "4607",
|
|
382
|
+
strLeague: "NCAAB",
|
|
383
|
+
strLeagueBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/ncaa.png",
|
|
384
|
+
strSeason: "2025-2026",
|
|
385
|
+
strHomeTeam: homeTeam.team.displayName,
|
|
386
|
+
strAwayTeam: awayTeam.team.displayName,
|
|
387
|
+
intHomeScore: null,
|
|
388
|
+
intAwayScore: null,
|
|
389
|
+
strTimestamp: strTimestamp,
|
|
390
|
+
dateEvent: dateEvent,
|
|
391
|
+
strTime: strTime,
|
|
392
|
+
idHomeTeam: homeTeam.team.id,
|
|
393
|
+
strHomeTeamBadge: homeTeam.team.logo,
|
|
394
|
+
idAwayTeam: awayTeam.team.id,
|
|
395
|
+
strAwayTeamBadge: awayTeam.team.logo,
|
|
396
|
+
strVenue: game.competitions?.[0]?.venue?.fullName || "",
|
|
397
|
+
strCity: game.competitions?.[0]?.venue?.address?.city || "",
|
|
398
|
+
strStatus: "NS",
|
|
399
|
+
strPostponed: "no",
|
|
400
|
+
strLocked: "unlocked"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error(`[NCAAB ESPN] Error fetching games for ${dateStr}:`, error.message);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Sort by date (soonest first)
|
|
410
|
+
events.sort((a, b) => {
|
|
411
|
+
const dateA = new Date(a.strTimestamp + 'Z');
|
|
412
|
+
const dateB = new Date(b.strTimestamp + 'Z');
|
|
413
|
+
return dateA.getTime() - dateB.getTime();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
console.log(`[NCAAB ESPN] Fetched ${events.length} upcoming games`);
|
|
417
|
+
return events;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
422
|
+
* 🥊 UFC EVENT ID MAPPING - BACKWARD COMPATIBILITY
|
|
423
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
424
|
+
* Maps fighter combinations to original stub event IDs to maintain backward
|
|
425
|
+
* compatibility with existing bets. The frontend matches bets by idEvent,
|
|
426
|
+
* so we must preserve these IDs for events that already have bets.
|
|
427
|
+
*
|
|
428
|
+
* Key format: "homeTeam|awayTeam" (lowercase, trimmed)
|
|
429
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
430
|
+
*/
|
|
431
|
+
const UFC_EVENT_ID_MAP = {
|
|
432
|
+
'mario bautista|vinicius oliveira': '2391879',
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Gets the original event ID for a UFC fight if it exists in our mapping
|
|
437
|
+
*/
|
|
438
|
+
function getOriginalUFCEventId(homeTeam, awayTeam) {
|
|
439
|
+
const key = `${homeTeam.toLowerCase().trim()}|${awayTeam.toLowerCase().trim()}`;
|
|
440
|
+
return UFC_EVENT_ID_MAP[key] || null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
445
|
+
* 🥊 UFC EVENTS FROM ESPN
|
|
446
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
447
|
+
* Fetch real UFC events from ESPN and transform to TheSportsDB format.
|
|
448
|
+
* ESPN provides scheduled fights with athlete IDs that we can use to construct
|
|
449
|
+
* headshot URLs using the pattern:
|
|
450
|
+
* https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/{id}.png
|
|
451
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
452
|
+
*/
|
|
453
|
+
async function getUFCEventsFromESPN() {
|
|
454
|
+
const events = [];
|
|
455
|
+
const now = new Date();
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
// Query ESPN for UFC events in the next 60 days
|
|
459
|
+
const startDate = now.toISOString().split('T')[0].replace(/-/g, '');
|
|
460
|
+
const endDate = new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000)
|
|
461
|
+
.toISOString().split('T')[0].replace(/-/g, '');
|
|
462
|
+
|
|
463
|
+
const response = await axios.get(
|
|
464
|
+
`https://site.api.espn.com/apis/site/v2/sports/mma/ufc/scoreboard?dates=${startDate}-${endDate}`
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
if (response.data?.events) {
|
|
468
|
+
for (const card of response.data.events) {
|
|
469
|
+
// Get the main event (usually the last competition on the card)
|
|
470
|
+
const competitions = card.competitions || [];
|
|
471
|
+
if (competitions.length === 0) continue;
|
|
472
|
+
|
|
473
|
+
// Main event is the last fight on the card
|
|
474
|
+
const mainEvent = competitions[competitions.length - 1];
|
|
475
|
+
const competitors = mainEvent.competitors || [];
|
|
476
|
+
if (competitors.length < 2) continue;
|
|
477
|
+
|
|
478
|
+
// Find red corner (order 1) and blue corner (order 2)
|
|
479
|
+
const fighter1 = competitors.find(c => c.order === 1) || competitors[0];
|
|
480
|
+
const fighter2 = competitors.find(c => c.order === 2) || competitors[1];
|
|
481
|
+
|
|
482
|
+
// Construct headshot URLs from athlete IDs
|
|
483
|
+
const getHeadshotUrl = (athleteId) => {
|
|
484
|
+
if (!athleteId) return null;
|
|
485
|
+
return `https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/${athleteId}.png&w=350&h=254`;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
// Parse date - use mainEvent.date (main card time) instead of card.date (prelims)
|
|
489
|
+
const eventDate = new Date(mainEvent.date || card.date);
|
|
490
|
+
const dateEvent = eventDate.toISOString().split('T')[0];
|
|
491
|
+
const strTime = eventDate.toISOString().split('T')[1].substring(0, 8);
|
|
492
|
+
const strTimestamp = `${dateEvent}T${strTime}`;
|
|
493
|
+
|
|
494
|
+
// Get venue info
|
|
495
|
+
const venue = mainEvent.venue || card.venues?.[0] || {};
|
|
496
|
+
|
|
497
|
+
// Weight class / fight type
|
|
498
|
+
const weightClass = mainEvent.type?.abbreviation || "Main Event";
|
|
499
|
+
|
|
500
|
+
// Check if this fight has an existing event ID (for backward compatibility with bets)
|
|
501
|
+
const homeTeam = fighter1.athlete?.fullName || 'TBA';
|
|
502
|
+
const awayTeam = fighter2.athlete?.fullName || 'TBA';
|
|
503
|
+
const originalEventId = getOriginalUFCEventId(homeTeam, awayTeam);
|
|
504
|
+
const eventId = originalEventId || `espn-ufc-${card.id}`;
|
|
505
|
+
|
|
506
|
+
events.push({
|
|
507
|
+
idEvent: eventId,
|
|
508
|
+
strEvent: `${homeTeam} vs ${awayTeam}`,
|
|
509
|
+
strEventAlternate: card.name,
|
|
510
|
+
strFilename: `UFC ${dateEvent} ${card.shortName || card.name}`,
|
|
511
|
+
strSport: "Fighting",
|
|
512
|
+
idLeague: "4443",
|
|
513
|
+
strLeague: "UFC",
|
|
514
|
+
strLeagueBadge: "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
|
|
515
|
+
strSeason: eventDate.getFullYear().toString(),
|
|
516
|
+
strDescriptionEN: `${card.name} - ${weightClass}`,
|
|
517
|
+
strHomeTeam: homeTeam,
|
|
518
|
+
strAwayTeam: awayTeam,
|
|
519
|
+
intHomeScore: null,
|
|
520
|
+
intAwayScore: null,
|
|
521
|
+
strTimestamp: strTimestamp,
|
|
522
|
+
dateEvent: dateEvent,
|
|
523
|
+
strTime: strTime,
|
|
524
|
+
strGroup: "Main Card",
|
|
525
|
+
idHomeTeam: fighter1.id,
|
|
526
|
+
strHomeTeamBadge: getHeadshotUrl(fighter1.id),
|
|
527
|
+
idAwayTeam: fighter2.id,
|
|
528
|
+
strAwayTeamBadge: getHeadshotUrl(fighter2.id),
|
|
529
|
+
strVenue: venue.fullName || "TBA",
|
|
530
|
+
strCountry: venue.address?.country || "",
|
|
531
|
+
strCity: venue.address?.city ? `${venue.address.city}, ${venue.address.state || ''}` : "",
|
|
532
|
+
strPoster: null, // ESPN doesn't provide card posters
|
|
533
|
+
strSquare: null,
|
|
534
|
+
strStatus: "NS",
|
|
535
|
+
strPostponed: "no",
|
|
536
|
+
strLocked: "unlocked",
|
|
537
|
+
// Extra ESPN data
|
|
538
|
+
espnEventId: card.id,
|
|
539
|
+
weightClass: weightClass,
|
|
540
|
+
fighterRecords: {
|
|
541
|
+
home: fighter1.records?.[0]?.summary || "",
|
|
542
|
+
away: fighter2.records?.[0]?.summary || ""
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} catch (error) {
|
|
548
|
+
console.error(`[UFC ESPN] Error fetching events:`, error.message);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Sort by date (soonest first)
|
|
552
|
+
events.sort((a, b) => {
|
|
553
|
+
const dateA = new Date(a.strTimestamp + 'Z');
|
|
554
|
+
const dateB = new Date(b.strTimestamp + 'Z');
|
|
555
|
+
return dateA.getTime() - dateB.getTime();
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
console.log(`[UFC ESPN] Fetched ${events.length} upcoming events`);
|
|
559
|
+
return events;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
router.get('/events/:league', async (req, res) => {
|
|
563
|
+
try {
|
|
564
|
+
const { league } = req.params;
|
|
565
|
+
const leagueId = LEAGUE_IDS[league.toUpperCase()];
|
|
566
|
+
|
|
567
|
+
if (!leagueId) {
|
|
568
|
+
return res.status(400).json({
|
|
569
|
+
success: false,
|
|
570
|
+
error: 'Invalid league. Please use NBA, NHL, MLB, NFL, EPL, UFC, NCAAF, or NCAAB.'
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
575
|
+
// 🥊 UFC EVENTS - ESPN DATA SOURCE
|
|
576
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
577
|
+
// Fetch real UFC events from ESPN with fighter headshots.
|
|
578
|
+
// Falls back to stub data if ESPN fails or returns no events.
|
|
579
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
580
|
+
if (league.toUpperCase() === 'UFC') {
|
|
581
|
+
let ufcEvents = await getUFCEventsFromESPN();
|
|
582
|
+
|
|
583
|
+
// Add mock event in development (for testing)
|
|
584
|
+
// Uses real fighter names/IDs so headshots work and oracle can resolve
|
|
585
|
+
if (process.env.NODE_ENV === 'development') {
|
|
586
|
+
const now = new Date();
|
|
587
|
+
const startTime = new Date(now.getTime() + 5 * 60000); // 5 minutes from now
|
|
588
|
+
const formattedDate = startTime.toISOString().split('T')[0];
|
|
589
|
+
const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
|
|
590
|
+
const timestamp = `${formattedDate}T${formattedTime}`;
|
|
591
|
+
|
|
592
|
+
ufcEvents.unshift({
|
|
593
|
+
idEvent: "mock-ufc-dev-test",
|
|
594
|
+
strEvent: "Mario Bautista vs Vinicius Oliveira",
|
|
595
|
+
strEventAlternate: "[MOCK] UFC Fight Night: Bautista vs Oliveira",
|
|
596
|
+
strSport: "Fighting",
|
|
597
|
+
idLeague: "4443",
|
|
598
|
+
strLeague: "UFC",
|
|
599
|
+
strLeagueBadge: "https://a.espncdn.com/i/teamlogos/leagues/500/ufc.png",
|
|
600
|
+
strSeason: "2026",
|
|
601
|
+
strDescriptionEN: "[MOCK EVENT] Development testing - Bantamweight bout",
|
|
602
|
+
strHomeTeam: "Mario Bautista",
|
|
603
|
+
strAwayTeam: "Vinicius Oliveira",
|
|
604
|
+
intHomeScore: null,
|
|
605
|
+
intAwayScore: null,
|
|
606
|
+
strTimestamp: timestamp,
|
|
607
|
+
dateEvent: formattedDate,
|
|
608
|
+
strTime: formattedTime,
|
|
609
|
+
strGroup: "Main Card",
|
|
610
|
+
idHomeTeam: "4410868",
|
|
611
|
+
strHomeTeamBadge: "https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/4410868.png&w=350&h=254",
|
|
612
|
+
idAwayTeam: "4884877",
|
|
613
|
+
strAwayTeamBadge: "https://a.espncdn.com/combiner/i?img=/i/headshots/mma/players/full/4884877.png&w=350&h=254",
|
|
614
|
+
strVenue: "UFC APEX",
|
|
615
|
+
strCountry: "United States",
|
|
616
|
+
strCity: "Las Vegas, NV",
|
|
617
|
+
strStatus: "NS",
|
|
618
|
+
strPostponed: "no",
|
|
619
|
+
strLocked: "unlocked"
|
|
620
|
+
});
|
|
621
|
+
console.log(`[UFC] Added mock event for development testing`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Fall back to stub data if ESPN returns no events
|
|
625
|
+
if (ufcEvents.length === 0) {
|
|
626
|
+
console.log(`[UFC] No ESPN events, falling back to stub data`);
|
|
627
|
+
ufcEvents = getUFCStubEvents();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return res.json({ success: true, data: { events: ufcEvents } });
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
634
|
+
// 🏀 NCAAB EVENTS - ESPN DATA SOURCE
|
|
635
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
636
|
+
// TheSportsDB has unreliable NCAAB data (placeholder times, missing games),
|
|
637
|
+
// so we fetch from ESPN and transform to TheSportsDB format.
|
|
638
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
639
|
+
if (league.toUpperCase() === 'NCAAB') {
|
|
640
|
+
const ncaabEvents = await getNCAABEventsFromESPN();
|
|
641
|
+
|
|
642
|
+
// Add mock event in development
|
|
643
|
+
if (process.env.NODE_ENV === 'development') {
|
|
644
|
+
const now = new Date();
|
|
645
|
+
const startTime = new Date(now.getTime() + 5 * 60000);
|
|
646
|
+
const formattedDate = startTime.toISOString().split('T')[0];
|
|
647
|
+
const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
|
|
648
|
+
const timestamp = `${formattedDate}T${formattedTime}`;
|
|
649
|
+
|
|
650
|
+
ncaabEvents.unshift({
|
|
651
|
+
idEvent: "mock-ncaab-duke-unc-123",
|
|
652
|
+
strEvent: "Duke vs North Carolina",
|
|
653
|
+
strEventAlternate: "North Carolina @ Duke",
|
|
654
|
+
strSport: "Basketball",
|
|
655
|
+
idLeague: "4607",
|
|
656
|
+
strLeague: "NCAAB",
|
|
657
|
+
strLeagueBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/ncaa.png",
|
|
658
|
+
strSeason: "2025-2026",
|
|
659
|
+
strDescriptionEN: "[MOCK EVENT] Tobacco Road Rivalry",
|
|
660
|
+
strHomeTeam: "Duke",
|
|
661
|
+
strAwayTeam: "North Carolina",
|
|
662
|
+
intHomeScore: null,
|
|
663
|
+
intAwayScore: null,
|
|
664
|
+
strTimestamp: timestamp,
|
|
665
|
+
dateEvent: formattedDate,
|
|
666
|
+
strTime: formattedTime,
|
|
667
|
+
idHomeTeam: "150",
|
|
668
|
+
strHomeTeamBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/150.png",
|
|
669
|
+
idAwayTeam: "153",
|
|
670
|
+
strAwayTeamBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/153.png",
|
|
671
|
+
strVenue: "Cameron Indoor Stadium",
|
|
672
|
+
strCity: "Durham, NC",
|
|
673
|
+
strStatus: "NS",
|
|
674
|
+
strPostponed: "no",
|
|
675
|
+
strLocked: "unlocked"
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return res.json({ success: true, data: { events: ncaabEvents } });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const config = {
|
|
683
|
+
method: 'get',
|
|
684
|
+
maxBodyLength: Infinity,
|
|
685
|
+
url: `https://www.thesportsdb.com/api/v1/json/${API_KEY}/eventsnextleague.php?id=${leagueId}`,
|
|
686
|
+
headers: { }
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
const response = await axios.request(config);
|
|
690
|
+
|
|
691
|
+
// Add mock NBA event in development environment
|
|
692
|
+
if (process.env.NODE_ENV === 'development' && league.toUpperCase() === 'NBA') {
|
|
693
|
+
// If events array doesn't exist, create it
|
|
694
|
+
if (!response.data.events) {
|
|
695
|
+
response.data.events = [];
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Get current date for base
|
|
699
|
+
const today = new Date();
|
|
700
|
+
|
|
701
|
+
// Set time to current time + 2 minutes
|
|
702
|
+
const startTime = new Date(today.getTime() + 5 * 60000);
|
|
703
|
+
|
|
704
|
+
// Format date and time properly
|
|
705
|
+
const formattedDate = startTime.toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
706
|
+
const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8); // HH:MM:SS format
|
|
707
|
+
const timestamp = `${formattedDate}T${formattedTime}`;
|
|
708
|
+
|
|
709
|
+
// Create a simple mock NBA event
|
|
710
|
+
const mockNBAEvent = {
|
|
711
|
+
idEvent: "mock-123456",
|
|
712
|
+
idAPIfootball: "mock-415061",
|
|
713
|
+
strEvent: "Adam vs. Amy",
|
|
714
|
+
strEventAlternate: "Amy @ Adams",
|
|
715
|
+
strFilename: `NBA ${formattedDate} Philadelphia 76ers vs Chicago Bulls`,
|
|
716
|
+
strSport: "Basketball",
|
|
717
|
+
idLeague: "4387",
|
|
718
|
+
strLeague: "NBA",
|
|
719
|
+
strLeagueBadge: "https://www.thesportsdb.com/images/media/league/badge/frdjqy1536585083.png",
|
|
720
|
+
strSeason: "2024-2025",
|
|
721
|
+
strDescriptionEN: "[MOCK EVENT] This is a mock event for development purposes.",
|
|
722
|
+
strHomeTeam: "Adam",
|
|
723
|
+
strAwayTeam: "Amy",
|
|
724
|
+
intHomeScore: null,
|
|
725
|
+
intRound: "0",
|
|
726
|
+
intAwayScore: null,
|
|
727
|
+
intSpectators: null,
|
|
728
|
+
strOfficial: null,
|
|
729
|
+
strTimestamp: timestamp,
|
|
730
|
+
dateEvent: formattedDate,
|
|
731
|
+
dateEventLocal: formattedDate,
|
|
732
|
+
strTime: formattedTime,
|
|
733
|
+
strTimeLocal: formattedTime,
|
|
734
|
+
strGroup: "",
|
|
735
|
+
idHomeTeam: "134863",
|
|
736
|
+
strHomeTeamBadge: "https://dubs-api-dev-f14bd1509129.herokuapp.com/images/adam.png",
|
|
737
|
+
idAwayTeam: "134870",
|
|
738
|
+
strAwayTeamBadge: "https://dubs-api-dev-f14bd1509129.herokuapp.com/images/amy.png",
|
|
739
|
+
intScore: null,
|
|
740
|
+
intScoreVotes: null,
|
|
741
|
+
strResult: "",
|
|
742
|
+
idVenue: "19333",
|
|
743
|
+
strVenue: "Wells Fargo Center",
|
|
744
|
+
strCountry: "United States",
|
|
745
|
+
strCity: "",
|
|
746
|
+
strPoster: "https://r2.thesportsdb.com/images/media/event/poster/omiq4k1738097128.jpg",
|
|
747
|
+
strSquare: "",
|
|
748
|
+
strFanart: null,
|
|
749
|
+
strThumb: "https://r2.thesportsdb.com/images/media/event/thumb/yl7lte1728123518.jpg",
|
|
750
|
+
strBanner: "",
|
|
751
|
+
strMap: null,
|
|
752
|
+
strTweet1: "",
|
|
753
|
+
strTweet2: "",
|
|
754
|
+
strTweet3: "",
|
|
755
|
+
strVideo: "",
|
|
756
|
+
strStatus: "NS",
|
|
757
|
+
strPostponed: "no",
|
|
758
|
+
strLocked: "unlocked"
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// Add mock event to the beginning of the array
|
|
762
|
+
response.data.events.unshift(mockNBAEvent);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Add mock EPL event in development environment (for testing draw betting)
|
|
766
|
+
if (process.env.NODE_ENV === 'development' && league.toUpperCase() === 'EPL') {
|
|
767
|
+
// If events array doesn't exist, create it
|
|
768
|
+
if (!response.data.events) {
|
|
769
|
+
response.data.events = [];
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Get current date for base
|
|
773
|
+
const today = new Date();
|
|
774
|
+
|
|
775
|
+
// Set time to current time + 5 minutes
|
|
776
|
+
const startTime = new Date(today.getTime() + 5 * 60000);
|
|
777
|
+
|
|
778
|
+
// Format date and time properly
|
|
779
|
+
const formattedDate = startTime.toISOString().split('T')[0];
|
|
780
|
+
const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
|
|
781
|
+
const timestamp = `${formattedDate}T${formattedTime}`;
|
|
782
|
+
|
|
783
|
+
// Create a mock EPL event (Chelsea @ Arsenal - will end in draw)
|
|
784
|
+
const mockEPLEvent = {
|
|
785
|
+
idEvent: "mock-epl-draw-123",
|
|
786
|
+
idAPIfootball: "mock-epl-415061",
|
|
787
|
+
strEvent: "Arsenal vs Chelsea",
|
|
788
|
+
strEventAlternate: "Chelsea @ Arsenal",
|
|
789
|
+
strFilename: `EPL ${formattedDate} Arsenal vs Chelsea`,
|
|
790
|
+
strSport: "Soccer",
|
|
791
|
+
idLeague: "4328",
|
|
792
|
+
strLeague: "English Premier League",
|
|
793
|
+
strLeagueBadge: "https://www.thesportsdb.com/images/media/league/badge/i6o0kh1549879062.png",
|
|
794
|
+
strSeason: "2024-2025",
|
|
795
|
+
strDescriptionEN: "[MOCK EVENT] Draw test - This game will end 1-1 for testing draw betting.",
|
|
796
|
+
strHomeTeam: "Arsenal",
|
|
797
|
+
strAwayTeam: "Chelsea",
|
|
798
|
+
intHomeScore: null,
|
|
799
|
+
intRound: "0",
|
|
800
|
+
intAwayScore: null,
|
|
801
|
+
intSpectators: null,
|
|
802
|
+
strOfficial: null,
|
|
803
|
+
strTimestamp: timestamp,
|
|
804
|
+
dateEvent: formattedDate,
|
|
805
|
+
dateEventLocal: formattedDate,
|
|
806
|
+
strTime: formattedTime,
|
|
807
|
+
strTimeLocal: formattedTime,
|
|
808
|
+
strGroup: "",
|
|
809
|
+
idHomeTeam: "133604",
|
|
810
|
+
strHomeTeamBadge: "https://www.thesportsdb.com/images/media/team/badge/uyhbfe1612467038.png",
|
|
811
|
+
idAwayTeam: "133610",
|
|
812
|
+
strAwayTeamBadge: "https://www.thesportsdb.com/images/media/team/badge/fbb0lh1617619204.png",
|
|
813
|
+
intScore: null,
|
|
814
|
+
intScoreVotes: null,
|
|
815
|
+
strResult: "",
|
|
816
|
+
idVenue: "12345",
|
|
817
|
+
strVenue: "Emirates Stadium",
|
|
818
|
+
strCountry: "England",
|
|
819
|
+
strCity: "London",
|
|
820
|
+
strPoster: "",
|
|
821
|
+
strSquare: "",
|
|
822
|
+
strFanart: null,
|
|
823
|
+
strThumb: "",
|
|
824
|
+
strBanner: "",
|
|
825
|
+
strMap: null,
|
|
826
|
+
strTweet1: "",
|
|
827
|
+
strTweet2: "",
|
|
828
|
+
strTweet3: "",
|
|
829
|
+
strVideo: "",
|
|
830
|
+
strStatus: "NS",
|
|
831
|
+
strPostponed: "no",
|
|
832
|
+
strLocked: "unlocked"
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
// Add mock EPL event to the beginning of the array
|
|
836
|
+
response.data.events.unshift(mockEPLEvent);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Add mock NCAAF event in development environment
|
|
840
|
+
if (process.env.NODE_ENV === 'development' && league.toUpperCase() === 'NCAAF') {
|
|
841
|
+
// If events array doesn't exist, create it
|
|
842
|
+
if (!response.data.events) {
|
|
843
|
+
response.data.events = [];
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Get current date for base
|
|
847
|
+
const today = new Date();
|
|
848
|
+
|
|
849
|
+
// Set time to current time + 5 minutes
|
|
850
|
+
const startTime = new Date(today.getTime() + 5 * 60000);
|
|
851
|
+
|
|
852
|
+
// Format date and time properly
|
|
853
|
+
const formattedDate = startTime.toISOString().split('T')[0];
|
|
854
|
+
const formattedTime = startTime.toISOString().split('T')[1].substring(0, 8);
|
|
855
|
+
const timestamp = `${formattedDate}T${formattedTime}`;
|
|
856
|
+
|
|
857
|
+
// Create a mock NCAAF event (Ohio State @ Michigan)
|
|
858
|
+
const mockNCAAFEvent = {
|
|
859
|
+
idEvent: "mock-ncaaf-osu-mich-123",
|
|
860
|
+
idAPIfootball: "mock-ncaaf-415061",
|
|
861
|
+
strEvent: "Ohio State vs Michigan",
|
|
862
|
+
strEventAlternate: "Michigan @ Ohio State",
|
|
863
|
+
strFilename: `NCAAF ${formattedDate} Ohio State vs Michigan`,
|
|
864
|
+
strSport: "American Football",
|
|
865
|
+
idLeague: "4479",
|
|
866
|
+
strLeague: "NCAA Division 1",
|
|
867
|
+
strLeagueAlternate: "NCAA Football",
|
|
868
|
+
strLeagueBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/ncaa.png",
|
|
869
|
+
strSeason: "2025-2026",
|
|
870
|
+
strDescriptionEN: "[MOCK EVENT] The Game - Classic Big Ten rivalry matchup",
|
|
871
|
+
strHomeTeam: "Ohio State",
|
|
872
|
+
strAwayTeam: "Michigan",
|
|
873
|
+
intHomeScore: null,
|
|
874
|
+
intRound: "0",
|
|
875
|
+
intAwayScore: null,
|
|
876
|
+
intSpectators: null,
|
|
877
|
+
strOfficial: null,
|
|
878
|
+
strTimestamp: timestamp,
|
|
879
|
+
dateEvent: formattedDate,
|
|
880
|
+
dateEventLocal: formattedDate,
|
|
881
|
+
strTime: formattedTime,
|
|
882
|
+
strTimeLocal: formattedTime,
|
|
883
|
+
strGroup: "",
|
|
884
|
+
idHomeTeam: "4522",
|
|
885
|
+
strHomeTeamBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/194.png",
|
|
886
|
+
idAwayTeam: "4523",
|
|
887
|
+
strAwayTeamBadge: "https://a.espncdn.com/i/teamlogos/ncaa/500/130.png",
|
|
888
|
+
intScore: null,
|
|
889
|
+
intScoreVotes: null,
|
|
890
|
+
strResult: "",
|
|
891
|
+
idVenue: "3856",
|
|
892
|
+
strVenue: "Ohio Stadium",
|
|
893
|
+
strCountry: "United States",
|
|
894
|
+
strCity: "Columbus, OH",
|
|
895
|
+
strPoster: "",
|
|
896
|
+
strSquare: "",
|
|
897
|
+
strFanart: null,
|
|
898
|
+
strThumb: "",
|
|
899
|
+
strBanner: "",
|
|
900
|
+
strMap: null,
|
|
901
|
+
strTweet1: "",
|
|
902
|
+
strTweet2: "",
|
|
903
|
+
strTweet3: "",
|
|
904
|
+
strVideo: "",
|
|
905
|
+
strStatus: "NS",
|
|
906
|
+
strPostponed: "no",
|
|
907
|
+
strLocked: "unlocked"
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
// Add mock NCAAF event to the beginning of the array
|
|
911
|
+
response.data.events.unshift(mockNCAAFEvent);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
res.json({ success: true, data: response.data });
|
|
915
|
+
} catch (error) {
|
|
916
|
+
console.error(`Error fetching events for ${req.params.league}:`, error);
|
|
917
|
+
res.status(500).json({ success: false, error: 'Failed to fetch events' });
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* @route GET /api/sports/ncaab/teams
|
|
923
|
+
* @desc Get all NCAAB teams with pagination (from ESPN)
|
|
924
|
+
* @access Public
|
|
925
|
+
* @query page - Page number (default: 1)
|
|
926
|
+
* @query limit - Items per page (default: 25, max: 100)
|
|
927
|
+
*/
|
|
928
|
+
router.get('/ncaab/teams', async (req, res) => {
|
|
929
|
+
try {
|
|
930
|
+
const page = Math.max(1, parseInt(req.query.page) || 1);
|
|
931
|
+
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 25));
|
|
932
|
+
|
|
933
|
+
const response = await axios.get('http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/teams');
|
|
934
|
+
|
|
935
|
+
// ESPN returns teams nested under sports[0].leagues[0].teams
|
|
936
|
+
const leagueData = response.data?.sports?.[0]?.leagues?.[0];
|
|
937
|
+
const rawTeams = leagueData?.teams || [];
|
|
938
|
+
|
|
939
|
+
// Extract team objects (ESPN wraps each in a { team: {...} } object)
|
|
940
|
+
const allTeams = rawTeams.map(item => {
|
|
941
|
+
const team = item.team || item;
|
|
942
|
+
return {
|
|
943
|
+
id: team.id,
|
|
944
|
+
name: team.displayName,
|
|
945
|
+
shortName: team.shortDisplayName,
|
|
946
|
+
nickname: team.nickname,
|
|
947
|
+
abbreviation: team.abbreviation,
|
|
948
|
+
location: team.location,
|
|
949
|
+
color: team.color,
|
|
950
|
+
alternateColor: team.alternateColor,
|
|
951
|
+
logo: team.logos?.[0]?.href || null,
|
|
952
|
+
isActive: team.isActive
|
|
953
|
+
};
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
// Calculate pagination
|
|
957
|
+
const totalTeams = allTeams.length;
|
|
958
|
+
const totalPages = Math.ceil(totalTeams / limit);
|
|
959
|
+
const startIndex = (page - 1) * limit;
|
|
960
|
+
const endIndex = startIndex + limit;
|
|
961
|
+
const paginatedTeams = allTeams.slice(startIndex, endIndex);
|
|
962
|
+
|
|
963
|
+
res.json({
|
|
964
|
+
success: true,
|
|
965
|
+
data: {
|
|
966
|
+
teams: paginatedTeams,
|
|
967
|
+
pagination: {
|
|
968
|
+
page,
|
|
969
|
+
limit,
|
|
970
|
+
totalTeams,
|
|
971
|
+
totalPages,
|
|
972
|
+
hasNextPage: page < totalPages,
|
|
973
|
+
hasPrevPage: page > 1
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
} catch (error) {
|
|
978
|
+
console.error('Error fetching NCAAB teams:', error);
|
|
979
|
+
res.status(500).json({ success: false, error: 'Failed to fetch NCAAB teams' });
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* @route GET /api/sports/ncaab/conferences
|
|
985
|
+
* @desc Get all NCAAB conferences with their teams (from ESPN)
|
|
986
|
+
* @access Public
|
|
987
|
+
* @query includeTeams - Include teams in each conference (default: false)
|
|
988
|
+
*/
|
|
989
|
+
router.get('/ncaab/conferences', async (req, res) => {
|
|
990
|
+
try {
|
|
991
|
+
const includeTeams = req.query.includeTeams === 'true';
|
|
992
|
+
|
|
993
|
+
const response = await axios.get('http://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/groups');
|
|
994
|
+
|
|
995
|
+
// ESPN returns conferences nested under groups[0].children
|
|
996
|
+
const topLevelGroups = response.data?.groups || [];
|
|
997
|
+
const conferences = [];
|
|
998
|
+
|
|
999
|
+
for (const group of topLevelGroups) {
|
|
1000
|
+
const children = group.children || [];
|
|
1001
|
+
for (const conf of children) {
|
|
1002
|
+
const conferenceData = {
|
|
1003
|
+
id: conf.id,
|
|
1004
|
+
name: conf.name,
|
|
1005
|
+
abbreviation: conf.abbreviation,
|
|
1006
|
+
teamCount: conf.teams?.length || 0
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
if (includeTeams && conf.teams) {
|
|
1010
|
+
conferenceData.teams = conf.teams.map(team => ({
|
|
1011
|
+
id: team.id,
|
|
1012
|
+
name: team.displayName,
|
|
1013
|
+
shortName: team.shortDisplayName,
|
|
1014
|
+
nickname: team.name,
|
|
1015
|
+
abbreviation: team.abbreviation,
|
|
1016
|
+
logo: team.logos?.[0]?.href || null
|
|
1017
|
+
}));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
conferences.push(conferenceData);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Sort conferences alphabetically by name
|
|
1025
|
+
conferences.sort((a, b) => a.name.localeCompare(b.name));
|
|
1026
|
+
|
|
1027
|
+
res.json({
|
|
1028
|
+
success: true,
|
|
1029
|
+
data: {
|
|
1030
|
+
conferences,
|
|
1031
|
+
totalConferences: conferences.length
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
console.error('Error fetching NCAAB conferences:', error);
|
|
1036
|
+
res.status(500).json({ success: false, error: 'Failed to fetch NCAAB conferences' });
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* @route GET /api/sports/ncaab/standings
|
|
1042
|
+
* @desc Get NCAAB standings by conference (from ESPN)
|
|
1043
|
+
* @access Public
|
|
1044
|
+
* @query conference - Filter by conference abbreviation (e.g., 'acc', 'big12')
|
|
1045
|
+
*/
|
|
1046
|
+
router.get('/ncaab/standings', async (req, res) => {
|
|
1047
|
+
try {
|
|
1048
|
+
const conferenceFilter = req.query.conference?.toLowerCase();
|
|
1049
|
+
|
|
1050
|
+
const response = await axios.get('http://site.api.espn.com/apis/v2/sports/basketball/mens-college-basketball/standings');
|
|
1051
|
+
|
|
1052
|
+
// ESPN returns standings grouped by conference under children array
|
|
1053
|
+
const division = response.data;
|
|
1054
|
+
const conferenceStandings = division?.children || [];
|
|
1055
|
+
|
|
1056
|
+
// Helper to extract stat value by name
|
|
1057
|
+
const getStatValue = (stats, name) => {
|
|
1058
|
+
const stat = stats?.find(s => s.name === name || s.abbreviation === name);
|
|
1059
|
+
return stat?.displayValue || stat?.value || null;
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const standings = [];
|
|
1063
|
+
|
|
1064
|
+
for (const conf of conferenceStandings) {
|
|
1065
|
+
// Skip if filtering by conference and this isn't it
|
|
1066
|
+
if (conferenceFilter && conf.abbreviation?.toLowerCase() !== conferenceFilter) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
const conferenceData = {
|
|
1071
|
+
id: conf.id,
|
|
1072
|
+
name: conf.name,
|
|
1073
|
+
abbreviation: conf.abbreviation,
|
|
1074
|
+
teams: []
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// Get standings entries (teams)
|
|
1078
|
+
const entries = conf.standings?.entries || [];
|
|
1079
|
+
|
|
1080
|
+
for (const entry of entries) {
|
|
1081
|
+
const team = entry.team || {};
|
|
1082
|
+
const stats = entry.stats || [];
|
|
1083
|
+
|
|
1084
|
+
conferenceData.teams.push({
|
|
1085
|
+
id: team.id,
|
|
1086
|
+
name: team.displayName,
|
|
1087
|
+
shortName: team.shortDisplayName,
|
|
1088
|
+
abbreviation: team.abbreviation,
|
|
1089
|
+
logo: team.logos?.[0]?.href || null,
|
|
1090
|
+
stats: {
|
|
1091
|
+
wins: getStatValue(stats, 'wins'),
|
|
1092
|
+
losses: getStatValue(stats, 'losses'),
|
|
1093
|
+
winPercent: getStatValue(stats, 'winPercent'),
|
|
1094
|
+
conferenceWins: getStatValue(stats, 'leagueWinPercent'),
|
|
1095
|
+
pointsFor: getStatValue(stats, 'pointsFor'),
|
|
1096
|
+
pointsAgainst: getStatValue(stats, 'pointsAgainst'),
|
|
1097
|
+
pointDifferential: getStatValue(stats, 'pointDifferential'),
|
|
1098
|
+
streak: getStatValue(stats, 'streak'),
|
|
1099
|
+
playoffSeed: getStatValue(stats, 'playoffSeed')
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Sort teams by wins descending, then by win percentage
|
|
1105
|
+
conferenceData.teams.sort((a, b) => {
|
|
1106
|
+
const winsA = parseInt(a.stats.wins) || 0;
|
|
1107
|
+
const winsB = parseInt(b.stats.wins) || 0;
|
|
1108
|
+
if (winsB !== winsA) return winsB - winsA;
|
|
1109
|
+
const pctA = parseFloat(a.stats.winPercent) || 0;
|
|
1110
|
+
const pctB = parseFloat(b.stats.winPercent) || 0;
|
|
1111
|
+
return pctB - pctA;
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
standings.push(conferenceData);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Sort conferences alphabetically
|
|
1118
|
+
standings.sort((a, b) => a.name.localeCompare(b.name));
|
|
1119
|
+
|
|
1120
|
+
res.json({
|
|
1121
|
+
success: true,
|
|
1122
|
+
data: {
|
|
1123
|
+
standings,
|
|
1124
|
+
totalConferences: standings.length
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
console.error('Error fetching NCAAB standings:', error);
|
|
1129
|
+
res.status(500).json({ success: false, error: 'Failed to fetch NCAAB standings' });
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* @route GET /api/sports/rankings/:league
|
|
1135
|
+
* @desc Get AP Top 25 rankings for NCAAF or NCAAB
|
|
1136
|
+
* @access Public
|
|
1137
|
+
* @query limit - Number of teams to return (default: 25, max: 25)
|
|
1138
|
+
*/
|
|
1139
|
+
router.get('/rankings/:league', async (req, res) => {
|
|
1140
|
+
try {
|
|
1141
|
+
const { league } = req.params;
|
|
1142
|
+
const leagueUpper = league.toUpperCase();
|
|
1143
|
+
const url = ESPN_RANKINGS_URLS[leagueUpper];
|
|
1144
|
+
|
|
1145
|
+
if (!url) {
|
|
1146
|
+
return res.status(400).json({
|
|
1147
|
+
success: false,
|
|
1148
|
+
error: 'Invalid league. Rankings are available for NCAAF and NCAAB only.'
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const limit = Math.min(25, Math.max(1, parseInt(req.query.limit) || 25));
|
|
1153
|
+
|
|
1154
|
+
const response = await axios.get(url);
|
|
1155
|
+
const rankingsData = response.data.rankings || [];
|
|
1156
|
+
|
|
1157
|
+
// Find AP Top 25 ranking (type: 'ap')
|
|
1158
|
+
const apRanking = rankingsData.find(r => r.type === 'ap') || rankingsData[0];
|
|
1159
|
+
|
|
1160
|
+
if (!apRanking) {
|
|
1161
|
+
return res.status(404).json({
|
|
1162
|
+
success: false,
|
|
1163
|
+
error: 'No rankings data available'
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const ranks = (apRanking.ranks || []).slice(0, limit);
|
|
1168
|
+
const others = apRanking.others || [];
|
|
1169
|
+
|
|
1170
|
+
// Transform to a cleaner format
|
|
1171
|
+
const transformedRanks = ranks.map(rank => ({
|
|
1172
|
+
rank: rank.current,
|
|
1173
|
+
previousRank: rank.previous,
|
|
1174
|
+
trend: rank.trend,
|
|
1175
|
+
points: rank.points,
|
|
1176
|
+
firstPlaceVotes: rank.firstPlaceVotes,
|
|
1177
|
+
record: rank.recordSummary,
|
|
1178
|
+
team: {
|
|
1179
|
+
id: rank.team?.id,
|
|
1180
|
+
name: rank.team?.name || rank.team?.location,
|
|
1181
|
+
nickname: rank.team?.nickname,
|
|
1182
|
+
abbreviation: rank.team?.abbreviation,
|
|
1183
|
+
location: rank.team?.location,
|
|
1184
|
+
logo: rank.team?.logos?.[0]?.href || null,
|
|
1185
|
+
color: rank.team?.color,
|
|
1186
|
+
alternateColor: rank.team?.alternateColor
|
|
1187
|
+
}
|
|
1188
|
+
}));
|
|
1189
|
+
|
|
1190
|
+
const transformedOthers = others.map(rank => ({
|
|
1191
|
+
points: rank.points,
|
|
1192
|
+
trend: rank.trend,
|
|
1193
|
+
record: rank.recordSummary,
|
|
1194
|
+
team: {
|
|
1195
|
+
id: rank.team?.id,
|
|
1196
|
+
name: rank.team?.name || rank.team?.location,
|
|
1197
|
+
nickname: rank.team?.nickname,
|
|
1198
|
+
abbreviation: rank.team?.abbreviation,
|
|
1199
|
+
location: rank.team?.location,
|
|
1200
|
+
logo: rank.team?.logos?.[0]?.href || null
|
|
1201
|
+
}
|
|
1202
|
+
}));
|
|
1203
|
+
|
|
1204
|
+
res.json({
|
|
1205
|
+
success: true,
|
|
1206
|
+
data: {
|
|
1207
|
+
name: apRanking.name,
|
|
1208
|
+
type: apRanking.type,
|
|
1209
|
+
season: apRanking.season?.year,
|
|
1210
|
+
lastUpdated: apRanking.date,
|
|
1211
|
+
ranks: transformedRanks,
|
|
1212
|
+
othersReceivingVotes: transformedOthers
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
console.error(`Error fetching rankings for ${req.params.league}:`, error);
|
|
1217
|
+
res.status(500).json({ success: false, error: 'Failed to fetch rankings' });
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* @route GET /api/sports/standings/:league/:season
|
|
1223
|
+
* @desc Get standings for a specific league and season
|
|
1224
|
+
* @access Public
|
|
1225
|
+
*/
|
|
1226
|
+
router.get('/standings/:league/:season', async (req, res) => {
|
|
1227
|
+
try {
|
|
1228
|
+
const { league, season } = req.params;
|
|
1229
|
+
const leagueId = LEAGUE_IDS[league.toUpperCase()];
|
|
1230
|
+
|
|
1231
|
+
if (!leagueId) {
|
|
1232
|
+
return res.status(400).json({
|
|
1233
|
+
success: false,
|
|
1234
|
+
error: 'Invalid league. Please use NBA, NHL, MLB, NFL, EPL, UFC, NCAAF, or NCAAB.'
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const data = await theSportsDB.getLookupTableByLeagueIdAndSeason(leagueId, season);
|
|
1239
|
+
res.json({ success: true, data });
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
console.error(`Error fetching standings for ${req.params.league}:`, error);
|
|
1242
|
+
res.status(500).json({ success: false, error: 'Failed to fetch standings' });
|
|
1243
|
+
}
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* @route GET /api/sports/seasons/:league
|
|
1248
|
+
* @desc Get available seasons for a league
|
|
1249
|
+
* @access Public
|
|
1250
|
+
*/
|
|
1251
|
+
router.get('/seasons/:league', async (req, res) => {
|
|
1252
|
+
try {
|
|
1253
|
+
const { league } = req.params;
|
|
1254
|
+
const leagueId = LEAGUE_IDS[league.toUpperCase()];
|
|
1255
|
+
|
|
1256
|
+
if (!leagueId) {
|
|
1257
|
+
return res.status(400).json({
|
|
1258
|
+
success: false,
|
|
1259
|
+
error: 'Invalid league. Please use NBA, NHL, MLB, NFL, EPL, UFC, NCAAF, or NCAAB.'
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const data = await theSportsDB.getSeasonsInLeagueById(leagueId);
|
|
1264
|
+
res.json({ success: true, data });
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
console.error(`Error fetching seasons for ${req.params.league}:`, error);
|
|
1267
|
+
res.status(500).json({ success: false, error: 'Failed to fetch seasons' });
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
module.exports = router;
|