Skip to content

Commit ef60f75

Browse files
hh
1 parent f8676ae commit ef60f75

5 files changed

Lines changed: 193 additions & 84 deletions

File tree

hooks/useSportsData.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import useSWR from 'swr';
2+
import { useEffect } from 'react';
23
import type { SportsData } from '@/types/Card';
34

45
const buildApiBasePath = () => {
@@ -44,6 +45,38 @@ export const useSportsData = (cardSet: string | null) => {
4445
}
4546
);
4647

48+
useEffect( () => {
49+
let isActive = true;
50+
51+
const refreshFromServer = async () => {
52+
if ( !cardSet ) {
53+
return;
54+
}
55+
56+
try {
57+
const response = await fetch( `${API_BASE_PATH}/Sports/sportsData`, {
58+
method: 'POST',
59+
headers: {
60+
'Content-Type': 'application/json',
61+
},
62+
body: JSON.stringify( { cardSet } ),
63+
} );
64+
65+
if ( response.ok && isActive ) {
66+
mutate();
67+
}
68+
} catch ( updateError ) {
69+
console.error( 'Failed to refresh sports data cache:', updateError );
70+
}
71+
};
72+
73+
refreshFromServer();
74+
75+
return () => {
76+
isActive = false;
77+
};
78+
}, [ cardSet, mutate ] );
79+
4780
const normalizedData = data ?? [];
4881
const normalizedError = error instanceof Error ? error.message : null;
4982

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "next dev",
77
"build": "next build",
88
"start": "next start",
9-
"lint": "next lint"
9+
"lint": "next lint",
10+
"refresh:sports-cache": "node scripts/refresh-sports-cache.js"
1011
},
1112
"dependencies": {
1213
"@auth0/nextjs-auth0": "^4.15.0",

pages/api/Sports/sportsData.ts

Lines changed: 93 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { NextApiRequest, NextApiResponse } from 'next';
2-
import { chromium } from 'playwright';
32
import clientPromise from '@/utils/mongo';
43
import { getSportsUrls } from '@/utils/sportsUrls';
54

@@ -9,132 +8,144 @@ const DEFAULT_HEADERS = {
98
accept: 'application/json, text/plain, */*',
109
'accept-language': 'en-US,en;q=0.9',
1110
referer: 'https://www.sportscardspro.com/',
11+
'user-agent': USER_AGENT,
1212
};
1313
const REQUEST_TIMEOUT_MS = 45000;
1414

15-
const parseJsonSafe = (payload: string) => {
15+
const normalizeCardSet = ( value: unknown ) => {
16+
if ( typeof value !== 'string' ) {
17+
return '';
18+
}
19+
return value.trim();
20+
};
21+
22+
const parseJsonSafe = ( payload: string ) => {
1623
try {
17-
return JSON.parse(payload);
18-
} catch (error) {
24+
return JSON.parse( payload );
25+
} catch ( error ) {
1926
return null;
2027
}
2128
};
2229

23-
const fetchWithBrowser = async (urls: string[]) => {
24-
const browser = await chromium.launch({
25-
headless: true,
26-
args: [ '--disable-blink-features=AutomationControlled' ],
27-
});
28-
29-
const context = await browser.newContext({
30-
userAgent: USER_AGENT,
31-
locale: 'en-US',
32-
extraHTTPHeaders: DEFAULT_HEADERS,
33-
});
30+
const fetchWithTimeout = async ( url: string ) => {
31+
const controller = new AbortController();
32+
const timer = setTimeout( () => controller.abort(), REQUEST_TIMEOUT_MS );
3433

35-
await context.addInitScript(() => {
36-
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
37-
});
34+
try {
35+
return await fetch( url, {
36+
headers: DEFAULT_HEADERS,
37+
signal: controller.signal,
38+
} );
39+
} finally {
40+
clearTimeout( timer );
41+
}
42+
};
3843

39-
const page = await context.newPage();
44+
const fetchFromServer = async ( urls: string[] ) => {
4045
const results: unknown[] = [];
4146

42-
for (const url of urls) {
47+
for ( const url of urls ) {
4348
try {
44-
const response = await page.goto(url, {
45-
waitUntil: 'domcontentloaded',
46-
timeout: REQUEST_TIMEOUT_MS,
47-
});
49+
const response = await fetchWithTimeout( url );
4850

49-
if (!response) {
50-
throw new Error('No response received');
51+
if ( !response.ok ) {
52+
throw new Error( `Status ${ response.status }` );
5153
}
5254

53-
if (!response.ok()) {
54-
throw new Error(`Status ${response.status()}`);
55-
}
56-
57-
const contentType = response.headers()['content-type'] || '';
55+
const contentType = response.headers.get( 'content-type' ) || '';
5856
let data: unknown = null;
5957

60-
if (contentType.includes('application/json')) {
58+
if ( contentType.includes( 'application/json' ) ) {
6159
data = await response.json();
6260
} else {
6361
const bodyText = await response.text();
64-
data = parseJsonSafe(bodyText);
62+
data = parseJsonSafe( bodyText );
6563
}
6664

67-
if (!data) {
68-
throw new Error('Invalid JSON payload');
65+
if ( !data ) {
66+
throw new Error( 'Invalid JSON payload' );
6967
}
7068

71-
results.push(data);
72-
} catch (error) {
73-
console.error(`Error fetching sports data from ${url}:`, error);
69+
results.push( data );
70+
} catch ( error ) {
71+
console.error( `Error fetching sports data from ${ url }:`, error );
7472
}
7573
}
7674

77-
await page.close();
78-
await context.close();
79-
await browser.close();
80-
8175
return results;
8276
};
8377

84-
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
85-
if (req.method !== 'GET') {
86-
res.setHeader('Allow', [ 'GET' ]);
87-
return res.status(405).json({ error: 'Method Not Allowed' });
88-
}
78+
export default async function handler( req: NextApiRequest, res: NextApiResponse ) {
79+
res.setHeader( 'Cache-Control', 'no-store' );
80+
81+
const method = req.method ?? 'GET';
8982

90-
res.setHeader('Cache-Control', 'no-store');
83+
if ( method !== 'GET' && method !== 'POST' ) {
84+
res.setHeader( 'Allow', [ 'GET', 'POST' ] );
85+
return res.status( 405 ).json( { error: 'Method Not Allowed' } );
86+
}
9187

9288
try {
93-
const { cardSet } = req.query;
89+
const cardSet = normalizeCardSet( method === 'POST'
90+
? req.body?.cardSet ?? req.query.cardSet
91+
: req.query.cardSet );
9492

95-
if (!cardSet || typeof cardSet !== 'string') {
96-
return res.status(400).json({ error: 'Card set is required' });
93+
if ( !cardSet ) {
94+
return res.status( 400 ).json( { error: 'Card set is required' } );
9795
}
9896

99-
const urls = getSportsUrls(cardSet);
100-
101-
if (!urls || urls.length === 0) {
102-
return res.status(404).json({ error: `No data found for card set: ${cardSet}` });
97+
const urls = getSportsUrls( cardSet );
98+
if ( !urls || urls.length === 0 ) {
99+
return res.status( 404 ).json( { error: `No data found for card set: ${ cardSet }` } );
103100
}
104101

105-
const results = await fetchWithBrowser(urls);
106-
const validData = results.filter((result) => result !== null);
107-
108102
const client = await clientPromise;
109-
const collection = client.db('cardPriceApp').collection('sportsDataCache');
110-
const fetchedAt = new Date();
103+
const collection = client.db( 'cardPriceApp' ).collection( 'sportsDataCache' );
111104

112-
if (validData.length > 0) {
113-
await collection.updateOne(
114-
{ cardSet },
115-
{
116-
$set: {
117-
cardSet,
118-
data: validData,
119-
fetchedAt,
120-
sourceUrls: urls,
121-
pageCount: validData.length,
122-
},
123-
},
124-
{ upsert: true }
125-
);
105+
if ( method === 'GET' ) {
106+
const cached = await collection.findOne( { cardSet } );
107+
if ( cached?.data?.length ) {
108+
return res.status( 200 ).json( cached.data );
109+
}
110+
111+
return res.status( 404 ).json( { error: 'No cached data found' } );
112+
}
113+
114+
const payload = Array.isArray( req.body?.data ) ? req.body.data.filter( Boolean ) : [];
115+
let dataToStore = payload;
126116

127-
return res.status(200).json(validData);
117+
if ( dataToStore.length === 0 ) {
118+
const serverData = await fetchFromServer( urls );
119+
dataToStore = serverData.filter( Boolean );
128120
}
129121

130-
const cached = await collection.findOne({ cardSet });
131-
if (cached?.data?.length) {
132-
return res.status(200).json(cached.data);
122+
if ( dataToStore.length === 0 ) {
123+
const cached = await collection.findOne( { cardSet } );
124+
if ( cached?.data?.length ) {
125+
return res.status( 200 ).json( cached.data );
126+
}
127+
128+
return res.status( 502 ).json( { error: 'No valid data found' } );
133129
}
134130

135-
return res.status(502).json({ error: 'No valid data found' });
136-
} catch (error) {
137-
console.error('Error fetching sports data:', error);
138-
return res.status(500).json({ error: 'Internal Server Error' });
131+
const fetchedAt = new Date();
132+
await collection.updateOne(
133+
{ cardSet },
134+
{
135+
$set: {
136+
cardSet,
137+
data: dataToStore,
138+
fetchedAt,
139+
sourceUrls: urls,
140+
pageCount: dataToStore.length,
141+
},
142+
},
143+
{ upsert: true }
144+
);
145+
146+
return res.status( 200 ).json( dataToStore );
147+
} catch ( error ) {
148+
console.error( 'Error handling sports data:', error );
149+
return res.status( 500 ).json( { error: 'Internal Server Error' } );
139150
}
140151
}

proxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const parseAuthenticatedUser = ( decoded: unknown ): AuthenticatedUser | null =>
7676
return null;
7777
}
7878

79-
const { userId, username } = decoded as { userId?: unknown; username?: unknown };
79+
const { userId, username } = decoded as { userId?: unknown; username?: unknown; };
8080
if ( !userId || !username ) {
8181
return null;
8282
}

scripts/refresh-sports-cache.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const DEFAULT_BASE_URL = 'http://localhost:3000';
5+
const BASE_URL = (process.env.SPORTS_REFRESH_BASE_URL || DEFAULT_BASE_URL).replace(/\/$/, '');
6+
const SETS_PATH = path.join(__dirname, '..', 'constants', 'cardSets.ts');
7+
8+
const readCardSets = () => {
9+
const source = fs.readFileSync(SETS_PATH, 'utf8');
10+
const matches = source.match(/['"]([^'"]+)['"]/g) || [];
11+
return matches
12+
.map((match) => match.replace(/^['"]|['"]$/g, '').trim())
13+
.filter(Boolean);
14+
};
15+
16+
const refreshCardSet = async (cardSet) => {
17+
const response = await fetch(`${BASE_URL}/api/Sports/sportsData`, {
18+
method: 'POST',
19+
headers: {
20+
'Content-Type': 'application/json',
21+
},
22+
body: JSON.stringify({ cardSet }),
23+
});
24+
25+
if (!response.ok) {
26+
const text = await response.text();
27+
throw new Error(`Failed (${response.status}) ${text}`);
28+
}
29+
30+
return response.json();
31+
};
32+
33+
const run = async () => {
34+
if (!fs.existsSync(SETS_PATH)) {
35+
console.error(`Card set list not found at ${SETS_PATH}`);
36+
process.exit(1);
37+
}
38+
39+
const sets = readCardSets();
40+
if (!sets.length) {
41+
console.error('No card sets found to refresh.');
42+
process.exit(1);
43+
}
44+
45+
console.log(`Refreshing ${sets.length} sets using ${BASE_URL}...`);
46+
47+
let successCount = 0;
48+
for (const cardSet of sets) {
49+
try {
50+
await refreshCardSet(cardSet);
51+
successCount += 1;
52+
console.log(`? ${cardSet}`);
53+
} catch (error) {
54+
console.error(`? ${cardSet}: ${error.message || error}`);
55+
}
56+
}
57+
58+
console.log(`Done. ${successCount}/${sets.length} sets refreshed.`);
59+
};
60+
61+
run().catch((error) => {
62+
console.error(error);
63+
process.exit(1);
64+
});

0 commit comments

Comments
 (0)