v2
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import prisma from './db';
|
||||
|
||||
/**
|
||||
* Logs an API request to the database.
|
||||
* This is intentionally fire-and-forget — it never blocks the response path.
|
||||
*/
|
||||
export function logApiRequest(
|
||||
endpoint: string,
|
||||
status: number,
|
||||
duration: number
|
||||
): void {
|
||||
prisma.apiRequestLog
|
||||
.create({ data: { endpoint, status, duration } })
|
||||
.catch((error) => console.error('Failed to log API request:', error));
|
||||
|
||||
// Prune old logs in the background (keep last 7 days)
|
||||
pruneOldLogs();
|
||||
}
|
||||
|
||||
let lastPruneAt = 0;
|
||||
const PRUNE_INTERVAL = 60 * 60 * 1000; // prune at most once per hour
|
||||
|
||||
function pruneOldLogs(): void {
|
||||
if (Date.now() - lastPruneAt < PRUNE_INTERVAL) return;
|
||||
lastPruneAt = Date.now();
|
||||
|
||||
const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
prisma.apiRequestLog
|
||||
.deleteMany({ where: { createdAt: { lt: cutoff } } })
|
||||
.catch((error) => console.error('Failed to prune API logs:', error));
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? ['query', 'error', 'warn']
|
||||
: ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
|
||||
export default prisma;
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export interface AppSettings {
|
||||
siteName: string;
|
||||
selectedLeague: string;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
showLiveMatches: boolean;
|
||||
showStandings: boolean;
|
||||
showTopScorers: boolean;
|
||||
showRecentResults: boolean;
|
||||
showUpcoming: boolean;
|
||||
refreshInterval: number;
|
||||
}
|
||||
|
||||
export const defaultSettings: AppSettings = {
|
||||
siteName: 'Primeira Liga Stats',
|
||||
selectedLeague: 'primeira_liga',
|
||||
primaryColor: '#E42518',
|
||||
secondaryColor: '#006600',
|
||||
showLiveMatches: true,
|
||||
showStandings: true,
|
||||
showTopScorers: true,
|
||||
showRecentResults: true,
|
||||
showUpcoming: true,
|
||||
refreshInterval: 300,
|
||||
};
|
||||
|
||||
export const SettingsContext = createContext<AppSettings>(defaultSettings);
|
||||
|
||||
export function useSettings() {
|
||||
return useContext(SettingsContext);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import prisma from './db';
|
||||
|
||||
export interface AppSettings {
|
||||
id: string;
|
||||
apiKey: string | null;
|
||||
selectedLeague: string;
|
||||
leagueId: number;
|
||||
season: number;
|
||||
siteName: string;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
showLiveMatches: boolean;
|
||||
showStandings: boolean;
|
||||
showTopScorers: boolean;
|
||||
showRecentResults: boolean;
|
||||
showUpcoming: boolean;
|
||||
refreshInterval: number;
|
||||
}
|
||||
|
||||
const defaultSettings: AppSettings = {
|
||||
id: 'main',
|
||||
apiKey: null,
|
||||
selectedLeague: 'primeira_liga',
|
||||
leagueId: 94,
|
||||
season: 2024,
|
||||
siteName: 'Primeira Liga Stats',
|
||||
primaryColor: '#E42518',
|
||||
secondaryColor: '#006600',
|
||||
showLiveMatches: true,
|
||||
showStandings: true,
|
||||
showTopScorers: true,
|
||||
showRecentResults: true,
|
||||
showUpcoming: true,
|
||||
refreshInterval: 300,
|
||||
};
|
||||
|
||||
// In-memory cache to avoid a DB round-trip on every API call
|
||||
let settingsCache: AppSettings | null = null;
|
||||
let settingsCacheAt = 0;
|
||||
const SETTINGS_CACHE_TTL = 30_000; // 30 seconds
|
||||
|
||||
export async function getSettings(): Promise<AppSettings> {
|
||||
// Return from cache if still fresh
|
||||
if (settingsCache && Date.now() - settingsCacheAt < SETTINGS_CACHE_TTL) {
|
||||
return settingsCache;
|
||||
}
|
||||
|
||||
try {
|
||||
let settings = await prisma.settings.findUnique({
|
||||
where: { id: 'main' },
|
||||
});
|
||||
|
||||
if (!settings) {
|
||||
// Create default settings with API key from env
|
||||
settings = await prisma.settings.create({
|
||||
data: {
|
||||
...defaultSettings,
|
||||
apiKey: process.env.API_FOOTBALL_API_KEY || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
settingsCache = settings as AppSettings;
|
||||
settingsCacheAt = Date.now();
|
||||
return settingsCache;
|
||||
} catch (error) {
|
||||
console.error('Failed to get settings:', error);
|
||||
return {
|
||||
...defaultSettings,
|
||||
apiKey: process.env.API_FOOTBALL_API_KEY || null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Call this after any settings update to bust the cache. */
|
||||
export function invalidateSettingsCache() {
|
||||
settingsCache = null;
|
||||
settingsCacheAt = 0;
|
||||
}
|
||||
|
||||
export function getLeagueConfig(selectedLeague: string) {
|
||||
if (selectedLeague === 'euro_2026') {
|
||||
return {
|
||||
leagueId: 4, // EURO Championship
|
||||
season: 2024, // Euro 2024 (most recent)
|
||||
name: 'UEFA Euro 2024',
|
||||
};
|
||||
}
|
||||
return {
|
||||
leagueId: 94, // Portuguese Primeira Liga
|
||||
season: 2024,
|
||||
name: 'Primeira Liga',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export type Expense = {
|
||||
id: string
|
||||
amount: number
|
||||
category: string
|
||||
description: string
|
||||
date: Date
|
||||
}
|
||||
|
||||
export type ExpenseFormData = Omit<Expense, 'id' | 'date'> & {
|
||||
date: string
|
||||
}
|
||||
|
||||
export const EXPENSE_CATEGORIES = [
|
||||
'Food',
|
||||
'Transportation',
|
||||
'Housing',
|
||||
'Utilities',
|
||||
'Entertainment',
|
||||
'Healthcare',
|
||||
'Shopping',
|
||||
'Education',
|
||||
'Other'
|
||||
] as const
|
||||
|
||||
export type DateRange = {
|
||||
from: Date | undefined
|
||||
to: Date | undefined
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function formatDuration(seconds: number): string {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
Reference in New Issue
Block a user