From de527073dc8e45f62999c5d5e4d5316e3afa9462 Mon Sep 17 00:00:00 2001 From: headpatsyou Date: Thu, 8 Jan 2026 19:48:13 +0000 Subject: [PATCH] we ball --- README.md | 33 +++--- src/app/admin/page.tsx | 159 +++----------------------- src/app/api/admin/config/route.ts | 33 +----- src/app/api/competition-name/route.ts | 7 +- src/config/config.json.example | 7 -- src/services/adminStats.ts | 6 +- src/utils/config.ts | 20 +++- 7 files changed, 60 insertions(+), 205 deletions(-) delete mode 100644 src/config/config.json.example diff --git a/README.md b/README.md index 68efa8c..4ec24f0 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ A plataforma sincroniza automaticamente com o LiftingCast, portanto os árbitros ### Para os Organizadores - Painel de administração em http://seu-dominio.pt/admin -- Edição de configuração em tempo real (Meet ID, Auth String, etc) - Upload de CSV e logo sem recompilar a aplicação - Visualização de estatísticas (quantos atletas estão no ficheiro, etc) -- Suporta múltiplas competições apenas trocando ficheiro CSV e configuração +- Configuração lida do `.env` (Meet ID, Auth String, etc) +- Suporta múltiplas competições apenas trocando ficheiro CSV e valores no `.env` ### Técnico @@ -46,16 +46,14 @@ npm install ### 2. Configurar -Edita `src/config/config.json`: +Copia `.env.example` para `.env` e preenche: -```json -{ - "meetId": "seu-meet-id-do-liftingcast", - "csvFileName": "seu-ficheiro.csv", - "authString": "palavra-passe-segura", - "adminPassword": "password-admin", - "competitionName": "Nome da sua Competição" -} +``` +MEET_ID=seu-meet-id-do-liftingcast +CSV_FILE_NAME=seu-ficheiro.csv +AUTH_STRING=palavra-passe-segura +ADMIN_PASSWORD=password-admin +COMPETITION_NAME=Nome da sua Competição ``` Copia o teu CSV para `src/data/seu-ficheiro.csv` (o formato esperado está documentado em baixo). @@ -81,11 +79,10 @@ src/ auth/ # Valida o auth param find-lifter/ # Busca atleta no CSV submit/ # Submete ao LiftingCast - admin/ # Config e upload (admin) + admin/ # Upload e stats layout.tsx page.tsx # Página principal components/ # UI reutilizável - config/ # config.json (não commitar!) data/ # Ficheiros CSV (não commitar!) services/ # Lógica de server (stats, etc) types/ # TypeScript types @@ -108,12 +105,11 @@ public/ Acede a `/admin` com a tua password. Daqui podes: -- Ver quantos atletas estão carregados +- Ver quantos atletas estão carregados (e valores atuais de configuração) - Upload de novo CSV (troca automática) - Upload de logo -- Editar todas as configurações (Meet ID, Auth String, etc) sem tocar em ficheiros -As mudanças levam efeito imediatamente. +Nota: a configuração (Meet ID, Auth String, etc.) é lida do `.env` e não pode ser editada via UI. ## Ficheiro CSV @@ -143,13 +139,14 @@ O matching de nomes é flexible: Se usas Docker/Coolify: - Mount `src/data/` e `public/branding/` como volumes, assim podes trocar ficheiros sem rebuild -- Variáveis de ambiente: a app lê de `src/config/config.json`, portanto não precisa .env (mas podes adicionar se quiseres) +- Define as variáveis de ambiente (MEET_ID, CSV_FILE_NAME, AUTH_STRING, ADMIN_PASSWORD, COMPETITION_NAME) - Build: `npm run build`, run: `npm start` Se usas Vercel: - Coloca o CSV em `src/data/` antes de fazer push - A logo em `public/branding/logo.png` +- Define as variáveis de ambiente no painel da Vercel - Deploy automático no push ## Desenvolvimento @@ -166,7 +163,7 @@ Adiciona `"use client"` no topo de ficheiros que usem useState, useEffect, event **"Não conseguo fazer upload do CSV"**: Verifica se o ficheiro está bem formatado (CSV simples, não Excel). A password de admin está correcta? -**"Ninguém consegue aceder à form"**: Verifica se o auth param na URL é igual ao `authString` em config.json. Gera um QR code e testa. +**"Ninguém consegue aceder à form"**: Verifica se o auth param na URL é igual ao `AUTH_STRING` no `.env`. Gera um QR code e testa. **"A logo não aparece"**: Coloca o ficheiro em `public/branding/logo.png` (ou outro nome) e actualiza o código se o nome for diferente. diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 8b9b057..4c5c045 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -8,30 +8,20 @@ type Stats = { csvFiles: string[] logoFiles: string[] meetId: string - error?: string -} - -type Config = { - meetId: string - csvFileName: string - authString: string - adminPassword: string competitionName: string + authString: string + error?: string } export default function AdminPage() { const [password, setPassword] = useState('') const [authenticated, setAuthenticated] = useState(false) const [stats, setStats] = useState(null) - const [config, setConfig] = useState(null) const [loading, setLoading] = useState(false) const [message, setMessage] = useState('') const [csvFile, setCsvFile] = useState(null) const [logoFile, setLogoFile] = useState(null) const [uploading, setUploading] = useState(false) - const [editingConfig, setEditingConfig] = useState(false) - const [configForm, setConfigForm] = useState(null) - const [savingConfig, setSavingConfig] = useState(false) async function handleLogin(e: React.FormEvent) { e.preventDefault() @@ -46,8 +36,6 @@ export default function AdminPage() { } const data = await res.json() setStats(data) - setConfig(data) - setConfigForm(data) setAuthenticated(true) } catch { setMessage('❌ Erro ao conectar') @@ -207,132 +195,23 @@ export default function AdminPage() {

Configuração

- {editingConfig && configForm ? ( -
{ - e.preventDefault() - setSavingConfig(true) - setMessage('') - try { - const res = await fetch('/api/admin/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'x-admin-password': password }, - body: JSON.stringify(configForm) - }) - const data = await res.json() - if (res.ok) { - setMessage(data.message || '✓ Configuração atualizada') - setConfig(configForm) - setEditingConfig(false) - } else { - setMessage(`❌ ${data.error}`) - } - } catch { - setMessage('❌ Erro ao salvar configuração') - } finally { - setSavingConfig(false) - } - }} - className="space-y-4" - > -
- - setConfigForm({ ...configForm, meetId: e.target.value })} - className="w-full rounded-md border border-gray-300 px-3 py-2" - disabled={savingConfig} - /> -
-
- - setConfigForm({ ...configForm, csvFileName: e.target.value })} - className="w-full rounded-md border border-gray-300 px-3 py-2" - disabled={savingConfig} - /> -
-
- - setConfigForm({ ...configForm, authString: e.target.value })} - className="w-full rounded-md border border-gray-300 px-3 py-2" - disabled={savingConfig} - /> -
-
- - setConfigForm({ ...configForm, adminPassword: e.target.value })} - className="w-full rounded-md border border-gray-300 px-3 py-2" - disabled={savingConfig} - /> -
-
- - setConfigForm({ ...configForm, competitionName: e.target.value })} - className="w-full rounded-md border border-gray-300 px-3 py-2" - disabled={savingConfig} - /> -
-
- - -
- {message && ( -

{message}

- )} -
- ) : ( -
-
-

- Meet ID: {config?.meetId} -

-

- CSV: {config?.csvFileName} -

-

- Auth String: {config?.authString} -

-

- Competition: {config?.competitionName} -

-
- -
- )} +
+

+ Competition: {stats?.competitionName || '—'} +

+

+ Meet ID: {stats?.meetId || '—'} +

+

+ CSV: {stats?.csvFileName || '—'} +

+

+ Auth String: {stats?.authString || '—'} +

+
+

+ Estes valores agora são definidos no ficheiro .env e não podem ser alterados na UI. +

diff --git a/src/app/api/admin/config/route.ts b/src/app/api/admin/config/route.ts index bdf64f1..483c9a2 100644 --- a/src/app/api/admin/config/route.ts +++ b/src/app/api/admin/config/route.ts @@ -1,6 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import { writeFile } from 'node:fs/promises' -import { CONFIG_PATH, readConfig } from '@/utils/config' +import { readConfig } from '@/utils/config' export const runtime = 'nodejs' @@ -22,30 +21,8 @@ export async function GET(req: NextRequest) { } export async function POST(req: NextRequest) { - const password = req.headers.get('x-admin-password') || '' - const config = await readConfig() - if (password !== config.adminPassword) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - try { - const body = (await req.json()) as Partial - - if (!body.meetId || !body.csvFileName || !body.authString || !body.adminPassword || !body.competitionName) { - return NextResponse.json({ error: 'All fields are required' }, { status: 400 }) - } - - const newConfig: ConfigData = { - meetId: body.meetId, - csvFileName: body.csvFileName, - authString: body.authString, - adminPassword: body.adminPassword, - competitionName: body.competitionName - } - - await writeFile(CONFIG_PATH, JSON.stringify(newConfig, null, 2)) - return NextResponse.json({ success: true, message: '✓ Configuração atualizada com sucesso!' }) - } catch (e: any) { - return NextResponse.json({ error: `Failed to save config: ${e.message}` }, { status: 500 }) - } + return NextResponse.json( + { error: 'Config is read-only. Update values via environment variables.' }, + { status: 405 } + ) } diff --git a/src/app/api/competition-name/route.ts b/src/app/api/competition-name/route.ts index a1fd588..c18d09d 100644 --- a/src/app/api/competition-name/route.ts +++ b/src/app/api/competition-name/route.ts @@ -1,14 +1,11 @@ import { NextResponse } from 'next/server' -import { readFile } from 'node:fs/promises' -import path from 'node:path' +import { readConfig } from '@/utils/config' export const runtime = 'nodejs' export async function GET() { try { - const configPath = path.join(process.cwd(), 'src', 'config', 'config.json') - const content = await readFile(configPath, 'utf8') - const data = JSON.parse(content) + const data = await readConfig() return NextResponse.json({ competitionName: data.competitionName }) } catch { return NextResponse.json({ competitionName: 'Altarra' }) diff --git a/src/config/config.json.example b/src/config/config.json.example deleted file mode 100644 index 736f130..0000000 --- a/src/config/config.json.example +++ /dev/null @@ -1,7 +0,0 @@ -{ - "meetId": "LIFTINGCAST_MEET_ID", - "csvFileName": "CSV_FILE_NAME", - "authString": "AUTH_STRING", - "adminPassword": "ADMIN_PASSWORD", - "competitionName": "COMPETITION_NAME" -} diff --git a/src/services/adminStats.ts b/src/services/adminStats.ts index 3f0f476..baf7dde 100644 --- a/src/services/adminStats.ts +++ b/src/services/adminStats.ts @@ -23,7 +23,9 @@ export async function getLifterStats() { csvFileName: config.csvFileName, csvFiles, logoFiles, - meetId: config.meetId + meetId: config.meetId, + competitionName: config.competitionName, + authString: config.authString } } catch (error) { return { @@ -32,6 +34,8 @@ export async function getLifterStats() { csvFiles: [], logoFiles: [], meetId: config.meetId, + competitionName: config.competitionName, + authString: config.authString, error: 'Failed to load stats' } } diff --git a/src/utils/config.ts b/src/utils/config.ts index 8b59336..198392c 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,6 +1,3 @@ -import { readFile } from 'node:fs/promises' -import path from 'node:path' - export type AppConfig = { meetId: string csvFileName: string @@ -9,9 +6,20 @@ export type AppConfig = { competitionName: string } -export const CONFIG_PATH = path.join(process.cwd(), 'src', 'config', 'config.json') +function getEnv(name: string) { + const value = process.env[name] + if (!value) { + throw new Error(`Missing required env: ${name}`) + } + return value +} export async function readConfig(): Promise { - const content = await readFile(CONFIG_PATH, 'utf8') - return JSON.parse(content) as AppConfig + return { + meetId: getEnv('MEET_ID'), + csvFileName: getEnv('CSV_FILE_NAME'), + authString: getEnv('AUTH_STRING'), + adminPassword: getEnv('ADMIN_PASSWORD'), + competitionName: getEnv('COMPETITION_NAME') + } }