ui update
This commit is contained in:
parent
d121596c8f
commit
737998bd6d
15 changed files with 1287 additions and 266 deletions
92
MUI_MIGRATION.md
Normal file
92
MUI_MIGRATION.md
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# Material-UI Migration Summary
|
||||||
|
|
||||||
|
This document outlines the migration of the Altarra app from Tailwind CSS to Material-UI (MUI).
|
||||||
|
|
||||||
|
## Dependencies Added
|
||||||
|
|
||||||
|
- `@mui/material@^5.14.20`
|
||||||
|
- `@mui/icons-material@^5.14.20`
|
||||||
|
- `@emotion/react@^11.11.1`
|
||||||
|
- `@emotion/styled@^11.11.0`
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Theme Setup
|
||||||
|
- Created `src/context/ThemeProvider.tsx` - Custom MUI theme with:
|
||||||
|
- Primary color: `#006600` (green)
|
||||||
|
- Secondary color: `#FF0000` (red)
|
||||||
|
- Custom typography and component overrides
|
||||||
|
|
||||||
|
### 2. Layout
|
||||||
|
- Updated `src/app/layout.tsx` to:
|
||||||
|
- Use `ThemeProvider` wrapper
|
||||||
|
- Remove Tailwind CSS import
|
||||||
|
- Use MUI `Box` for flex layouts
|
||||||
|
|
||||||
|
### 3. Component Migration
|
||||||
|
|
||||||
|
#### FormField.tsx
|
||||||
|
- Replaced custom input with `TextField` component
|
||||||
|
- Automatic error handling with `helperText` prop
|
||||||
|
- Integrated label and validation styling
|
||||||
|
|
||||||
|
#### SubmitButton.tsx
|
||||||
|
- Replaced custom button with `Button` component
|
||||||
|
- Used `startIcon` for loading spinner (`CircularProgress`)
|
||||||
|
- Automatic disabled state styling
|
||||||
|
|
||||||
|
#### StatusMessage.tsx
|
||||||
|
- Replaced custom alerts with `Alert` component
|
||||||
|
- Automatic severity-based styling (success, error, info)
|
||||||
|
|
||||||
|
#### IdentityForm.tsx
|
||||||
|
- Used `Box` component for form wrapper
|
||||||
|
- Used `Typography` for headings
|
||||||
|
- FormField components integrated
|
||||||
|
|
||||||
|
#### RackHeightsForm.tsx
|
||||||
|
- Replaced form inputs with FormField components
|
||||||
|
- Used `Alert` for success message
|
||||||
|
- Used `Button` for secondary actions
|
||||||
|
- Replaced custom spinner with MUI `CircularProgress`
|
||||||
|
|
||||||
|
#### HomePage.tsx
|
||||||
|
- Used `Box`, `Paper`, `Typography`, and `Button` components
|
||||||
|
- Language toggle buttons use variant-based styling
|
||||||
|
- Logo and layout uses MUI's sx prop system
|
||||||
|
|
||||||
|
#### LandingPage.tsx
|
||||||
|
- Used `Paper` component for card styling
|
||||||
|
- Used `Button` with link functionality
|
||||||
|
- Replaced Lucide icon with `OpenInNewIcon` from `@mui/icons-material`
|
||||||
|
|
||||||
|
#### Footer.tsx
|
||||||
|
- Used `Box`, `Typography`, and `Container` components
|
||||||
|
- Maintained styling with MUI theming
|
||||||
|
|
||||||
|
#### Spinner.tsx
|
||||||
|
- Replaced custom spinner with `CircularProgress` component
|
||||||
|
|
||||||
|
## Styling Approach
|
||||||
|
|
||||||
|
All components now use MUI's `sx` prop system, which provides:
|
||||||
|
- Type-safe styling
|
||||||
|
- Direct access to theme values
|
||||||
|
- Responsive design support
|
||||||
|
- Better performance
|
||||||
|
|
||||||
|
## Migration Benefits
|
||||||
|
|
||||||
|
1. **Consistency**: Unified design system across the app
|
||||||
|
2. **Accessibility**: Better built-in a11y features
|
||||||
|
3. **Theming**: Centralized theme management
|
||||||
|
4. **Components**: Rich set of pre-built components
|
||||||
|
5. **Developer Experience**: Better TypeScript support and IDE integration
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Tailwind CSS configuration can be safely removed if no longer needed
|
||||||
|
- The app maintains the same visual appearance and functionality
|
||||||
|
- All forms and interactive elements work as before
|
||||||
|
- Language switching is preserved
|
||||||
|
- Responsive design is maintained through MUI's breakpoint system
|
||||||
742
package-lock.json
generated
742
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,6 +9,10 @@
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.11.1",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/material": "^5.14.20",
|
||||||
|
"@mui/icons-material": "^5.14.20",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "14.1.0",
|
"next": "14.1.0",
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import './globals.css'
|
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { Inter } from 'next/font/google'
|
|
||||||
import Footer from '@/components/Footer'
|
import Footer from '@/components/Footer'
|
||||||
import { LanguageProvider } from '@/context/LanguageContext'
|
import { LanguageProvider } from '@/context/LanguageContext'
|
||||||
|
import { ThemeProvider } from '@/context/ThemeProvider'
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Altarra - Submissão de Alturas',
|
title: 'Altarra - Submissão de Alturas',
|
||||||
|
|
@ -14,13 +12,15 @@ export const metadata: Metadata = {
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="pt-PT">
|
<html lang="pt-PT">
|
||||||
<body className={`${inter.className} flex flex-col min-h-screen bg-white text-gray-900`}>
|
<body>
|
||||||
<LanguageProvider>
|
<ThemeProvider>
|
||||||
<div className="flex flex-col min-h-screen">
|
<LanguageProvider>
|
||||||
<main className="flex-1">{children}</main>
|
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||||
<Footer />
|
<main style={{ flex: 1 }}>{children}</main>
|
||||||
</div>
|
<Footer />
|
||||||
</LanguageProvider>
|
</div>
|
||||||
|
</LanguageProvider>
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,30 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { Box, Typography, Container } from '@mui/material'
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="mt-12 border-t border-gray-200 bg-white py-8">
|
<Box component="footer" sx={{ mt: 6, borderTop: '1px solid', borderTopColor: 'divider', py: 4 }}>
|
||||||
<div className="mx-auto max-w-2xl px-4">
|
<Container maxWidth="sm">
|
||||||
<p className="text-center text-sm font-medium text-gray-900">
|
<Typography align="center" variant="body2" sx={{ fontWeight: 500 }}>
|
||||||
© 2026 APP — Associação Portuguesa de Powerlifting
|
© 2026 APP — Associação Portuguesa de Powerlifting
|
||||||
</p>
|
</Typography>
|
||||||
<p className="mt-3 text-center text-xs text-gray-500">
|
<Typography align="center" variant="caption" sx={{ mt: 2, display: 'block', color: 'text.secondary' }}>
|
||||||
feito por{' '}
|
feito por{' '}
|
||||||
<Link href="https://comfy.solutions" className="hover:underline">
|
<Link href="https://comfy.solutions" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
comfy.solutions
|
<Typography component="span" variant="caption" sx={{ '&:hover': { textDecoration: 'underline' } }}>
|
||||||
|
comfy.solutions
|
||||||
|
</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
{' '}•{' '}
|
{' '}•{' '}
|
||||||
<Link href="https://comfy.fillout.com/altarra-errors" className="hover:underline">
|
<Link href="https://comfy.fillout.com/altarra-errors" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||||
Erros? Informe-nos!
|
<Typography component="span" variant="caption" sx={{ '&:hover': { textDecoration: 'underline' } }}>
|
||||||
|
Erros? Informe-nos!
|
||||||
|
</Typography>
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</Typography>
|
||||||
</div>
|
</Container>
|
||||||
</footer>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useId } from 'react'
|
import { TextField, FormHelperText, Box } from '@mui/material'
|
||||||
import { AlertCircle } from 'lucide-react'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -30,37 +29,27 @@ export default function FormField({
|
||||||
disabled,
|
disabled,
|
||||||
error
|
error
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const id = useId()
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-4">
|
<Box sx={{ mb: 2 }}>
|
||||||
<label htmlFor={id} className="mb-2 block text-sm font-semibold text-gray-900">
|
<TextField
|
||||||
{label} {required ? <span className="text-[#FF0000]" aria-hidden>*</span> : null}
|
fullWidth
|
||||||
</label>
|
label={label}
|
||||||
<input
|
|
||||||
id={id}
|
|
||||||
className={`w-full rounded-lg border-2 px-4 py-3 text-base outline-none transition-colors ${
|
|
||||||
error
|
|
||||||
? 'border-[#FF0000] bg-[#fff5f5] focus-visible:ring-2 focus-visible:ring-[#FF0000]/30'
|
|
||||||
: 'border-gray-300 focus-visible:ring-2 focus-visible:ring-[#006600]/30'
|
|
||||||
}`}
|
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
placeholder={placeholder}
|
placeholder={type === 'date' ? undefined : placeholder}
|
||||||
required={required}
|
required={required}
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
step={step}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-invalid={!!error}
|
error={!!error}
|
||||||
aria-describedby={error ? `${id}-error` : undefined}
|
variant="outlined"
|
||||||
|
inputProps={{
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
}}
|
||||||
|
InputLabelProps={type === 'date' ? { shrink: true } : undefined}
|
||||||
|
helperText={error}
|
||||||
/>
|
/>
|
||||||
{error ? (
|
</Box>
|
||||||
<div id={`${id}-error`} className="mt-2 flex items-center gap-2 text-sm text-[#991b1b] font-medium">
|
|
||||||
<AlertCircle className="h-4 w-4 flex-shrink-0" style={{ color: '#FF0000' }} />
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import { Box, Container, Paper, Typography, Button, CircularProgress } from '@mui/material'
|
||||||
import LandingPage from '@/components/LandingPage'
|
import LandingPage from '@/components/LandingPage'
|
||||||
import IdentityForm from '@/components/IdentityForm'
|
import IdentityForm from '@/components/IdentityForm'
|
||||||
import RackHeightsForm from '@/components/RackHeightsForm'
|
import RackHeightsForm from '@/components/RackHeightsForm'
|
||||||
|
|
@ -63,12 +64,19 @@ export default function HomePageContent() {
|
||||||
|
|
||||||
if (authorized === null) {
|
if (authorized === null) {
|
||||||
return (
|
return (
|
||||||
<main className="grid min-h-screen place-items-center">
|
<Box
|
||||||
<div className="flex flex-col items-center gap-2 text-gray-600">
|
component="main"
|
||||||
|
sx={{
|
||||||
|
display: 'grid',
|
||||||
|
placeItems: 'center',
|
||||||
|
minHeight: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
<p>A carregar…</p>
|
<Typography color="textSecondary">A carregar…</Typography>
|
||||||
</div>
|
</Box>
|
||||||
</main>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,48 +85,46 @@ export default function HomePageContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto max-w-xl px-4 py-8">
|
<Box component="main" sx={{ mx: 'auto', maxWidth: 'md', px: 2, py: 4 }}>
|
||||||
<header className="mb-6 flex flex-col items-start gap-3">
|
<Box sx={{ mb: 3, display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||||
<div className="flex items-center justify-between w-full">
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', width: '100%', flexWrap: 'wrap', gap: 1.5 }}>
|
||||||
<div className="flex items-center gap-3">
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||||
<Image
|
<Image
|
||||||
src="/branding/logo.png"
|
src="/branding/logo.png"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
width={360}
|
width={360}
|
||||||
height={120}
|
height={120}
|
||||||
className="h-auto w-72 object-contain"
|
style={{ height: 'auto', width: 288, objectFit: 'contain' }}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</Box>
|
||||||
<div className="flex gap-2">
|
<Box sx={{ display: 'flex', gap: 1, alignSelf: 'center' }}>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setLanguage('pt')}
|
onClick={() => setLanguage('pt')}
|
||||||
className={`px-3 py-2 rounded-lg font-medium transition-colors ${
|
variant={language === 'pt' ? 'contained' : 'outlined'}
|
||||||
language === 'pt'
|
size="small"
|
||||||
? 'bg-[#006600] text-white'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
PT
|
PT
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
onClick={() => setLanguage('en')}
|
onClick={() => setLanguage('en')}
|
||||||
className={`px-3 py-2 rounded-lg font-medium transition-colors ${
|
variant={language === 'en' ? 'contained' : 'outlined'}
|
||||||
language === 'en'
|
size="small"
|
||||||
? 'bg-[#006600] text-white'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
EN
|
EN
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
<div>
|
<Box>
|
||||||
<h1 className="text-2xl font-semibold text-black">{competitionName}</h1>
|
<Typography variant="h4" sx={{ fontWeight: 600 }}>
|
||||||
<p className="text-gray-600">{language === 'pt' ? 'Submissão de Alturas' : 'Heights Submission'}</p>
|
{competitionName}
|
||||||
</div>
|
</Typography>
|
||||||
</header>
|
<Typography color="textSecondary">
|
||||||
<section className="rounded-lg border p-4 shadow-sm">
|
{language === 'pt' ? 'Submissão de Alturas' : 'Heights Submission'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
{!memberNumber || !birthDate ? (
|
{!memberNumber || !birthDate ? (
|
||||||
<IdentityForm
|
<IdentityForm
|
||||||
onVerified={({ name, birthDate, memberNumber }) => {
|
onVerified={({ name, birthDate, memberNumber }) => {
|
||||||
|
|
@ -128,8 +134,10 @@ export default function HomePageContent() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<Box>
|
||||||
<div className="mb-4 text-sm text-gray-700">{t('identified_as', language)} <span className="font-medium">{lifterName}</span></div>
|
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
|
||||||
|
{t('identified_as', language)} <Typography component="span" sx={{ fontWeight: 600 }}>{lifterName}</Typography>
|
||||||
|
</Typography>
|
||||||
<RackHeightsForm
|
<RackHeightsForm
|
||||||
memberNumber={memberNumber}
|
memberNumber={memberNumber}
|
||||||
birthDate={birthDate}
|
birthDate={birthDate}
|
||||||
|
|
@ -140,9 +148,9 @@ export default function HomePageContent() {
|
||||||
setLifterName(null)
|
setLifterName(null)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</section>
|
</Paper>
|
||||||
</main>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
|
import { Box, Typography } from '@mui/material'
|
||||||
import FormField from '@/components/FormField'
|
import FormField from '@/components/FormField'
|
||||||
import SubmitButton from '@/components/SubmitButton'
|
import SubmitButton from '@/components/SubmitButton'
|
||||||
import StatusMessage from '@/components/StatusMessage'
|
import StatusMessage from '@/components/StatusMessage'
|
||||||
|
import LoadingOverlay from '@/components/LoadingOverlay'
|
||||||
import { isValidDateDDMMYYYY } from '@/utils/validation'
|
import { isValidDateDDMMYYYY } from '@/utils/validation'
|
||||||
import { useLanguage } from '@/context/LanguageContext'
|
import { useLanguage } from '@/context/LanguageContext'
|
||||||
import { t, translations } from '@/utils/translations'
|
import { t, translations } from '@/utils/translations'
|
||||||
|
|
@ -36,7 +38,14 @@ export default function IdentityForm({ onVerified }: Props) {
|
||||||
setStatus({ type: 'error', msg: errorMessages.validation_error })
|
setStatus({ type: 'error', msg: errorMessages.validation_error })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!isValidDateDDMMYYYY(birthDate)) {
|
|
||||||
|
// Convert ISO date (YYYY-MM-DD) to DD/MM/YYYY format
|
||||||
|
const isISODate = birthDate.match(/^\d{4}-\d{2}-\d{2}$/)
|
||||||
|
const formattedDate = isISODate
|
||||||
|
? birthDate.split('-').reverse().join('/')
|
||||||
|
: birthDate
|
||||||
|
|
||||||
|
if (!isValidDateDDMMYYYY(formattedDate)) {
|
||||||
setStatus({ type: 'error', msg: errorMessages.date_format_error })
|
setStatus({ type: 'error', msg: errorMessages.date_format_error })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +55,7 @@ export default function IdentityForm({ onVerified }: Props) {
|
||||||
const res = await fetch('/api/find-lifter', {
|
const res = await fetch('/api/find-lifter', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ name, birthDate })
|
body: JSON.stringify({ name, birthDate: formattedDate })
|
||||||
})
|
})
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json().catch(() => ({}))
|
const data = await res.json().catch(() => ({}))
|
||||||
|
|
@ -65,24 +74,29 @@ export default function IdentityForm({ onVerified }: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit} aria-labelledby="identificacao">
|
<>
|
||||||
<h2 id="identificacao" className="mb-4 text-lg font-semibold">{t('step_1_identification', language)}</h2>
|
<LoadingOverlay visible={loading} />
|
||||||
<FormField
|
<Box component="form" onSubmit={onSubmit} aria-labelledby="identificacao">
|
||||||
label={t('full_name', language)}
|
<Typography id="identificacao" variant="h6" sx={{ mb: 2 }}>
|
||||||
value={name}
|
{t('step_1_identification', language)}
|
||||||
onChange={setName}
|
</Typography>
|
||||||
placeholder={t('full_name_placeholder', language)}
|
<FormField
|
||||||
required
|
label={t('full_name', language)}
|
||||||
/>
|
value={name}
|
||||||
<FormField
|
onChange={setName}
|
||||||
label={t('birth_date', language)}
|
placeholder={t('full_name_placeholder', language)}
|
||||||
value={birthDate}
|
required
|
||||||
onChange={setBirthDate}
|
/>
|
||||||
placeholder={t('birth_date_placeholder', language)}
|
<FormField
|
||||||
required
|
label={t('birth_date', language)}
|
||||||
/>
|
type="date"
|
||||||
<SubmitButton loading={loading}>{t('search', language)}</SubmitButton>
|
value={birthDate}
|
||||||
<StatusMessage status={status.type === 'success' ? 'success' : status.type === 'error' ? 'error' : 'info'} message={status.msg} />
|
onChange={setBirthDate}
|
||||||
</form>
|
required
|
||||||
|
/>
|
||||||
|
<SubmitButton disabled={loading}>{t('search', language)}</SubmitButton>
|
||||||
|
<StatusMessage status={status.type === 'success' ? 'success' : status.type === 'error' ? 'error' : 'info'} message={status.msg} />
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,103 @@
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { ExternalLink } from 'lucide-react'
|
import { Box, Paper, Typography, Button, Container } from '@mui/material'
|
||||||
|
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
|
||||||
import { useLanguage } from '@/context/LanguageContext'
|
import { useLanguage } from '@/context/LanguageContext'
|
||||||
import { t } from '@/utils/translations'
|
import { t } from '@/utils/translations'
|
||||||
|
|
||||||
export default function LandingPage() {
|
export default function LandingPage() {
|
||||||
const { language } = useLanguage()
|
const { language, setLanguage } = useLanguage()
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto max-w-xl px-4 py-10">
|
<Box
|
||||||
<header className="mb-8 flex flex-col items-center gap-3 text-center">
|
component="main"
|
||||||
<Image
|
sx={{
|
||||||
src="/branding/logo.png"
|
minHeight: '100vh',
|
||||||
alt="Logo"
|
display: 'flex',
|
||||||
width={360}
|
flexDirection: 'column',
|
||||||
height={120}
|
py: 4,
|
||||||
className="h-auto w-72 object-contain"
|
}}
|
||||||
priority
|
>
|
||||||
/>
|
<Container maxWidth="sm" sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center' }}>
|
||||||
</header>
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mb: 4, gap: 1 }}>
|
||||||
<section className="rounded-lg border-l-4 border-[#006600] bg-[#f0fdf4] p-8 shadow-md">
|
<Button
|
||||||
<h2 className="mb-3 text-2xl font-bold text-[#004d00]">{t('welcome_title', language)}</h2>
|
onClick={() => setLanguage('pt')}
|
||||||
<p className="mb-8 text-gray-700 leading-relaxed">
|
variant={language === 'pt' ? 'contained' : 'outlined'}
|
||||||
{t('welcome_text', language)}
|
size="small"
|
||||||
</p>
|
>
|
||||||
<Link
|
PT
|
||||||
href="https://powerlifting.pt"
|
</Button>
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-[#006600] px-6 py-3 font-semibold text-white shadow-sm hover:bg-[#004d00] transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#006600]"
|
<Button
|
||||||
|
onClick={() => setLanguage('en')}
|
||||||
|
variant={language === 'en' ? 'contained' : 'outlined'}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
EN
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ textAlign: 'center', mb: 5 }}>
|
||||||
|
<Image
|
||||||
|
src="/branding/logo.png"
|
||||||
|
alt="Logo"
|
||||||
|
width={360}
|
||||||
|
height={120}
|
||||||
|
style={{ height: 'auto', width: 320, objectFit: 'contain' }}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
sx={{
|
||||||
|
p: { xs: 3, sm: 5 },
|
||||||
|
borderLeft: '6px solid',
|
||||||
|
borderLeftColor: 'primary.main',
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||||
|
backgroundColor: 'rgba(0, 102, 0, 0.02)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{t('visit_website', language)}
|
<Typography
|
||||||
<ExternalLink className="h-4 w-4" />
|
variant="h4"
|
||||||
</Link>
|
sx={{
|
||||||
</section>
|
mb: 3,
|
||||||
</main>
|
fontWeight: 700,
|
||||||
|
color: 'primary.dark',
|
||||||
|
fontSize: { xs: '1.75rem', sm: '2.125rem' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('welcome_title', language)}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
mb: 4,
|
||||||
|
color: 'text.secondary',
|
||||||
|
lineHeight: 1.8,
|
||||||
|
fontSize: { xs: '0.95rem', sm: '1rem' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('welcome_text', language)}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href="https://powerlifting.pt"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
fullWidth
|
||||||
|
endIcon={<OpenInNewIcon />}
|
||||||
|
sx={{
|
||||||
|
py: 1.5,
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('visit_website', language)}
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
src/components/LoadingOverlay.tsx
Normal file
31
src/components/LoadingOverlay.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Box, CircularProgress } from '@mui/material'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoadingOverlay({ visible }: Props) {
|
||||||
|
if (!visible) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
bgcolor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
aria-busy="true"
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useRef, useState, lazy, Suspense } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import dynamic from 'next/dynamic'
|
import dynamic from 'next/dynamic'
|
||||||
|
import { Box, Typography, Alert, Button, TextField, MenuItem } from '@mui/material'
|
||||||
import FormField from '@/components/FormField'
|
import FormField from '@/components/FormField'
|
||||||
import SubmitButton from '@/components/SubmitButton'
|
import SubmitButton from '@/components/SubmitButton'
|
||||||
import StatusMessage from '@/components/StatusMessage'
|
import StatusMessage from '@/components/StatusMessage'
|
||||||
|
import LoadingOverlay from '@/components/LoadingOverlay'
|
||||||
import type { RackHeightsPayload } from '@/types/rackHeights'
|
import type { RackHeightsPayload } from '@/types/rackHeights'
|
||||||
import { required } from '@/utils/validation'
|
import { required } from '@/utils/validation'
|
||||||
import { useLanguage } from '@/context/LanguageContext'
|
import { useLanguage } from '@/context/LanguageContext'
|
||||||
|
|
@ -91,77 +93,89 @@ export default function RackHeightsForm({ memberNumber, birthDate, onSuccess }:
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div aria-live="polite">
|
<>
|
||||||
{submitted ? (
|
<LoadingOverlay visible={loading} />
|
||||||
<div className="space-y-4">
|
<Box aria-live="polite">
|
||||||
<div className="rounded-lg border-l-4 border-[#006600] bg-[#f0fdf4] px-6 py-6 shadow-sm">
|
{submitted ? (
|
||||||
<div className="mb-4 flex justify-center">
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
<Checkmark size="xxLarge" color="#006600" />
|
<Box
|
||||||
</div>
|
sx={{
|
||||||
<h2 className="text-center text-xl font-bold text-[#004d00]">{t('heights_submitted_title', language)}</h2>
|
py: 3,
|
||||||
<p className="mt-3 text-center text-[#1f5e2e]">{t('heights_submitted_text', language)}</p>
|
px: 2,
|
||||||
</div>
|
borderRadius: 1,
|
||||||
<button
|
textAlign: 'center',
|
||||||
type="button"
|
bgcolor: '#f0fdf4',
|
||||||
onClick={handleNewSubmission}
|
borderLeft: '4px solid',
|
||||||
className="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-[#006600] px-5 py-3 font-semibold text-white shadow-sm hover:bg-[#004d00] transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#006600]"
|
borderLeftColor: 'primary.main',
|
||||||
>
|
}}
|
||||||
{t('submit_another', language)}
|
>
|
||||||
</button>
|
<Box sx={{ mb: 2, display: 'flex', justifyContent: 'center' }}>
|
||||||
</div>
|
<Checkmark size="xxLarge" color="#006600" />
|
||||||
) : (
|
</Box>
|
||||||
<form onSubmit={onSubmit} aria-labelledby="alturas">
|
<Typography variant="h6" sx={{ fontWeight: 600, mb: 1, color: 'primary.dark' }}>
|
||||||
<h2 id="alturas" className="mb-4 text-lg font-semibold text-black">{t('step_2_heights', language)}</h2>
|
{t('heights_submitted_title', language)}
|
||||||
<div className="grid grid-cols-1 gap-3">
|
</Typography>
|
||||||
<div>
|
<Typography variant="body2" sx={{ color: '#1f5e2e' }}>
|
||||||
<label className="mb-2 block text-sm font-semibold text-gray-900">{t('squat_rack_height', language)} <span className="text-[#FF0000]" aria-hidden>*</span></label>
|
{t('heights_submitted_text', language)}
|
||||||
<input
|
</Typography>
|
||||||
ref={firstFieldRef}
|
</Box>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
fullWidth
|
||||||
|
onClick={handleNewSubmission}
|
||||||
|
sx={{ py: 1.5 }}
|
||||||
|
>
|
||||||
|
{t('submit_another', language)}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box component="form" onSubmit={onSubmit} aria-labelledby="alturas">
|
||||||
|
<Typography id="alturas" variant="h6" sx={{ mb: 2 }}>
|
||||||
|
{t('step_2_heights', language)}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||||
|
<FormField
|
||||||
|
label={t('squat_rack_height', language)}
|
||||||
type="number"
|
type="number"
|
||||||
inputMode="numeric"
|
|
||||||
min={1}
|
|
||||||
max={20}
|
|
||||||
value={squatRackHeight}
|
value={squatRackHeight}
|
||||||
onChange={(e) => setSquatRackHeight(e.target.value)}
|
onChange={(e) => setSquatRackHeight(e)}
|
||||||
className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 text-base outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[#006600]/30"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="mb-2 block text-sm font-semibold text-gray-900">{t('bench_rack_height', language)} <span className="text-[#FF0000]" aria-hidden>*</span></label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
inputMode="numeric"
|
|
||||||
min={1}
|
min={1}
|
||||||
max={20}
|
max={20}
|
||||||
value={benchRackHeight}
|
|
||||||
onChange={(e) => setBenchRackHeight(e.target.value)}
|
|
||||||
className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 text-base outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[#006600]/30"
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
<FormField
|
||||||
<div>
|
label={t('bench_rack_height', language)}
|
||||||
<label className="mb-2 block text-sm font-semibold text-gray-900">{t('bench_foot_blocks', language)} <span className="text-[#FF0000]" aria-hidden>*</span></label>
|
type="number"
|
||||||
<select
|
value={benchRackHeight}
|
||||||
value={benchRackFootBlocks}
|
onChange={(e) => setBenchRackHeight(e)}
|
||||||
onChange={(e) => setBenchRackFootBlocks(e.target.value as any)}
|
min={1}
|
||||||
className="w-full rounded-lg border-2 border-gray-300 px-4 py-3 text-base outline-none transition-colors focus-visible:ring-2 focus-visible:ring-[#006600]/30"
|
max={20}
|
||||||
required
|
required
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
select
|
||||||
|
label={t('bench_foot_blocks', language)}
|
||||||
|
value={benchRackFootBlocks}
|
||||||
|
onChange={(e) => setBenchRackFootBlocks(e.target.value as 'NONE' | '5cm' | '10cm' | '20cm' | '30cm')}
|
||||||
|
required
|
||||||
|
variant="outlined"
|
||||||
>
|
>
|
||||||
<option value="NONE">{t('foot_blocks_none', language)}</option>
|
<MenuItem value="NONE">{t('foot_blocks_none', language)}</MenuItem>
|
||||||
<option value="5cm">5cm</option>
|
<MenuItem value="5cm">5cm</MenuItem>
|
||||||
<option value="10cm">10cm</option>
|
<MenuItem value="10cm">10cm</MenuItem>
|
||||||
<option value="20cm">20cm</option>
|
<MenuItem value="20cm">20cm</MenuItem>
|
||||||
<option value="30cm">30cm</option>
|
<MenuItem value="30cm">30cm</MenuItem>
|
||||||
</select>
|
</TextField>
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
<Box sx={{ mt: 2 }}>
|
||||||
<div className="mt-4">
|
<SubmitButton disabled={loading}>{t('submit_heights', language)}</SubmitButton>
|
||||||
<SubmitButton loading={loading}>{t('submit_heights', language)}</SubmitButton>
|
</Box>
|
||||||
</div>
|
<StatusMessage status={status.type === 'success' ? 'success' : status.type === 'error' ? 'error' : 'info'} message={status.msg} />
|
||||||
<StatusMessage status={status.type === 'success' ? 'success' : status.type === 'error' ? 'error' : 'info'} message={status.msg} />
|
</Box>
|
||||||
</form>
|
)}
|
||||||
)}
|
</Box>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { CircularProgress } from '@mui/material'
|
||||||
|
|
||||||
export default function Spinner() {
|
export default function Spinner() {
|
||||||
return (
|
return <CircularProgress aria-hidden size={20} />
|
||||||
<div className="inline-block h-5 w-5 animate-spin rounded-full border-2 border-brand-600 border-t-transparent" aria-hidden />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { CheckCircle2, AlertCircle, Info } from 'lucide-react'
|
import { Alert, AlertTitle } from '@mui/material'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: 'idle' | 'success' | 'error' | 'info'
|
status: 'idle' | 'success' | 'error' | 'info'
|
||||||
|
|
@ -10,35 +10,21 @@ type Props = {
|
||||||
export default function StatusMessage({ status, message }: Props) {
|
export default function StatusMessage({ status, message }: Props) {
|
||||||
if (!message) return null
|
if (!message) return null
|
||||||
|
|
||||||
const config = {
|
const severityMap = {
|
||||||
success: {
|
success: 'success' as const,
|
||||||
color: 'bg-[#f0fdf4] border-[#006600] text-[#004d00]',
|
error: 'error' as const,
|
||||||
icon: <CheckCircle2 className="h-5 w-5 flex-shrink-0" style={{ color: '#006600' }} />
|
info: 'info' as const,
|
||||||
},
|
idle: 'info' as const,
|
||||||
error: {
|
|
||||||
color: 'bg-[#fef2f2] border-[#FF0000] text-[#991b1b]',
|
|
||||||
icon: <AlertCircle className="h-5 w-5 flex-shrink-0" style={{ color: '#FF0000' }} />
|
|
||||||
},
|
|
||||||
info: {
|
|
||||||
color: 'bg-[#f3f4f6] border-[#6b7280] text-[#374151]',
|
|
||||||
icon: <Info className="h-5 w-5 flex-shrink-0" style={{ color: '#6b7280' }} />
|
|
||||||
},
|
|
||||||
idle: {
|
|
||||||
color: 'bg-[#f3f4f6] border-[#6b7280] text-[#374151]',
|
|
||||||
icon: <Info className="h-5 w-5 flex-shrink-0" style={{ color: '#6b7280' }} />
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = config[status]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Alert
|
||||||
|
severity={severityMap[status]}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
className={`mt-4 flex items-center gap-3 rounded-lg border-l-4 px-4 py-3 text-sm font-medium ${current.color}`}
|
|
||||||
>
|
>
|
||||||
{current.icon}
|
{message}
|
||||||
<span>{message}</span>
|
</Alert>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { CheckCircle2 } from 'lucide-react'
|
import { Button, CircularProgress, Box } from '@mui/material'
|
||||||
|
import CheckCircleIcon from '@mui/icons-material/CheckCircle'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
|
@ -10,17 +11,21 @@ type Props = {
|
||||||
|
|
||||||
export default function SubmitButton({ children, loading, disabled }: Props) {
|
export default function SubmitButton({ children, loading, disabled }: Props) {
|
||||||
return (
|
return (
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-[#006600] px-5 py-3 font-semibold text-white shadow-sm hover:bg-[#004d00] transition-colors disabled:cursor-not-allowed disabled:opacity-60"
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
disabled={disabled || loading}
|
disabled={disabled || loading}
|
||||||
|
sx={{
|
||||||
|
py: 1.5,
|
||||||
|
display: 'flex',
|
||||||
|
gap: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <CheckCircleIcon />}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{children}
|
||||||
<span className="inline-block h-5 w-5 animate-spin rounded-full border-2 border-white border-r-transparent" aria-hidden />
|
</Button>
|
||||||
) : (
|
|
||||||
<CheckCircle2 className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
<span>{children}</span>
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
117
src/context/ThemeProvider.tsx
Normal file
117
src/context/ThemeProvider.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { createTheme, ThemeProvider as MuiThemeProvider } from '@mui/material/styles'
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: '#006600',
|
||||||
|
dark: '#004d00',
|
||||||
|
light: '#00cc00',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: '#FF0000',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
main: '#FF0000',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
main: '#FFA500',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
main: '#006600',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: '#ffffff',
|
||||||
|
paper: '#ffffff',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#1f2937',
|
||||||
|
secondary: '#6b7280',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
fontFamily: [
|
||||||
|
'-apple-system',
|
||||||
|
'BlinkMacSystemFont',
|
||||||
|
'"Segoe UI"',
|
||||||
|
'Roboto',
|
||||||
|
'"Helvetica Neue"',
|
||||||
|
'Arial',
|
||||||
|
'sans-serif',
|
||||||
|
].join(','),
|
||||||
|
h1: {
|
||||||
|
fontSize: '2.5rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '1.875rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
body1: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiButton: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
contained: {
|
||||||
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiTextField: {
|
||||||
|
defaultProps: {
|
||||||
|
variant: 'outlined',
|
||||||
|
},
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
fontSize: '1rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiSelect: {
|
||||||
|
defaultProps: {
|
||||||
|
variant: 'outlined',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiCard: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MuiAlert: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
borderRadius: '0.5rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<MuiThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{children}
|
||||||
|
</MuiThemeProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue