diff --git a/.gitignore b/.gitignore
index 3c1d217..c2814ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,9 @@ node_modules
out
dist
.DS_Store
-config.json
-*.csv
+src/config/config.json
+src/data/*.csv
+public/branding/*.png
+public/branding/*.jpg
+public/branding/*.jpeg
+public/branding/*.svg
diff --git a/package-lock.json b/package-lock.json
index 6fccc01..e7f1655 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,13 +1,14 @@
{
- "name": "heights",
+ "name": "altarra",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "heights",
+ "name": "altarra",
"version": "0.1.0",
"dependencies": {
+ "lucide-react": "^0.562.0",
"next": "14.1.0",
"papaparse": "5.4.1",
"react": "18.2.0",
@@ -3834,6 +3835,15 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/lucide-react": {
+ "version": "0.562.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
+ "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
diff --git a/package.json b/package.json
index e5a99b1..bcc3448 100644
--- a/package.json
+++ b/package.json
@@ -4,22 +4,24 @@
"private": true,
"scripts": {
"dev": "next dev",
+ "prebuild": "node scripts/setup-config.js",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
+ "lucide-react": "^0.562.0",
"next": "14.1.0",
- "react": "18.2.0",
- "react-dom": "18.2.0",
"papaparse": "5.4.1",
- "react-checkmark": "2.1.1"
+ "react": "18.2.0",
+ "react-checkmark": "2.1.1",
+ "react-dom": "18.2.0"
},
"devDependencies": {
"@types/node": "20.10.6",
+ "@types/papaparse": "5.3.14",
"@types/react": "18.2.37",
"@types/react-dom": "18.2.15",
- "@types/papaparse": "5.3.14",
"autoprefixer": "10.4.16",
"eslint": "8.55.0",
"eslint-config-next": "14.1.0",
diff --git a/scripts/setup-config.js b/scripts/setup-config.js
new file mode 100644
index 0000000..9139c36
--- /dev/null
+++ b/scripts/setup-config.js
@@ -0,0 +1,37 @@
+#!/usr/bin/env node
+
+/**
+ * Setup script: creates config.json from example if it doesn't exist.
+ * Runs during build phase.
+ */
+
+const fs = require('fs')
+const path = require('path')
+
+const projectRoot = path.join(__dirname, '..')
+const configPath = path.join(projectRoot, 'src', 'config', 'config.json')
+const examplePath = path.join(projectRoot, 'src', 'config', 'config.json.example')
+
+// Check if config.json exists
+if (fs.existsSync(configPath)) {
+ console.log('✓ config.json found')
+ process.exit(0)
+}
+
+// Check if example exists
+if (!fs.existsSync(examplePath)) {
+ console.error('✗ config.json.example not found at', examplePath)
+ process.exit(1)
+}
+
+// Copy example to config.json
+try {
+ const exampleContent = fs.readFileSync(examplePath, 'utf8')
+ fs.writeFileSync(configPath, exampleContent)
+ console.log('✓ config.json created from example')
+ process.exit(0)
+} catch (err) {
+ console.error('✗ Failed to create config.json:', err.message)
+ process.exit(1)
+}
+
diff --git a/src/app/api/admin/upload/route.ts b/src/app/api/admin/upload/route.ts
index 0de1a8b..ad93ab2 100644
--- a/src/app/api/admin/upload/route.ts
+++ b/src/app/api/admin/upload/route.ts
@@ -4,7 +4,6 @@ import path from 'node:path'
import config from '@/config/config.json'
export const runtime = 'nodejs'
-export const config_edge = { maxDuration: 30 }
export async function POST(req: NextRequest) {
const password = req.headers.get('x-admin-password') || ''
diff --git a/src/app/api/competition-name/route.ts b/src/app/api/competition-name/route.ts
new file mode 100644
index 0000000..a1fd588
--- /dev/null
+++ b/src/app/api/competition-name/route.ts
@@ -0,0 +1,16 @@
+import { NextResponse } from 'next/server'
+import { readFile } from 'node:fs/promises'
+import path from 'node:path'
+
+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)
+ return NextResponse.json({ competitionName: data.competitionName })
+ } catch {
+ return NextResponse.json({ competitionName: 'Altarra' })
+ }
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 2363c1a..c245e18 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -2,6 +2,7 @@ import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Footer from '@/components/Footer'
+import { LanguageProvider } from '@/context/LanguageContext'
const inter = Inter({ subsets: ['latin'] })
@@ -14,8 +15,12 @@ export default function RootLayout({ children }: { children: React.ReactNode })
return (
- {children}
-
+
+
+ {children}
+
+
+
)
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 48ce151..f7eb700 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,108 +1,5 @@
-"use client"
-
-import { useEffect, useMemo, useRef, useState } from 'react'
-import Image from 'next/image'
-import LandingPage from '@/components/LandingPage'
-import IdentityForm from '@/components/IdentityForm'
-import RackHeightsForm from '@/components/RackHeightsForm'
-import Spinner from '@/components/Spinner'
+import HomePageContent from '@/components/HomePage'
export default function HomePage() {
- const [authorized, setAuthorized] = useState(null)
- const [memberNumber, setMemberNumber] = useState(null)
- const [birthDate, setBirthDate] = useState(null)
- const [lifterName, setLifterName] = useState(null)
- const [competitionName, setCompetitionName] = useState('Altarra')
-
- const search = useMemo(() => new URLSearchParams(typeof window !== 'undefined' ? window.location.search : ''), [])
- const auth = search.get('auth') || ''
-
- useEffect(() => {
- let mounted = true
- async function check() {
- try {
- const res = await fetch(`/api/auth?auth=${encodeURIComponent(auth)}`)
- if (mounted) setAuthorized(res.ok)
- } catch {
- if (mounted) setAuthorized(false)
- }
- // Load competition name from config
- try {
- const configRes = await fetch('/api/admin/config', {
- headers: { 'x-admin-password': '' }
- }).catch(() => null)
- if (configRes && configRes.ok) {
- const data = await configRes.json()
- if (mounted && data.competitionName) {
- setCompetitionName(data.competitionName)
- }
- }
- } catch {
- // Fallback to default
- }
- }
- check()
- return () => { mounted = false }
- }, [auth])
-
- if (authorized === null) {
- return (
-
-
-
- )
- }
-
- if (!authorized) {
- return
- }
-
- return (
-
-
-
-
-
-
-
{competitionName}
-
Submissão de Alturas
-
-
-
- {!memberNumber || !birthDate ? (
- {
- setMemberNumber(memberNumber)
- setBirthDate(birthDate)
- setLifterName(name)
- }}
- />
- ) : (
-
-
Identificad@ como: {lifterName}
-
{
- // Return to step 1
- setMemberNumber(null)
- setBirthDate(null)
- setLifterName(null)
- }}
- />
-
- )}
-
-
- )
+ return
}
diff --git a/src/components/FormField.tsx b/src/components/FormField.tsx
index fd50883..02f2f47 100644
--- a/src/components/FormField.tsx
+++ b/src/components/FormField.tsx
@@ -1,6 +1,7 @@
"use client"
import { useId } from 'react'
+import { AlertCircle } from 'lucide-react'
type Props = {
label: string
@@ -32,13 +33,15 @@ export default function FormField({
const id = useId()
return (
)
diff --git a/src/components/HomePage.tsx b/src/components/HomePage.tsx
new file mode 100644
index 0000000..c53de07
--- /dev/null
+++ b/src/components/HomePage.tsx
@@ -0,0 +1,148 @@
+"use client"
+
+import { useEffect, useMemo, useRef, useState } from 'react'
+import Image from 'next/image'
+import LandingPage from '@/components/LandingPage'
+import IdentityForm from '@/components/IdentityForm'
+import RackHeightsForm from '@/components/RackHeightsForm'
+import Spinner from '@/components/Spinner'
+import { useLanguage } from '@/context/LanguageContext'
+import { t } from '@/utils/translations'
+
+export default function HomePageContent() {
+ const { language, setLanguage } = useLanguage()
+ const [authorized, setAuthorized] = useState(null)
+ const [memberNumber, setMemberNumber] = useState(null)
+ const [birthDate, setBirthDate] = useState(null)
+ const [lifterName, setLifterName] = useState(null)
+ const [competitionName, setCompetitionName] = useState('Altarra')
+
+ useEffect(() => {
+ let mounted = true
+ let timeoutId: NodeJS.Timeout
+
+ async function check() {
+ const search = new URLSearchParams(window.location.search)
+ const auth = search.get('auth') || ''
+
+ try {
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), 5000)
+
+ const res = await fetch(`/api/auth?auth=${encodeURIComponent(auth)}`, {
+ signal: controller.signal
+ })
+ clearTimeout(timeoutId)
+ console.log('Auth response:', res.ok, res.status)
+ if (mounted) setAuthorized(res.ok)
+ } catch (err) {
+ console.error('Auth error:', err)
+ if (mounted) setAuthorized(false)
+ }
+
+ // Load competition name from config
+ try {
+ const configRes = await fetch('/api/competition-name')
+ if (configRes.ok) {
+ const data = await configRes.json()
+ if (mounted && data.competitionName) {
+ setCompetitionName(data.competitionName)
+ }
+ }
+ } catch {
+ // Fallback to default
+ }
+ }
+
+ check()
+ return () => {
+ mounted = false
+ clearTimeout(timeoutId)
+ }
+ }, [])
+
+ if (authorized === null) {
+ return (
+
+
+
+ )
+ }
+
+ if (!authorized) {
+ return
+ }
+
+ return (
+
+
+
+ {!memberNumber || !birthDate ? (
+ {
+ setMemberNumber(memberNumber)
+ setBirthDate(birthDate)
+ setLifterName(name)
+ }}
+ />
+ ) : (
+
+
{t('identified_as', language)} {lifterName}
+
{
+ // Return to step 1
+ setMemberNumber(null)
+ setBirthDate(null)
+ setLifterName(null)
+ }}
+ />
+
+ )}
+
+
+ )
+}
diff --git a/src/components/IdentityForm.tsx b/src/components/IdentityForm.tsx
index 05c4c06..a6d6f27 100644
--- a/src/components/IdentityForm.tsx
+++ b/src/components/IdentityForm.tsx
@@ -4,19 +4,31 @@ import { useRef, useState } from 'react'
import FormField from '@/components/FormField'
import SubmitButton from '@/components/SubmitButton'
import StatusMessage from '@/components/StatusMessage'
-import { errorMessages, isValidDateDDMMYYYY } from '@/utils/validation'
+import { isValidDateDDMMYYYY } from '@/utils/validation'
+import { useLanguage } from '@/context/LanguageContext'
+import { t, translations } from '@/utils/translations'
type Props = {
onVerified: (data: { name: string; birthDate: string; memberNumber: string }) => void
}
export default function IdentityForm({ onVerified }: Props) {
+ const { language } = useLanguage()
const [name, setName] = useState('')
const [birthDate, setBirthDate] = useState('')
const [loading, setLoading] = useState(false)
const [status, setStatus] = useState<{ type: 'idle' | 'success' | 'error'; msg: string | null }>({ type: 'idle', msg: null })
const nameRef = useRef(null)
+ const errorMessages = {
+ no_match: t('error_no_match', language),
+ multiple_matches: t('error_multiple_matches', language),
+ network_error: t('error_network', language),
+ api_error: t('error_api', language),
+ validation_error: t('error_validation', language),
+ date_format_error: t('error_date_format', language)
+ }
+
async function onSubmit(e: React.FormEvent) {
e.preventDefault()
setStatus({ type: 'idle', msg: null })
@@ -43,7 +55,7 @@ export default function IdentityForm({ onVerified }: Props) {
return
}
const data = (await res.json()) as { lifter: { name: string; birthDate: string; memberNumber: string } }
- setStatus({ type: 'success', msg: '✓ Identificação confirmada.' })
+ setStatus({ type: 'success', msg: t('identification_success', language) })
onVerified(data.lifter)
} catch {
setStatus({ type: 'error', msg: errorMessages.network_error })
@@ -54,22 +66,22 @@ export default function IdentityForm({ onVerified }: Props) {
return (
)
diff --git a/src/components/LandingPage.tsx b/src/components/LandingPage.tsx
index c58cb4b..1943f19 100644
--- a/src/components/LandingPage.tsx
+++ b/src/components/LandingPage.tsx
@@ -2,8 +2,12 @@
import Link from 'next/link'
import Image from 'next/image'
+import { ExternalLink } from 'lucide-react'
+import { useLanguage } from '@/context/LanguageContext'
+import { t } from '@/utils/translations'
export default function LandingPage() {
+ const { language } = useLanguage()
return (
@@ -16,16 +20,17 @@ export default function LandingPage() {
priority
/>
-
- Boas-vindas ao Altarra
-
- Leia o QR Code na área de treino para inserir as suas alturas.
+
+ {t('welcome_title', language)}
+
+ {t('welcome_text', language)}
- Ir para powerlifting.pt
+ {t('visit_website', language)}
+
diff --git a/src/components/RackHeightsForm.tsx b/src/components/RackHeightsForm.tsx
index 6ca179c..bb5e8f6 100644
--- a/src/components/RackHeightsForm.tsx
+++ b/src/components/RackHeightsForm.tsx
@@ -1,12 +1,16 @@
"use client"
-import { useEffect, useRef, useState } from 'react'
-import { Checkmark } from 'react-checkmark'
+import { useEffect, useRef, useState, lazy, Suspense } from 'react'
+import dynamic from 'next/dynamic'
import FormField from '@/components/FormField'
import SubmitButton from '@/components/SubmitButton'
import StatusMessage from '@/components/StatusMessage'
import type { RackHeightsPayload } from '@/types/rackHeights'
-import { errorMessages, required } from '@/utils/validation'
+import { required } from '@/utils/validation'
+import { useLanguage } from '@/context/LanguageContext'
+import { t } from '@/utils/translations'
+
+const Checkmark = dynamic(() => import('react-checkmark').then(mod => ({ default: mod.Checkmark })), { ssr: false })
type Props = {
memberNumber: string
@@ -15,6 +19,7 @@ type Props = {
}
export default function RackHeightsForm({ memberNumber, birthDate, onSuccess }: Props) {
+ const { language } = useLanguage()
const [squatRackHeight, setSquatRackHeight] = useState('')
const [squatRackInOut, setSquatRackInOut] = useState('')
const [benchRackHeight, setBenchRackHeight] = useState('')
@@ -25,6 +30,12 @@ export default function RackHeightsForm({ memberNumber, birthDate, onSuccess }:
const [submitted, setSubmitted] = useState(false)
const firstFieldRef = useRef(null)
+ const errorMessages = {
+ validation_error: t('error_validation', language),
+ network_error: t('error_network', language),
+ api_error: t('error_api', language)
+ }
+
useEffect(() => {
firstFieldRef.current?.focus()
}, [])
@@ -59,7 +70,7 @@ export default function RackHeightsForm({ memberNumber, birthDate, onSuccess }:
setStatus({ type: 'error', msg })
return
}
- setStatus({ type: 'success', msg: '✓ Alturas submetidas com sucesso!' })
+ setStatus({ type: 'success', msg: t('submission_success', language) })
setSubmitted(true)
} catch {
setStatus({ type: 'error', msg: errorMessages.network_error })
@@ -83,27 +94,27 @@ export default function RackHeightsForm({ memberNumber, birthDate, onSuccess }:
{submitted ? (
-
-
-
+
+
+
-
As suas alturas foram submetidas.
-
As alturas foram enviadas e já foram alteradas. Se precisar de alterar as alturas outra vez, pode voltar a submeter este formulário.
+
{t('heights_submitted_title', language)}
+
{t('heights_submitted_text', language)}
) : (
diff --git a/src/components/StatusMessage.tsx b/src/components/StatusMessage.tsx
index b42a6c6..59381f8 100644
--- a/src/components/StatusMessage.tsx
+++ b/src/components/StatusMessage.tsx
@@ -1,5 +1,7 @@
"use client"
+import { CheckCircle2, AlertCircle, Info } from 'lucide-react'
+
type Props = {
status: 'idle' | 'success' | 'error' | 'info'
message: string | null
@@ -7,17 +9,36 @@ type Props = {
export default function StatusMessage({ status, message }: Props) {
if (!message) return null
- const color =
- status === 'success' ? 'text-green-700 bg-green-50 border-green-200' :
- status === 'error' ? 'text-red-700 bg-red-50 border-red-200' :
- 'text-gray-700 bg-gray-50 border-gray-200'
+
+ const config = {
+ success: {
+ color: 'bg-[#f0fdf4] border-[#006600] text-[#004d00]',
+ icon:
+ },
+ error: {
+ color: 'bg-[#fef2f2] border-[#FF0000] text-[#991b1b]',
+ icon:
+ },
+ info: {
+ color: 'bg-[#f3f4f6] border-[#6b7280] text-[#374151]',
+ icon:
+ },
+ idle: {
+ color: 'bg-[#f3f4f6] border-[#6b7280] text-[#374151]',
+ icon:
+ }
+ }
+
+ const current = config[status]
+
return (
- {message}
+ {current.icon}
+ {message}
)
}
diff --git a/src/components/SubmitButton.tsx b/src/components/SubmitButton.tsx
index 0ded7f4..dd7b0ad 100644
--- a/src/components/SubmitButton.tsx
+++ b/src/components/SubmitButton.tsx
@@ -1,5 +1,7 @@
"use client"
+import { CheckCircle2 } from 'lucide-react'
+
type Props = {
children: React.ReactNode
loading?: boolean
@@ -10,12 +12,14 @@ export default function SubmitButton({ children, loading, disabled }: Props) {
return (
)
diff --git a/src/context/LanguageContext.tsx b/src/context/LanguageContext.tsx
new file mode 100644
index 0000000..5493649
--- /dev/null
+++ b/src/context/LanguageContext.tsx
@@ -0,0 +1,49 @@
+'use client'
+
+import { createContext, useContext, useState, useEffect } from 'react'
+import type { Language } from '@/utils/translations'
+
+type LanguageContextType = {
+ language: Language
+ setLanguage: (lang: Language) => void
+}
+
+const LanguageContext = createContext
({
+ language: 'pt',
+ setLanguage: () => {}
+})
+
+export function LanguageProvider({ children }: { children: React.ReactNode }) {
+ const [language, setLanguageState] = useState('pt')
+
+ useEffect(() => {
+ // Load language from localStorage
+ const saved = localStorage.getItem('language') as Language | null
+ if (saved && ['pt', 'en'].includes(saved)) {
+ setLanguageState(saved)
+ } else {
+ // Detect browser language
+ const browserLang = navigator.language.toLowerCase()
+ if (browserLang.startsWith('en')) {
+ setLanguageState('en')
+ } else {
+ setLanguageState('pt')
+ }
+ }
+ }, [])
+
+ const setLanguage = (lang: Language) => {
+ setLanguageState(lang)
+ localStorage.setItem('language', lang)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export function useLanguage() {
+ return useContext(LanguageContext)
+}
diff --git a/src/react-checkmark.d.ts b/src/react-checkmark.d.ts
new file mode 100644
index 0000000..7028c32
--- /dev/null
+++ b/src/react-checkmark.d.ts
@@ -0,0 +1,9 @@
+declare module 'react-checkmark' {
+ import { ReactNode } from 'react'
+
+ interface CheckmarkProps {
+ [key: string]: any
+ }
+
+ export const Checkmark: React.FC
+}
diff --git a/src/utils/translations.ts b/src/utils/translations.ts
new file mode 100644
index 0000000..201f59c
--- /dev/null
+++ b/src/utils/translations.ts
@@ -0,0 +1,84 @@
+export type Language = 'pt' | 'en'
+
+export const translations = {
+ pt: {
+ // Navigation
+ logout: 'Sair',
+ // Landing page
+ welcome_title: 'Boas-vindas ao Altarra',
+ welcome_text: 'Leia o QR Code na área de treino para inserir as suas alturas.',
+ visit_website: 'Ir para powerlifting.pt',
+ // Forms
+ step_1_identification: 'Passo 1: Identificação',
+ step_2_heights: 'Passo 2: Alturas de Rack',
+ full_name: 'Nome Completo',
+ full_name_placeholder: 'Ex: João Silva',
+ birth_date: 'Data de Nascimento',
+ birth_date_placeholder: 'DD/MM/AAAA',
+ search: 'Procurar',
+ squat_rack_height: 'Altura do Rack de Agachamento',
+ bench_rack_height: 'Altura do Rack de Supino',
+ bench_foot_blocks: 'Blocos para os Pés no Supino',
+ foot_blocks_none: 'Nenhum',
+ submit_heights: 'Submeter Alturas',
+ submit_another: 'Submeter outro atleta ou Alterar alturas',
+ identified_as: 'Identificad@ como:',
+ competition: 'Competição',
+ heights_submitted_title: 'As suas alturas foram submetidas.',
+ heights_submitted_text: 'As alturas foram enviadas e já foram alteradas. Se precisar de alterar as alturas outra vez, pode voltar a submeter este formulário.',
+ heights_submission_loading: 'A enviar...',
+ // Errors
+ error_no_match: 'Não foi possível encontrar o seu nome. O nome está igual ao LiftingCast?',
+ error_multiple_matches: 'Erro inesperado - fale com a equipa da APP.',
+ error_network: 'Occoreu um erro de ligação. Tem rede/wi-fi?',
+ error_api: 'Erro ao submeter. Por favor, tente novamente ou contacte a equipa.',
+ error_validation: 'Por favor, preenche todos os campos obrigatórios.',
+ error_date_format: 'Formato de data inválido. Use DD/MM/AAAA.',
+ // Other
+ loading: 'A carregar…',
+ submission_success: '✓ Alturas submetidas com sucesso!',
+ identification_success: '✓ Identificação confirmada.'
+ },
+ en: {
+ // Navigation
+ logout: 'Logout',
+ // Landing page
+ welcome_title: 'Welcome to Altarra',
+ welcome_text: 'Scan the QR Code in the training area to enter your heights.',
+ visit_website: 'Go to powerlifting.pt',
+ // Forms
+ step_1_identification: 'Step 1: Identification',
+ step_2_heights: 'Step 2: Rack Heights',
+ full_name: 'Full Name',
+ full_name_placeholder: 'Ex: John Smith',
+ birth_date: 'Birth Date',
+ birth_date_placeholder: 'DD/MM/YYYY',
+ search: 'Search',
+ squat_rack_height: 'Squat Rack Height',
+ bench_rack_height: 'Bench Rack Height',
+ bench_foot_blocks: 'Foot Blocks for Bench',
+ foot_blocks_none: 'None',
+ submit_heights: 'Submit Heights',
+ submit_another: 'Submit another athlete or Change heights',
+ identified_as: 'Identified as:',
+ competition: 'Competition',
+ heights_submitted_title: 'Your heights have been submitted.',
+ heights_submitted_text: 'The heights have been sent and have already been changed. If you need to change the heights again, you can resubmit this form.',
+ heights_submission_loading: 'Sending...',
+ // Errors
+ error_no_match: 'Could not find your name. Is the name the same as in LiftingCast?',
+ error_multiple_matches: 'Unexpected error - contact the app team.',
+ error_network: 'A connection error occurred. Do you have internet/wi-fi?',
+ error_api: 'Error submitting. Please try again or contact the team.',
+ error_validation: 'Please fill in all required fields.',
+ error_date_format: 'Invalid date format. Use DD/MM/YYYY.',
+ // Other
+ loading: 'Loading…',
+ submission_success: '✓ Heights submitted successfully!',
+ identification_success: '✓ Identification confirmed.'
+ }
+}
+
+export function t(key: keyof typeof translations.pt, lang: Language): string {
+ return translations[lang][key]
+}
diff --git a/src/utils/validation.ts b/src/utils/validation.ts
index 3e93b04..4273a45 100644
--- a/src/utils/validation.ts
+++ b/src/utils/validation.ts
@@ -1,10 +1,10 @@
export const errorMessages = {
- no_match: 'Não foi possível encontrar o seu nome. Por favor, verifique se está igual ao LiftingCast e tente novamente.',
- multiple_matches: 'Por favor fala com a equipa da APP.',
- network_error: 'Occoreu um erro de ligação. Tente outravez.',
+ no_match: 'Não foi possível encontrar o seu nome. O nome está igual ao LiftingCast?',
+ multiple_matches: 'Erro inesperado - fale com a equipa da APP.',
+ network_error: 'Occoreu um erro de ligação. Tem rede/wi-fi?',
api_error: 'Erro ao submeter. Por favor, tente novamente ou contacte a equipa.',
- validation_error: 'Por favor, preencha todos os campos obrigatórios.',
- date_format_error: 'Formato de data inválido. Use DD/MM/YYYY.'
+ validation_error: 'Por favor, preenche todos os campos obrigatórios.',
+ date_format_error: 'Formato de data inválido. Use DD/MM/AAAA.'
}
export function isValidDateDDMMYYYY(value: string): boolean {
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 8d34050..dc3f3fb 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -15,6 +15,10 @@ const config: Config = {
600: '#006600',
700: '#004d00',
accent: '#ff0000'
+ },
+ pt: {
+ red: '#FF0000',
+ green: '#006600'
}
}
}
diff --git a/tsconfig.json b/tsconfig.json
index df3cb36..fa1bcd3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,6 +21,9 @@
"@/components/*": [
"src/components/*"
],
+ "@/context/*": [
+ "src/context/*"
+ ],
"@/services/*": [
"src/services/*"
],