diff --git a/web/src/App.css b/web/src/App.css new file mode 100644 index 0000000..148c1e1 --- /dev/null +++ b/web/src/App.css @@ -0,0 +1,193 @@ + +/**********/ +/* Footer */ +/**********/ + +.app-footer { + background-color: var(--color-footer-background); + width: 100%; + + margin-block-start: var(--space-lg); + + padding-block: var(--space-md); +} + +.app-footer > div { + width: var(--main-wrapper); + + margin-inline: auto; +} + +/**********/ +/* Header */ +/**********/ + +.app-header { + background-color: var(--color-header-background); + + border-radius: var(--radius-md); + + box-shadow: var(--shadow-md); + + padding: var(--space-md); + + margin-block-start: var(--space-sm); + margin-block-end: var(--space-lg); + + display: grid; + + grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr)); + + gap: var(--space-lg); +} + +.app-header__title-container { + display: flex; + flex-direction: column; +} + +.app-header__weather-controls-section { + display: flex; + flex-direction: column; + + gap: var(--space-md); +} + +.app-header__weather-controls-title { + font-weight: var(--weight-medium); + font-size: var(--font-md); +} + +.app-header__title-container p { + color: var(--color-text-muted); +} + +.app-header__weather-controls { + display: flex; + flex-wrap: wrap; + gap: var(--space-md); +} + +.app-header__weather-controls label { + display: flex; + flex-direction: column; + gap: var(--space-xs); + + flex: 1 1 10rem; +} + + +.app-header__weather-controls select { + width: 100%; +} + +/*******************/ +/* Weather Details */ +/*******************/ + +.details-overlay { + position: fixed; + inset: 0; + z-index: 100; + + display: grid; + place-items: center; + + padding: var(--space-md); + + background: rgb(23 61 53 / 35%); +} + +.details-backdrop { + position: absolute; + inset: 0; + + width: 100%; + height: 100%; + border: 0; + padding: 0; + + background: transparent; +} + +.details-dialog { + position: relative; + z-index: 1; + + width: min(42rem, calc(100vw - 2rem)); + max-height: calc(100vh - 2rem); + overflow: auto; + + background-color: var(--color-background); + + padding: var(--space-lg); + + border-radius: var(--radius-lg); + + border: 1px solid var(--color-border); + + box-shadow: var(--shadow-lg); +} + +.details-dialog__close-button { + width: 100%; + + padding: var(--space-xs); + + border-radius: var(--radius-md); + + background-color: var(--color-action-surface); + border: 1px solid var(--color-action-surface-border); +} + +.details-dialog__close-button:hover, +.details-dialog__close-button:focus-visible { + background-color: var(--color-action-surface-hover); +} + +.details-dialog__close-button:active { + background-color: var(--color-action-surface-active); +} + +.details-dialog h2 { + font-size: var(--font-xl); +} + +.details-dialog__temperature-condition { + display: flex; + flex-direction: column; +} + +.details-dialog__temperature-condition > p:first-child { + font-size: var(--font-lg); + font-weight: var(--weight-semibold); +} + +.details-dialog__temperature-condition > p:last-child { + color: var(--color-text-muted); + font-weight: var(--weight-medium); +} + +.details-dialog__detail-row { + display: flex; + gap: var(--space-xs); +} + +.details-dialog__detail-row dd { + font-weight: var(--weight-medium); +} + +.details-dialog__detail-row dt { + color: var(--color-text-secondary); +} + +.details-dialog__update-time { + color: var(--color-text-muted); + + font-size: var(--font-sm); +} + +.details-dialog__section { + display: grid; + gap: var(--space-sm) +} \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx index 1cd4222..fd69f3a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -10,7 +10,8 @@ import { } from './features/weather/cities'; import { weatherQuery, type CityWeather } from './features/weather/weather'; import { useStoredState } from './shared/storage'; -import { WeatherList, type WeatherListItem } from './WeatherList'; +import { WeatherList, type WeatherListItem } from './components/WeatherList/WeatherList'; +import './App.css'; import { activities } from './data/activities'; import type { ActivityId } from './types/activity'; @@ -47,52 +48,57 @@ onActivityChange: (activity: ActivityId) => void; onSortChange: (sortMode: SortMode) => void; }) { return ( -
-

IT2810 · Gruppe 31

-

Turvær Norge

-

Finn været for din neste tur.

-
- - - +
+
+

Turvær Norge

+

IT2810 · Gruppe 31

+
+ +
+

Finn været for din neste tur.

+
+ + + +
); @@ -289,17 +295,14 @@ function MainWeather({ }) { return (
-
+
-

Valgt by

+

Valgt by

Været i {city.name}

-

Vurdert for{' '} @@ -316,36 +319,36 @@ function MainWeather({ )} {weather && ( <> -

-
-

+

+
+

{weather.current.temperature_2m}°

+

{weatherTypes[weather.current.weather_code] ?? 'Ukjent værtype'}

-

{weather.current.temperature_2m}°

-

- Oppdatert{' '} - -

-
-
-
Nedbør
+
+
+
Nedbør:
{weather.current.precipitation} mm
-
-
Vind
+
+
Vind:
{weather.current.wind_speed_10m} m/s
-
-
Intervall
-
{weather.current.interval / 60} min
-
+

+ Oppdatert{' '} + +

)} + +
); } @@ -437,6 +440,7 @@ export function App() { ); return ( + <>
-
+
+ - +
+ + ); } diff --git a/web/src/WeatherList.test.tsx b/web/src/WeatherList.test.tsx index 2c58173..c243951 100644 --- a/web/src/WeatherList.test.tsx +++ b/web/src/WeatherList.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; import { vi, expect, it } from 'vitest'; -import { WeatherList } from './WeatherList'; +import { WeatherList } from './components/WeatherList/WeatherList'; import { exampleCity } from './features/weather/cities'; it('shows feedback when there are no weather cards to display', () => { diff --git a/web/src/components/WeatherCard/WeatherCard.css b/web/src/components/WeatherCard/WeatherCard.css new file mode 100644 index 0000000..ca3873f --- /dev/null +++ b/web/src/components/WeatherCard/WeatherCard.css @@ -0,0 +1,124 @@ +h3 { + font-size: var(--font-xl) +} + +.weather-card { + display: grid; + gap: var(--space-sm); + + background-color: var(--color-card-background); + + padding-inline: var(--space-lg); + padding-block-start: var(--space-sm); + padding-block-end: var(--space-lg); + + border-radius: var(--radius-lg); + border: 1px solid var(--color-card-border); +} + +.weather-card:hover, +.weather-card:focus-visible { + border-color: var(--color-card-border-hover); + background-color: var(--color-card-hover); +} + +.weather-card-selected { + border-color: var(--color-card-border-selected); + background-color: var(--color-card-selected); + + border-width: 2px; +} + +.weather-card__header { + display: flex; + flex-wrap: wrap; + + justify-content: space-between; + align-items: center; +} + +.weather-card__favorite-button { + background-color: transparent; + border: 0; + + font-size: var(--control-font-2xl); +} + +.weather-card__favorite-button:hover, +.weather-card__favorite-button:focus-visible { + border-radius: var(--radius-lg); + + background-color: var(--color-action-surface-hover); + + border: 1px solid var(--color-border-hover); +} + +.weather-card__favorite-button:active { + background-color: var(--color-action-surface-active); +} + +.weather-card__button-full-width { + width: 100%; + + padding: var(--space-xs); + + border-radius: var(--radius-md); + + background-color: var(--color-action-surface); + border: 1px solid var(--color-action-surface-border); +} + +.weather-card__button-full-width:hover, +.weather-card__button-full-width:focus-visible { + background-color: var(--color-action-surface-hover); +} + +.weather-card__button-full-width:active { + background-color: var(--color-action-surface-active); +} + +.weather-card__temperature-condition { + display: flex; + flex-direction: column; +} + +.weather-card__temperature-condition > div:first-child { + font-size: var(--font-lg); + font-weight: var(--weight-semibold); +} + +.weather-card__temperature-condition > div:last-child { + color: var(--color-text-muted); + font-weight: var(--weight-medium); +} + +.weather-card__details { + display: grid; + gap: var(--space-sm); +} + +.weather-card__detail-row { + display: flex; + gap: var(--space-xs); +} + +.weather-card__detail-row dd { + font-weight: var(--weight-medium); +} + +.weather-card__detail-row dt { + color: var(--color-text-secondary); +} + +.weather-card__rank { + padding: var(--space-xs); + + background-color: var(--color-action-surface); + + border-radius: var(--radius-md); +} + +.weather-card__weather-error { + display: grid; + gap: var(--space-sm); +} \ No newline at end of file diff --git a/web/src/WeatherCard.tsx b/web/src/components/WeatherCard/WeatherCard.tsx similarity index 59% rename from web/src/WeatherCard.tsx rename to web/src/components/WeatherCard/WeatherCard.tsx index 71a375e..1114a80 100644 --- a/web/src/WeatherCard.tsx +++ b/web/src/components/WeatherCard/WeatherCard.tsx @@ -1,5 +1,6 @@ -import type { City } from './features/weather/cities'; -import type { CityWeather } from './features/weather/weather'; +import type { City } from '../../features/weather/cities'; +import type { CityWeather } from '../../features/weather/weather'; +import './WeatherCard.css'; export type WeatherCardProps = { city: City; @@ -63,40 +64,32 @@ export function WeatherCard({ isSelected ? 'weather-card weather-card-selected' : 'weather-card' } > -

{city.name}

+
+

{city.name}

+ + +
+ {rank !== null && ( -

#{rank} for valgt aktivitet

+

#{rank} for valgt aktivitet

)} - - - + {isLoading &&

Henter værdata…

} + {hasError && ( -
+

Kunne ikke hente værdata.

)} {weather && ( -
-
-
Værtype
-
{weatherType}
-
-
-
Temperatur
-
{weather.current.temperature_2m} °C
+
+
+
+
Temperatur
+
{weather.current.temperature_2m} °C
+
+ +
+
Værtype
+
{weatherType}
+
-
-
Nedbør
+ +
+
Nedbør:
{weather.current.precipitation} mm
-
-
Vind
+
+
Vind:
{weather.current.wind_speed_10m} m/s
)} + + + + ); } diff --git a/web/src/components/WeatherList/WeatherList.css b/web/src/components/WeatherList/WeatherList.css new file mode 100644 index 0000000..e9f2296 --- /dev/null +++ b/web/src/components/WeatherList/WeatherList.css @@ -0,0 +1,6 @@ +.weather-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(var(--weather-card-min-width), 1fr)); + + gap: var(--space-md); +} \ No newline at end of file diff --git a/web/src/WeatherList.tsx b/web/src/components/WeatherList/WeatherList.tsx similarity index 86% rename from web/src/WeatherList.tsx rename to web/src/components/WeatherList/WeatherList.tsx index 865b301..77d2dc0 100644 --- a/web/src/WeatherList.tsx +++ b/web/src/components/WeatherList/WeatherList.tsx @@ -1,7 +1,8 @@ import type { ReactNode } from 'react'; -import type { City } from './features/weather/cities'; -import type { CityWeather } from './features/weather/weather'; -import { WeatherCard } from './WeatherCard'; +import type { City } from '../../features/weather/cities'; +import type { CityWeather } from '../../features/weather/weather'; +import { WeatherCard } from '../WeatherCard/WeatherCard'; +import './WeatherList.css'; export type WeatherListItem = { city: City; @@ -35,7 +36,7 @@ export function WeatherList({ }: WeatherListProps) { return (
-

Vær i norske byer

+

Været i norske byer

{items.length === 0 ? (

Ingen værkort å vise.

diff --git a/web/src/styles.css b/web/src/styles.css index 02670f1..fe88d11 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -1,247 +1,246 @@ -:root { - font-family: system-ui, sans-serif; - line-height: 1.6; - color: #173d35; - background: #f2f5f0; -} + +/********************************************************************/ +/* Includes: CSS resets, CSS variables, Utilities, and Base Layout */ +/********************************************************************/ + + +/**************/ +/* CSS resets */ +/**************/ + * { box-sizing: border-box; } + body { margin: 0; + line-height: 1.5; + min-height: 100dvh; } -main { - width: min(1120px, 100% - 2rem); - margin: 2rem auto 4rem; -} -h1 { - font-size: clamp(2rem, 6vw, 3rem); - line-height: 1.1; -} -.app-header { - display: flex; - justify-content: space-between; - gap: 2rem; - align-items: end; - margin-bottom: 2rem; -} -.eyebrow { - margin: 0; - color: #9a491c; - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; + +/* Responsive media by default */ +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; } -.controls { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; -} -label { - display: grid; - gap: 0.25rem; - font-size: 0.85rem; - font-weight: 600; -} -select { - min-height: 44px; - padding: 0.5rem 2rem 0.5rem 0.65rem; - border: 1px solid #bacac1; - border-radius: 0.25rem; - background: #fff; - color: inherit; + +/* Make form controls inherit font */ +button, +input, +select, +textarea { font: inherit; } -section { - border: 1px solid #bacac1; - background: #fff; - padding: clamp(1rem, 4vw, 2rem); - border-radius: 0.5rem; -} -.main-weather { - margin-bottom: 1.5rem; -} -.section-heading { - display: flex; - align-items: start; - justify-content: space-between; - gap: 1rem; -} -.section-heading h2 { - margin: 0.25rem 0 0; -} -.resource-navigation { - display: flex; - gap: 0.5rem; + +/* Avoid text overflowing containers */ +p, +h1, +h2, +h3, +h4 { + overflow-wrap: break-word; } -.main-weather-summary { - display: flex; - justify-content: space-between; - align-items: end; - border-block: 1px solid #d5e2dc; - margin: 1.5rem 0; - padding: 1rem 0; -} -.weather-type, -.update-time { + +/* Remove default margins */ +h1, +h2, +h3, +h4, +p, +figure, +blockquote, +dl, +dd { margin: 0; } -.weather-type { - font-weight: 700; -} -.temperature { - margin: 0.25rem 0 0; - font-size: clamp(3rem, 8vw, 5rem); - line-height: 1; - font-weight: 700; + +/*****************/ +/* CSS variables */ +/*****************/ + +:root { + /* Surfaces */ + --color-background: #f2f5f0; + --color-surface: #ffffff; + --color-surface-hover: #f8fafc; + --color-surface-selected: #f1f5f9; + + /* Text Color */ + --color-text: #173d35; + --color-text-secondary: #475569; + --color-text-muted: #64748b; + --color-text-inverse: #ffffff; + + /* Borders */ + --color-border: #e2e8f0; + --color-border-hover: #cbd5e1; + --color-border-focus: #3b82f6; + + /* Primary */ + --color-primary: #398d4b; + --color-primary-hover: #327d42; + --color-primary-active: #296c38; + --color-primary-selected: #398d4b; + + --color-primary-focus: #2563eb; + --color-primary-text: #ffffff; + + /* Secondary */ + --color-action-surface: #ddeaf7; + --color-action-surface-hover: #bdcadb; + --color-action-surface-active: #cbd5e1; + + --color-action-surface-border: #b2cbe4; + + --color-action-text: #1e293b; + --color-action-focus: #2563eb; + + /* Header */ + --color-header-background: #f3f9ff; + + /* Footer */ + --color-footer-background: #afc4d5; + + /* Cards */ + --color-card-background: #ffffff; + --color-card-hover: #f8fafc; + --color-card-selected: #f3f9fc; + + --color-card-border: #e2e8f0; + --color-card-border-hover: #cbd5e1; + --color-card-border-selected: #94a3b8; + --color-card-focus: #2563eb; + + /* Weather Card */ + --weather-card-min-width: 17rem; + + /* Spacing */ + --space-xs: clamp(0.25rem, 0.2rem + 0.2vw, 0.5rem); + --space-sm: clamp(0.5rem, 0.4rem + 0.3vw, 0.75rem); + --space-md: clamp(1rem, 0.8rem + 0.5vw, 1.5rem); + --space-lg: clamp(1.5rem, 1.1rem + 1vw, 2.5rem); + --space-xl: clamp(2rem, 1.2rem + 2vw, 4rem); + --space-2xl: clamp(3rem, 1.5rem + 4vw, 7rem); + + /* Padding */ + --padding-xs: clamp(0.375rem, 0.3rem + 0.2vw, 0.5rem); + --padding-sm: clamp(0.5rem, 0.4rem + 0.3vw, 0.75rem); + --padding-md: clamp(0.75rem, 0.5rem + 0.7vw, 1.25rem); + --padding-lg: clamp(1rem, 0.5rem + 1.5vw, 2rem); + --padding-xl: clamp(1.5rem, 0.5rem + 3vw, 4rem); + + /* Font Size */ + --font-xs: clamp(0.75rem, 0.72rem + 0.1vw, 0.8rem); + --font-sm: clamp(0.875rem, 0.84rem + 0.1vw, 0.95rem); + --font-md: clamp(1rem, 0.95rem + 0.2vw, 1.125rem); + --font-lg: clamp(1.125rem, 1rem + 0.4vw, 1.375rem); + --font-xl: clamp(1.5rem, 1.2rem + 1vw, 2rem); + --font-2xl: clamp(2rem, 1.4rem + 2vw, 3.25rem); + --font-3xl: clamp(2.5rem, 1.5rem + 4vw, 5rem); + + /* Font Weight */ + --weight-normal: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; + + /* Radius */ + --radius-xs: 0.25rem; + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + --radius-2xl: 1.5rem; + --radius-full: 9999px; + + /* Interactice Controls Font Size */ + --control-font-sm: 0.875rem; + --control-font-md: 1rem; + --control-font-lg: 1.25rem; + --control-font-xl: 1.5rem; + --control-font-2xl: 2rem; + --control-font-3xl: 2.7rem; + + /* Bow Shadow */ + --shadow-sm: 0 1px 3px rgb(0 0 0 / 10%); + --shadow-md: 0 4px 12px rgb(0 0 0 / 15%); + --shadow-lg: 0 12px 32px rgb(0 0 0 / 20%); + + /* Wrappers */ + --main-wrapper: min(1120px, 100% - 2rem); } -.weather-details { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1rem; + +/*************/ +/* Utilities */ +/*************/ + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; } -.weather-details dd { - font-size: 1.3rem; + +.hidden { + display: none; } -.weather-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); - gap: 1rem; + +/* Margin block */ +.margin-block-start-sm { + margin-block-start: var(--space-sm); } -.weather-card-slot { - min-width: 0; + +.margin-block-start-md { + margin-block-start: var(--space-md); } -.weather-card-slot-expanded { - position: relative; - z-index: 10; + +.margin-block-start-lg { + margin-block-start: var(--space-lg); } -.expanded-weather { - width: 100%; - margin: 0; + +.margin-block-end-sm { + margin-block-end: var(--space-sm); } -.details-overlay { - position: fixed; - inset: 0; - z-index: 100; - display: grid; - place-items: center; - padding: 1rem; - background: rgb(23 61 53 / 35%); + +.margin-block-end-md { + margin-block-end: var(--space-md); } -.details-backdrop { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - min-height: 0; - padding: 0; - border: 0; - background: transparent; - cursor: default; -} -.details-dialog { - position: relative; - z-index: 1; - width: min(42rem, 100%); - max-height: calc(100vh - 2rem); - overflow: auto; - border-radius: 0.5rem; - box-shadow: 0 1rem 2.5rem rgb(23 61 53 / 25%); -} -.expanded-weather .resource-navigation { - flex-wrap: wrap; - justify-content: flex-end; -} -.weather-card { - border: 1px solid #bacac1; - border-radius: 0.5rem; - padding: 1rem; - background: #fff; - cursor: pointer; -} -.weather-card button + button { - margin-left: 0.5rem; -} -.weather-card:hover, -.weather-card:focus-visible, -.weather-card-selected { - border-color: #173d35; - box-shadow: 0 0 0 3px #d5e2dc; -} -.weather-card h3 { - margin-top: 0; -} -.weather-rank { - margin: -0.5rem 0 0.75rem; - color: #9a491c; - font-size: 0.8rem; - font-weight: 700; -} -.weather-card dl { - gap: 0.75rem 1rem; -} -.weather-card dd { - font-size: 1.1rem; -} -dl { - display: flex; - flex-wrap: wrap; - gap: 1.5rem; + +.margin-block-end-lg { + margin-block-end: var(--space-lg); } -dt { - font-size: 0.85rem; + + +/***************/ +/* Base layout */ +/***************/ + +main { + width: var(--main-wrapper); + margin-inline: auto; + + margin-block-start: var(--space-md); + + /* Fill any unused vertical space so the footer is pushed down */ + flex: 1; } -dd { - margin: 0.25rem 0; - font-size: 1.5rem; - font-weight: 600; + +:root { + font-family: system-ui, sans-serif; + color: var(--color-text); + background: var(--color-background); } -button { - font: inherit; - min-height: 44px; - padding: 0.5rem 1rem; - background: #173d35; - color: white; - border: 0; - border-radius: 0.25rem; - cursor: pointer; -} - -button[aria-pressed='true'] { - background: #9a491c; -} -a { - color: inherit; - text-underline-offset: 0.2em; -} -:focus-visible { - outline: 3px solid #9a491c; - outline-offset: 4px; -} -footer { - margin-top: 2rem; - font-size: 0.85rem; -} -@media (max-width: 760px) { - .app-header, - .section-heading, - .main-weather-summary { - align-items: stretch; - flex-direction: column; - } - .controls, - .controls label, - .controls select { - width: 100%; - } - .weather-details { - grid-template-columns: 1fr; - } - .resource-navigation button { - flex: 1; - } - .details-dialog { - width: 100%; - } + +/* Make the React app at least as tall as the viewport, and stack main + footer vertically */ +#root { + min-height: 100dvh; + display: flex; + flex-direction: column; } +