mirror of
https://github.com/domfelipe/clima-cuida-care-weather.git
synced 2026-08-07 07:36:47 +00:00
Merge bc09d9a278 into 9b57320ff2
This commit is contained in:
commit
e98fb82370
7 changed files with 302 additions and 36 deletions
1
.nojekyll
Normal file
1
.nojekyll
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
1
assets/index-CPSikBCn.css
Normal file
1
assets/index-CPSikBCn.css
Normal file
File diff suppressed because one or more lines are too long
160
assets/index-Do1NYCCz.js
Normal file
160
assets/index-Do1NYCCz.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -8,9 +8,10 @@
|
||||||
content="Clima Cuida: leitura prática de clima, qualidade do ar e risco do dia."
|
content="Clima Cuida: leitura prática de clima, qualidade do ar e risco do dia."
|
||||||
/>
|
/>
|
||||||
<title>Clima Cuida</title>
|
<title>Clima Cuida</title>
|
||||||
|
<script type="module" crossorigin src="/clima-cuida-care-weather-fork/assets/index-Do1NYCCz.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/clima-cuida-care-weather-fork/assets/index-CPSikBCn.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
35
src/App.tsx
35
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,7 +64,7 @@ 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,
|
||||||
|
|
@ -72,7 +76,7 @@ export default function App() {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[profile],
|
[profile, theme],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -80,13 +84,21 @@ 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 não oferece geolocalização. Busque uma cidade pelo nome.');
|
||||||
|
|
@ -127,7 +139,18 @@ 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}>
|
||||||
|
|
|
||||||
106
src/styles.css
106
src/styles.css
|
|
@ -1,19 +1,22 @@
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
--bg: oklch(98% 0.007 175);
|
--bg: oklch(97% 0.018 205);
|
||||||
--surface: oklch(100% 0 0);
|
--surface: oklch(100% 0 0);
|
||||||
--surface-soft: oklch(96% 0.012 170);
|
--surface-soft: oklch(95% 0.02 205);
|
||||||
|
--glass: oklch(100% 0 0 / 0.78);
|
||||||
--fg: oklch(19% 0.025 190);
|
--fg: oklch(19% 0.025 190);
|
||||||
--muted: oklch(47% 0.025 190);
|
--muted: oklch(47% 0.025 190);
|
||||||
--muted-2: oklch(62% 0.02 190);
|
--muted-2: oklch(62% 0.02 190);
|
||||||
--border: oklch(88% 0.014 180);
|
--border: oklch(86% 0.028 205);
|
||||||
--accent: oklch(55% 0.12 170);
|
--accent: oklch(58% 0.13 208);
|
||||||
--accent-2: oklch(62% 0.15 35);
|
--accent-2: oklch(70% 0.15 82);
|
||||||
--green: oklch(58% 0.14 155);
|
--green: oklch(58% 0.14 155);
|
||||||
--yellow: oklch(72% 0.13 88);
|
--yellow: oklch(72% 0.13 88);
|
||||||
--orange: oklch(66% 0.16 50);
|
--orange: oklch(66% 0.16 50);
|
||||||
--red: oklch(58% 0.19 25);
|
--red: oklch(58% 0.19 25);
|
||||||
--shadow: 0 22px 70px -44px oklch(22% 0.05 190 / 0.55);
|
--sky-glow: oklch(83% 0.105 215 / 0.42);
|
||||||
|
--sun-glow: oklch(85% 0.16 82 / 0.28);
|
||||||
|
--shadow: 0 24px 72px -44px oklch(22% 0.06 215 / 0.55);
|
||||||
--radius: 22px;
|
--radius: 22px;
|
||||||
--radius-sm: 14px;
|
--radius-sm: 14px;
|
||||||
--font-display: "Avenir Next", "Söhne", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
--font-display: "Avenir Next", "Söhne", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
|
|
@ -23,6 +26,27 @@
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme='dark'] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: oklch(18% 0.035 236);
|
||||||
|
--surface: oklch(23% 0.04 236);
|
||||||
|
--surface-soft: oklch(27% 0.045 236);
|
||||||
|
--glass: oklch(23% 0.04 236 / 0.76);
|
||||||
|
--fg: oklch(96% 0.012 220);
|
||||||
|
--muted: oklch(76% 0.028 220);
|
||||||
|
--muted-2: oklch(64% 0.03 220);
|
||||||
|
--border: oklch(37% 0.045 236);
|
||||||
|
--accent: oklch(73% 0.13 210);
|
||||||
|
--accent-2: oklch(78% 0.15 78);
|
||||||
|
--green: oklch(72% 0.13 155);
|
||||||
|
--yellow: oklch(82% 0.13 88);
|
||||||
|
--orange: oklch(75% 0.16 50);
|
||||||
|
--red: oklch(70% 0.18 25);
|
||||||
|
--sky-glow: oklch(42% 0.11 240 / 0.45);
|
||||||
|
--sun-glow: oklch(73% 0.15 78 / 0.18);
|
||||||
|
--shadow: 0 30px 82px -46px oklch(4% 0.02 236 / 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +61,9 @@ body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, oklch(96% 0.017 174) 0, var(--bg) 360px),
|
radial-gradient(circle at 14% -12%, var(--sun-glow), transparent 34vw),
|
||||||
|
radial-gradient(circle at 84% 0, var(--sky-glow), transparent 36vw),
|
||||||
|
linear-gradient(180deg, color-mix(in oklch, var(--bg), var(--accent) 12%) 0, var(--bg) 420px),
|
||||||
var(--bg);
|
var(--bg);
|
||||||
color: var(--fg);
|
color: var(--fg);
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
|
|
@ -79,9 +105,10 @@ a {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 16px clamp(16px, 4vw, 34px);
|
padding: 16px clamp(16px, 4vw, 34px);
|
||||||
border-bottom: 1px solid color-mix(in oklch, var(--border), white 24%);
|
border-bottom: 1px solid color-mix(in oklch, var(--border), transparent 20%);
|
||||||
background: oklch(98% 0.007 175 / 0.86);
|
background: var(--glass);
|
||||||
backdrop-filter: blur(18px);
|
box-shadow: 0 16px 44px -34px color-mix(in oklch, var(--fg), transparent 35%);
|
||||||
|
backdrop-filter: blur(20px) saturate(1.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-mark {
|
.brand-mark {
|
||||||
|
|
@ -106,6 +133,47 @@ a {
|
||||||
background: color-mix(in oklch, var(--accent), white 89%);
|
background: color-mix(in oklch, var(--accent), white 89%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topbar-actions {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 40px;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid color-mix(in oklch, var(--border), var(--accent) 18%);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--fg);
|
||||||
|
background: color-mix(in oklch, var(--surface), var(--accent) 6%);
|
||||||
|
box-shadow: 0 14px 34px -28px color-mix(in oklch, var(--accent), black 45%);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
font-weight: 780;
|
||||||
|
transition:
|
||||||
|
transform 180ms cubic-bezier(0.23, 1, 0.32, 1),
|
||||||
|
border-color 180ms cubic-bezier(0.23, 1, 0.32, 1),
|
||||||
|
background 180ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: color-mix(in oklch, var(--accent), var(--fg) 8%);
|
||||||
|
background: color-mix(in oklch, var(--surface), var(--accent) 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:active {
|
||||||
|
transform: translateY(1px) scale(0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle svg {
|
||||||
|
color: var(--accent-2);
|
||||||
|
}
|
||||||
|
|
||||||
.search-cluster {
|
.search-cluster {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -842,8 +910,14 @@ a {
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-selector {
|
.topbar-actions {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-selector {
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-cluster {
|
.search-cluster {
|
||||||
|
|
@ -881,12 +955,14 @@ a {
|
||||||
|
|
||||||
@media (min-width: 1180px) {
|
@media (min-width: 1180px) {
|
||||||
.topbar {
|
.topbar {
|
||||||
grid-template-columns: auto minmax(440px, 1fr) minmax(480px, auto);
|
grid-template-columns: auto minmax(440px, 1fr) minmax(560px, auto);
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-selector {
|
.topbar-actions {
|
||||||
grid-column: auto;
|
grid-column: auto;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-options {
|
.profile-options {
|
||||||
|
|
@ -934,6 +1010,10 @@ a {
|
||||||
padding: 12px 10px;
|
padding: 12px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.semaforo-content {
|
.semaforo-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
export default defineConfig(({ command }) => ({
|
export default defineConfig(({ command }) => ({
|
||||||
base: command === 'build' ? '/clima-cuida-care-weather/' : '/',
|
base: command === 'build' ? '/clima-cuida-care-weather-fork/' : '/',
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue