Phase 2: Database e API Layer
- Configurado Prisma 7 com multiSchema para staging database - Models: Site, GA4DailyTraffic, GA4TrafficSources, GSCSearchPerformance - Created lib utilities (prisma.ts, utils.ts) com formatação PT-PT - API routes implementadas: * GET /api/sites - lista sites activos * GET /api/metrics/[siteId]?period=30d - métricas agregadas + charts * GET /api/health - health check com conexão DB Métricas incluem: - KPIs: visitors, sessions, pageViews, newUsers com % change - Charts: dailyTraffic, trafficSources, topQueries (GSC) - Comparação período anterior para trends Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
15
src/lib/prisma.ts
Normal file
15
src/lib/prisma.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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
|
||||
}
|
||||
58
src/lib/utils.ts
Normal file
58
src/lib/utils.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { type ClassValue, clsx } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
/**
|
||||
* Merge Tailwind CSS classes with proper precedence
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number with Portuguese locale
|
||||
* @example formatNumber(1234567) => "1.234.567"
|
||||
*/
|
||||
export function formatNumber(num: number): string {
|
||||
return new Intl.NumberFormat('pt-PT').format(num)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number as percentage
|
||||
* @example formatPercent(0.1234) => "12,3%"
|
||||
*/
|
||||
export function formatPercent(num: number, decimals: number = 1): string {
|
||||
return `${(num * 100).toFixed(decimals)}%`
|
||||
}
|
||||
|
||||
/**
|
||||
* Format number as compact (K, M, B)
|
||||
* @example formatCompact(1234567) => "1,2M"
|
||||
*/
|
||||
export function formatCompact(num: number): string {
|
||||
return new Intl.NumberFormat('pt-PT', {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(num)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date as Portuguese short date
|
||||
* @example formatDate(new Date()) => "13/02/2026"
|
||||
*/
|
||||
export function formatDate(date: Date | string): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date
|
||||
return new Intl.DateTimeFormat('pt-PT', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
}).format(d)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentage change
|
||||
* @example calculateChange(100, 120) => 20
|
||||
*/
|
||||
export function calculateChange(oldValue: number, newValue: number): number {
|
||||
if (oldValue === 0) return 0
|
||||
return ((newValue - oldValue) / oldValue) * 100
|
||||
}
|
||||
Reference in New Issue
Block a user