diff --git a/client/src/App.css b/client/src/App.css index f9d228f..e058860 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -37,7 +37,7 @@ a { } .clouds { - top: clamp(5vh, 8.5%, 20vh); + top: clamp(5vh, 7.5%, 20vh); left: 10.5vw; width: 10rem; height: 10rem; @@ -83,7 +83,7 @@ a { position: relative; border-radius: 16px; width: clamp(80%, 800px, 950px); - height: clamp(550px, 80%, 700px); + height: clamp(650px, 75%, 700px); background: rgba(0, 0, 0, 0.1); box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); backdrop-filter: blur(5px); @@ -165,17 +165,15 @@ a { main { display: grid; padding: 2rem; - --gap: 1.5rem; + --gap: 1.25rem; --search-bar-height: 2.5rem; --middle-column-width: 50%; gap: var(--gap); grid-template: 'searchbar . .' 'content content content'; width: 100%; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); grid-template-rows: var(--search-bar-height) minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) var(--middle-column-width) minmax(0, 1fr); - } .dashboard { @@ -188,9 +186,10 @@ main { min-height: 0; gap: var(--gap); grid-area: content; - grid-template: 'current-forecast map popular-cities' + grid-template: + 'current-forecast map popular-cities' 'weekly-forecast hourly-forecast hourly-forecast'; - grid-template-rows: var(--dashboard-first-row-height) minmax(0, 1fr); + grid-template-rows: var(--dashboard-first-row-height) minmax(0, 1fr); grid-template-columns: minmax(0, 1fr) var(--middle-column-width) minmax(0, 1fr); } @@ -322,21 +321,21 @@ main { } .weather-degrees-container .degrees { - font-size: clamp(2vw, 4.5rem, 4vw); + font-size: clamp(2vw, 4rem, 4vw); font-weight: 600; } .weather-degrees-container .degrees::after { font-size: 1rem; font-weight: 400; - inset: 0 -0.75rem auto auto; + margin-left: 5px; } .weather-degrees-container .weather-description { font-weight: 300; + width: max-content; } - .weather-details { display: flex; justify-content: space-between; @@ -371,7 +370,7 @@ main { .popular-cities .cities { flex: 1; - gap: 2rem; + gap: 1rem; display: flex; overflow: auto; flex-direction: column; @@ -379,7 +378,15 @@ main { .popular-cities .cities div { display: flex; + cursor: pointer; + border-radius: 5rem; + padding: 0.5rem 0.5rem; justify-content: space-between; + transition: opacity 0.15s ease-in-out; +} + +.popular-cities .cities div:hover { + opacity: 0.6; } .popular-cities .cities h2, @@ -412,6 +419,7 @@ main { font-weight: 300; padding: 8px 10px; align-items: center; + justify-content: start; border-radius: 0.5rem; } @@ -419,8 +427,8 @@ main { background: rgba(0, 0, 0, 0.2); } -.weekly-forecast li span.date { - margin-left: auto; +.weekly-forecast li span:nth-child(2) { + margin-right: 5px; } .weekly-forecast li img { @@ -484,3 +492,26 @@ main { 50% { transform: translate(20px, -10px); } } +@media (max-width: 1280px) { + html { + font-size: 14px; + } + + .app-container { + width: 80%; + } +} + +@media (max-width: 1080px) { + .app-container { + width: 90%; + } +} + +/* Support for tablet & mobile */ +@media (max-width: 840px) { + .app-container { + width: 100%; + height: 100%; + } +} diff --git a/client/src/app.tsx b/client/src/app.tsx index 1b90579..621f1bb 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -1,12 +1,13 @@ import './App.css'; import tabs from '@utils/tabs'; -import { useState } from 'react'; +import { Suspense, useState } from 'react'; import SideNavigation from '@components/side-navigation'; import SearchBar from '@components/search-bar/search-bar'; import { FavoritesProvider } from '@context/favorites-provider'; function App() { const [currentTab, setCurrentTab] = useState(tabs[0]); + const [selectedLocation, setSelectedLocation] = useState('Jerusalem'); return ( <> @@ -20,9 +21,14 @@ function App() {
- + - + Loading... }> + +
diff --git a/client/src/components/current-weather-card.tsx b/client/src/components/current-weather-card.tsx new file mode 100644 index 0000000..0449703 --- /dev/null +++ b/client/src/components/current-weather-card.tsx @@ -0,0 +1,76 @@ +import { getHours } from '@utils/utility-functions'; +import WeatherDetails from '@components/shared/weather-details'; +import { PiEyeLight, PiSunLight, PiDropLight, PiWindLight } from 'react-icons/pi'; + +type Props = { + uv: number; + name: string; + wind: number; + degrees: number; + humidity: number; + localTime: string; + visibility: number; + description: string; +} + +const CurrentWeatherCard: React.FC = ({ + uv, + name, + wind, + degrees, + humidity, + localTime, + visibility, + description, +}) => { + const formattedTime = getHours(localTime); + + return ( +
+
+

{ name }

+ { formattedTime } +
+ +
+ weather-icon + +
+ { degrees } + { description } +
+
+ +
+ } + /> + + } + /> + + } + /> + + } + /> +
+
+ ) +} + +export default CurrentWeatherCard; diff --git a/client/src/components/dashboard.tsx b/client/src/components/dashboard.tsx index bc7f6ad..467a5ab 100644 --- a/client/src/components/dashboard.tsx +++ b/client/src/components/dashboard.tsx @@ -1,109 +1,38 @@ import Map from './map'; +import { trpc } from '@utils/trpc'; +import WeeklyForecast from './weekly-forecast'; +import HourlyForecast from './hourly-forecast'; import PopularLocations from './popular-locations'; -import { PiWind, PiEyeLight, PiSunLight, PiDropLight, PiWindLight, PiThermometerSimple } from 'react-icons/pi'; +import CurrentWeatherCard from './current-weather-card'; -const Dashboard = () => { - return ( -
-
-
-

Current Weather

- 18:15 -
- -
- weather-icon - -
- 25 - Clear Sky -
-
- -
-
- - 5km/h -
- -
- - 80% -
- -
- - 3 -
- -
- - 10km -
-
-
+const Dashboard: React.FC<{ + selectedLocation: string; + setSelectedLocation: React.Dispatch>; +}> = ({ selectedLocation, setSelectedLocation }) => { + const [data] = trpc.getWeeklyForecast.useSuspenseQuery(selectedLocation); - + const { current, hourly, forecast, location } = data; - - -
-

Forecast

- -
    - { - Array.from({ length: 7 }).map((_, index) => ( -
  • - weather-icon - - - 24° / 18° - - - - 25 Jul, Thu - -
  • - )) - } -
-
- -
-

Hourly Forecast

- -
- { - Array.from({ length: 5 }).map((_, index) => ( -
- 7 PM + return ( +
+ - 20° + -
-
- - Feels 22° -
+ -
- - 8% -
+ -
- - 20 km/h -
-
-
- )) - } -
-
+
); }; diff --git a/client/src/components/hourly-forecast.tsx b/client/src/components/hourly-forecast.tsx new file mode 100644 index 0000000..b748861 --- /dev/null +++ b/client/src/components/hourly-forecast.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { getHours } from '@utils/utility-functions'; +import WeatherDetails from './shared/weather-details'; +import { PiWind, PiDropLight, PiThermometerSimple } from 'react-icons/pi'; + +type HourlyForecastProps = { + temp_c: number; + wind: number; + humidity: number; + time: string; + feels_like: number; +}[]; + +const HourlyForecast: React.FC<{ + forecast: HourlyForecastProps; +}> = ({ forecast }) => { + return ( +
+

Hourly Forecast

+ +
+ { + forecast.map((hour) => { + const formattedTime = getHours(hour.time); + + return ( +
+ { formattedTime } + + + { Math.round(hour.temp_c) }° + + +
+ + } + /> + + } + /> + + } + /> +
+
+ ); + }) + } +
+
+ ); +}; + +export default HourlyForecast; diff --git a/client/src/components/map.tsx b/client/src/components/map.tsx index 432bd15..0095cbc 100644 --- a/client/src/components/map.tsx +++ b/client/src/components/map.tsx @@ -15,6 +15,9 @@ const Map: React.FC<{ coordinates: Record<'lat' | 'lng', number>; }> = ({ center={ coordinates } className='map-container' scrollWheelZoom={ false } + key={ new Date().getTime() } + // workaround for leaflet bug: + //https://github.com/PaulLeCam/react-leaflet/issues/936 zoom={ mapSettings.defaultZoom } > { +const PopularLocations: React.FC<{ + setSelectedLocation: (location: string) => void; +}> = ({ setSelectedLocation }) => { const [data] = trpc.getPopularLocations.useSuspenseQuery(); return ( @@ -10,7 +12,10 @@ const PopularLocations = () => {
{ data.map((city) => ( -
+
setSelectedLocation(city.name) } + >

{ city.name }

{ city.temp_c }°C
diff --git a/client/src/components/search-bar/result-list.tsx b/client/src/components/search-bar/result-list.tsx index 04ef62c..af5d43a 100644 --- a/client/src/components/search-bar/result-list.tsx +++ b/client/src/components/search-bar/result-list.tsx @@ -3,7 +3,8 @@ import FavoriteButton from '@components/shared/favorite-button'; const ResultList: React.FC<{ searchQuery: string; -}> = ({ searchQuery }) => { + setSelectedLocation: (loaction: string) => void; +}> = ({ searchQuery, setSelectedLocation }) => { const { data, isLoading } = trpc.searchLocations.useQuery(searchQuery); if(isLoading) { @@ -19,7 +20,10 @@ const ResultList: React.FC<{ { data?.length ? data.map((location) => ( -
  • console.error('Click')}> +
  • setSelectedLocation(location.name) } + > { location.name } diff --git a/client/src/components/search-bar/search-bar.tsx b/client/src/components/search-bar/search-bar.tsx index ec78e15..c6efafc 100644 --- a/client/src/components/search-bar/search-bar.tsx +++ b/client/src/components/search-bar/search-bar.tsx @@ -2,7 +2,9 @@ import { useState } from 'react'; import ResultList from './result-list'; import { debounce } from '@utils/utility-functions'; -const SearchBar = () => { +const SearchBar: React.FC<{ + setSelectedLocation: (location: string) => void; +}> = ({ setSelectedLocation }) => { const [searchQuery, setSearchQuery] = useState(''); return ( @@ -16,7 +18,10 @@ const SearchBar = () => { { searchQuery && ( - + ) }
  • diff --git a/client/src/components/shared/weather-details.tsx b/client/src/components/shared/weather-details.tsx new file mode 100644 index 0000000..e63a934 --- /dev/null +++ b/client/src/components/shared/weather-details.tsx @@ -0,0 +1,14 @@ +const WeatherDetails: React.FC<{ + unit: string; + value: number | string; + icon: React.JSX.Element; +}> = ({ unit, icon, value }) => { + return ( +
    + { icon } + { value + unit } +
    + ) +} + +export default WeatherDetails; diff --git a/client/src/components/weekly-forecast.tsx b/client/src/components/weekly-forecast.tsx new file mode 100644 index 0000000..8ab8b7b --- /dev/null +++ b/client/src/components/weekly-forecast.tsx @@ -0,0 +1,46 @@ +type Forecast = { + date: string; + icon_code: number; + max_temp_c: number; + min_temp_c: number; + condition: string; +}[]; + +const WeeklyForecast: React.FC<{ + forecast: Forecast; +}> = ({ forecast }) => { + + return ( +
    +

    Forecast

    + +
      + { + forecast.map((day) => { + const formattedDate = new Date(day.date).toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric', + }); + + return ( +
    • + weather-icon + + + { Math.round(day.max_temp_c) }° / { Math.round(day.min_temp_c) }° + + + + { formattedDate } + +
    • + ) + }) + } +
    +
    + ); +}; + +export default WeeklyForecast; diff --git a/client/src/utils/tabs.ts b/client/src/utils/tabs.ts index e9db9a4..b7a3531 100644 --- a/client/src/utils/tabs.ts +++ b/client/src/utils/tabs.ts @@ -8,7 +8,10 @@ export type Tab = { id: string; label: string; icon: IconType; - component: React.FC; + component: React.FC<{ + selectedLocation: string; + setSelectedLocation: React.Dispatch> + }>; }; const tabs: Tab[] = [ diff --git a/client/src/utils/utility-functions.ts b/client/src/utils/utility-functions.ts index 4a2a646..e63e448 100644 --- a/client/src/utils/utility-functions.ts +++ b/client/src/utils/utility-functions.ts @@ -11,3 +11,12 @@ export const debounce = any>( timer = setTimeout(() => fn(...args), ms); }; }; + +export const getHours = (localtime: string) => { + const date = new Date(localtime); + + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + + return `${ hours }:${ minutes }`; +} diff --git a/server/utils/fetchers/fetchers.ts b/server/utils/fetchers/fetchers.ts index 278b83e..0c4b38b 100644 --- a/server/utils/fetchers/fetchers.ts +++ b/server/utils/fetchers/fetchers.ts @@ -30,6 +30,7 @@ export const fetchForecast = async (query: string) => { lat: location.lat, lng: location.lon, name: location.name, + localtime: location.localtime, }, current: { uv: current.uv,