mirror of
https://github.com/domfelipe/clima-cuida-care-weather.git
synced 2026-08-07 06:56:49 +00:00
feat: adicionar alternancia de tema
This commit is contained in:
parent
9b57320ff2
commit
eeded6c20e
1 changed files with 36 additions and 13 deletions
49
src/App.tsx
49
src/App.tsx
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { HeartPulse } from 'lucide-react';
|
import { HeartPulse, Moon, Sun } from 'lucide-react';
|
||||||
import { fetchClimaCuidaData } from './api/openMeteo';
|
import { fetchClimaCuidaData } from './api/openMeteo';
|
||||||
import { ErrorBanner } from './components/ErrorBanner';
|
import { ErrorBanner } from './components/ErrorBanner';
|
||||||
import { Forecast7Days } from './components/Forecast7Days';
|
import { Forecast7Days } from './components/Forecast7Days';
|
||||||
|
|
@ -16,10 +16,12 @@ import { calculateRiskScore } from './lib/riskScore';
|
||||||
import type { LocationOption, ProfileId, WeatherBundle } from './types';
|
import type { LocationOption, ProfileId, WeatherBundle } from './types';
|
||||||
|
|
||||||
const STORAGE_KEY = 'clima-cuida-preferences';
|
const STORAGE_KEY = 'clima-cuida-preferences';
|
||||||
|
type ThemeMode = 'light' | 'dark';
|
||||||
|
|
||||||
interface StoredPreferences {
|
interface StoredPreferences {
|
||||||
profile?: ProfileId;
|
profile?: ProfileId;
|
||||||
location?: LocationOption;
|
location?: LocationOption;
|
||||||
|
theme?: ThemeMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPreferences(): StoredPreferences {
|
function loadPreferences(): StoredPreferences {
|
||||||
|
|
@ -42,6 +44,7 @@ function savePreferences(preferences: StoredPreferences) {
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const initialPreferences = useMemo(() => loadPreferences(), []);
|
const initialPreferences = useMemo(() => loadPreferences(), []);
|
||||||
const [profile, setProfile] = useState<ProfileId>(initialPreferences.profile ?? 'adult');
|
const [profile, setProfile] = useState<ProfileId>(initialPreferences.profile ?? 'adult');
|
||||||
|
const [theme, setTheme] = useState<ThemeMode>(initialPreferences.theme ?? 'light');
|
||||||
const [activeLocation, setActiveLocation] = useState<LocationOption>(
|
const [activeLocation, setActiveLocation] = useState<LocationOption>(
|
||||||
initialPreferences.location ?? MOCK_WEATHER.location,
|
initialPreferences.location ?? MOCK_WEATHER.location,
|
||||||
);
|
);
|
||||||
|
|
@ -51,6 +54,7 @@ export default function App() {
|
||||||
|
|
||||||
const selectedProfile = USE_PROFILES.find((item) => item.id === profile) ?? USE_PROFILES[0];
|
const selectedProfile = USE_PROFILES.find((item) => item.id === profile) ?? USE_PROFILES[0];
|
||||||
const risk = useMemo(() => calculateRiskScore(weather.current, profile), [profile, weather.current]);
|
const risk = useMemo(() => calculateRiskScore(weather.current, profile), [profile, weather.current]);
|
||||||
|
const isDarkTheme = theme === 'dark';
|
||||||
|
|
||||||
const loadWeather = useCallback(
|
const loadWeather = useCallback(
|
||||||
async (location: LocationOption) => {
|
async (location: LocationOption) => {
|
||||||
|
|
@ -60,19 +64,19 @@ export default function App() {
|
||||||
const nextWeather = await fetchClimaCuidaData(location);
|
const nextWeather = await fetchClimaCuidaData(location);
|
||||||
setWeather(nextWeather);
|
setWeather(nextWeather);
|
||||||
setActiveLocation(location);
|
setActiveLocation(location);
|
||||||
savePreferences({ profile, location });
|
savePreferences({ profile, location, theme });
|
||||||
} catch {
|
} catch {
|
||||||
setWeather({
|
setWeather({
|
||||||
...MOCK_WEATHER,
|
...MOCK_WEATHER,
|
||||||
fetchedAt: new Date().toISOString(),
|
fetchedAt: new Date().toISOString(),
|
||||||
source: 'mock',
|
source: 'mock',
|
||||||
});
|
});
|
||||||
setError('A API não respondeu agora. Exibindo dados de exemplo para manter a leitura do dia utilizável.');
|
setError('A API nao respondeu agora. Exibindo dados de exemplo para manter a leitura do dia utilizavel.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[profile],
|
[profile, theme],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -80,16 +84,24 @@ export default function App() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
savePreferences({ profile, location: activeLocation });
|
document.documentElement.dataset.theme = theme;
|
||||||
}, [profile, activeLocation]);
|
}, [theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
savePreferences({ profile, location: activeLocation, theme });
|
||||||
|
}, [profile, activeLocation, theme]);
|
||||||
|
|
||||||
function handleProfileChange(nextProfile: ProfileId) {
|
function handleProfileChange(nextProfile: ProfileId) {
|
||||||
setProfile(nextProfile);
|
setProfile(nextProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleToggleTheme() {
|
||||||
|
setTheme((currentTheme) => (currentTheme === 'light' ? 'dark' : 'light'));
|
||||||
|
}
|
||||||
|
|
||||||
function handleUseCurrentLocation() {
|
function handleUseCurrentLocation() {
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
setError('Este navegador não oferece geolocalização. Busque uma cidade pelo nome.');
|
setError('Este navegador nao oferece geolocalizacao. Busque uma cidade pelo nome.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +110,7 @@ export default function App() {
|
||||||
(position) => {
|
(position) => {
|
||||||
const location: LocationOption = {
|
const location: LocationOption = {
|
||||||
id: 'browser-location',
|
id: 'browser-location',
|
||||||
name: 'Sua localização',
|
name: 'Sua localizacao',
|
||||||
latitude: position.coords.latitude,
|
latitude: position.coords.latitude,
|
||||||
longitude: position.coords.longitude,
|
longitude: position.coords.longitude,
|
||||||
};
|
};
|
||||||
|
|
@ -106,7 +118,7 @@ export default function App() {
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setError('Não foi possível acessar sua localização. Você pode pesquisar uma cidade manualmente.');
|
setError('Nao foi possivel acessar sua localizacao. Voce pode pesquisar uma cidade manualmente.');
|
||||||
},
|
},
|
||||||
{ enableHighAccuracy: false, timeout: 9000, maximumAge: 10 * 60 * 1000 },
|
{ enableHighAccuracy: false, timeout: 9000, maximumAge: 10 * 60 * 1000 },
|
||||||
);
|
);
|
||||||
|
|
@ -127,16 +139,27 @@ export default function App() {
|
||||||
onSelectLocation={loadWeather}
|
onSelectLocation={loadWeather}
|
||||||
onUseCurrentLocation={handleUseCurrentLocation}
|
onUseCurrentLocation={handleUseCurrentLocation}
|
||||||
/>
|
/>
|
||||||
<ProfileSelector profiles={USE_PROFILES} value={profile} onChange={handleProfileChange} />
|
<div className="topbar-actions">
|
||||||
|
<button
|
||||||
|
className="theme-toggle"
|
||||||
|
type="button"
|
||||||
|
aria-label={isDarkTheme ? 'Ativar modo claro' : 'Ativar modo escuro'}
|
||||||
|
onClick={handleToggleTheme}
|
||||||
|
>
|
||||||
|
{isDarkTheme ? <Sun size={17} aria-hidden="true" /> : <Moon size={17} aria-hidden="true" />}
|
||||||
|
<span>{isDarkTheme ? 'Modo Claro' : 'Modo Escuro'}</span>
|
||||||
|
</button>
|
||||||
|
<ProfileSelector profiles={USE_PROFILES} value={profile} onChange={handleProfileChange} />
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="dashboard-shell" aria-busy={isLoading}>
|
<main className="dashboard-shell" aria-busy={isLoading}>
|
||||||
{error && <ErrorBanner message={error} onRetry={() => loadWeather(activeLocation)} />}
|
{error && <ErrorBanner message={error} onRetry={() => loadWeather(activeLocation)} />}
|
||||||
<div className="dashboard-intro">
|
<div className="dashboard-intro">
|
||||||
<p className="eyebrow">Orientação geral, não recomendação médica</p>
|
<p className="eyebrow">Orientacao geral, nao recomendacao medica</p>
|
||||||
<p>
|
<p>
|
||||||
Leitura combinada de clima, UV e qualidade do ar para decidir saída, exercício, deslocamento
|
Leitura combinada de clima, UV e qualidade do ar para decidir saida, exercicio, deslocamento
|
||||||
e proteção diária.
|
e protecao diaria.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue