mirror of
https://github.com/hazemKrimi/react-weather-app.git
synced 2026-05-01 18:30:25 +00:00
Update project structure
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
[*.{html,css,js,ts,jsx,tsx,json}]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
quote_type= single
|
||||
+1
-1
@@ -37,6 +37,6 @@ const App: React.FC = () => {
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -1,31 +1,5 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.13);
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
row-gap: 0.5rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0rem;
|
||||
|
||||
i {
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
.cold {
|
||||
color: #429bb8;
|
||||
}
|
||||
|
||||
.hot {
|
||||
color: #ff4500;
|
||||
}
|
||||
`;
|
||||
import { Wrapper } from './styles';
|
||||
|
||||
interface Props {
|
||||
date: Date;
|
||||
@@ -0,0 +1,28 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.13);
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
row-gap: 0.5rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0rem;
|
||||
word-wrap: break-word;
|
||||
|
||||
i {
|
||||
font-size: 5rem;
|
||||
}
|
||||
|
||||
.cold {
|
||||
color: #429bb8;
|
||||
}
|
||||
|
||||
.hot {
|
||||
color: #ff4500;
|
||||
}
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
|
||||
@media(max-width: 768px) {
|
||||
width: 90%;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Container;
|
||||
@@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 90%;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Container;
|
||||
@@ -1,32 +1,8 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import TW from '../assets/twitter.svg';
|
||||
import GH from '../assets/github.svg';
|
||||
import LI from '../assets/linkedin.svg';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto 0.7fr;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0rem;
|
||||
|
||||
p {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
|
||||
.contact {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 24px);
|
||||
column-gap: 0.25rem;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
import { Wrapper } from './styles';
|
||||
import TW from '../../assets/twitter.svg';
|
||||
import GH from '../../assets/github.svg';
|
||||
import LI from '../../assets/linkedin.svg';
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
return (
|
||||
@@ -0,0 +1,25 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: auto 0.7fr;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0rem;
|
||||
|
||||
p {
|
||||
justify-self: flex-start;
|
||||
}
|
||||
|
||||
.contact {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 24px);
|
||||
column-gap: 0.25rem;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,15 +0,0 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
font-family: 'Poppins';
|
||||
color: #000000;
|
||||
}
|
||||
`;
|
||||
|
||||
export default GlobalStyles;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
outline: none;
|
||||
font-family: 'Poppins';
|
||||
color: #000000;
|
||||
}
|
||||
`;
|
||||
|
||||
export default GlobalStyles;
|
||||
@@ -1,19 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Loader = styled.div`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 25px);
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 3px solid rgba(0, 0, 0, 0.3);
|
||||
border-top-color: rgba(0, 0, 0, 1);
|
||||
animation: anim 0.5s infinite linear;
|
||||
|
||||
@keyframes anim {
|
||||
to { transform: rotate(360deg) }
|
||||
}
|
||||
`;
|
||||
|
||||
export default Loader;
|
||||
@@ -0,0 +1,21 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Loader = styled.div`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 25px);
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 3px solid rgba(0, 0, 0, 0.3);
|
||||
border-top-color: rgba(0, 0, 0, 1);
|
||||
animation: anim 0.5s infinite linear;
|
||||
|
||||
@keyframes anim {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Loader;
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import SearchBar from './SearchBar';
|
||||
|
||||
const Nav = styled.nav`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 0.7fr;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0rem;
|
||||
|
||||
h1 {
|
||||
cursor: pointer;
|
||||
justify-self: flex-start;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavBar: React.FC = () => {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Nav>
|
||||
<h1 onClick={() => history.push('/home')}>Weather</h1>
|
||||
<SearchBar />
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
|
||||
export default NavBar;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Wrapper } from './styles';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import SearchBar from '../SearchBar';
|
||||
|
||||
const NavBar: React.FC = () => {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<h1 onClick={() => history.push('/home')}>Weather</h1>
|
||||
<SearchBar />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
@@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.nav`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 0.7fr;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0rem;
|
||||
|
||||
h1 {
|
||||
cursor: pointer;
|
||||
justify-self: flex-start;
|
||||
}
|
||||
`;
|
||||
@@ -1,55 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import SearchIcon from '../assets/search.svg';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 5px;
|
||||
justify-self: flex-end;
|
||||
padding: 0.2rem 0.5rem;
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
`;
|
||||
|
||||
const SearchBar: React.FC = () => {
|
||||
const [ query, setQuery ] = useState<string>('');
|
||||
const history = useHistory();
|
||||
|
||||
const search = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter' && query !== '') {
|
||||
history.push(`/search/${query}`);
|
||||
setQuery('');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Input
|
||||
placeholder='Search'
|
||||
value={query}
|
||||
onKeyPress={search}
|
||||
onChange={event => setQuery(event.target.value)}
|
||||
/>
|
||||
<img
|
||||
src={SearchIcon}
|
||||
alt='Search icon'
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchBar;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Wrapper, Input } from './styles';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import SearchIcon from '../../assets/search.svg';
|
||||
|
||||
const SearchBar: React.FC = () => {
|
||||
const [query, setQuery] = useState<string>('');
|
||||
const history = useHistory();
|
||||
|
||||
const search = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter' && query !== '') {
|
||||
history.push(`/search/${query}`);
|
||||
setQuery('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Input
|
||||
placeholder='Search'
|
||||
value={query}
|
||||
onKeyPress={search}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setQuery(event.target.value)}
|
||||
/>
|
||||
<img src={SearchIcon} alt='Search icon' />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchBar;
|
||||
@@ -0,0 +1,23 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 5px;
|
||||
justify-self: flex-end;
|
||||
padding: 0.2rem 0.5rem;
|
||||
`;
|
||||
|
||||
export const Input = styled.input`
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
|
||||
&::placeholder {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
`;
|
||||
Vendored
+5335
-1
File diff suppressed because one or more lines are too long
Vendored
+1823
-2
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 181 KiB |
@@ -1,298 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { motion } from 'framer-motion';
|
||||
import Loader from '../components/Loader';
|
||||
import Card from '../components/Card';
|
||||
import LeftArrow from '../assets/left-arrow.svg';
|
||||
import RightArrow from '../assets/right-arrow.svg';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
min-height: 85vh;
|
||||
padding: 2rem 0rem;
|
||||
display: grid;
|
||||
row-gap: 3rem;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main, .wind, .humidity {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.slider-background {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.forecast-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 10rem);
|
||||
grid-auto-flow: column;
|
||||
column-gap: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface WeatherResponse {
|
||||
weather: Array<{ id: number, description: string }>,
|
||||
main: { temp: number, humidity: number },
|
||||
wind: { speed: number },
|
||||
name: string,
|
||||
sys: {
|
||||
country: string
|
||||
},
|
||||
dt: number,
|
||||
};
|
||||
|
||||
interface ForecastResponse {
|
||||
hourly: Array<{
|
||||
dt: number,
|
||||
temp: number,
|
||||
weather: Array<{ id: number, description: string }>
|
||||
}>,
|
||||
daily: Array<{
|
||||
dt: number,
|
||||
temp: { min: number, max: number },
|
||||
weather: Array<{ id: number, description: string }>
|
||||
}>
|
||||
};
|
||||
|
||||
interface Weather {
|
||||
description: string,
|
||||
icon: number,
|
||||
main: {
|
||||
temp: number,
|
||||
humidity: number
|
||||
},
|
||||
wind: {
|
||||
speed: number
|
||||
},
|
||||
country: string,
|
||||
timestamp: number,
|
||||
name: string
|
||||
};
|
||||
|
||||
interface Forecast {
|
||||
hourly: Array<{
|
||||
dt: number,
|
||||
temp: number,
|
||||
weather: Array<{ id: number, description: string }>
|
||||
}>,
|
||||
daily: Array<{
|
||||
dt: number,
|
||||
temp: { min: number, max: number },
|
||||
weather: Array<{ id: number, description: string }>
|
||||
}>
|
||||
};
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const [ weather, setWeather ] = useState<Weather | null>(null);
|
||||
const [ forecast, setForecast ] = useState<Forecast | null>(null);
|
||||
const [ loading, setLoading ] = useState<boolean>(true);
|
||||
const [ error, setError ] = useState<string>('');
|
||||
const [ dailyForecastGrid, setDailyForecastGrid ] = useState<number>(0);
|
||||
const [ hourlyForecastGrid, setHourlyForecastGrid ] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.geolocation) setError('Geolocation not supported in this browser! Try searching for a city instead');
|
||||
else navigator.geolocation.getCurrentPosition(
|
||||
async ({ coords }) => {
|
||||
try {
|
||||
let weatherRes: Response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/weather?lat=${coords.latitude}&lon=${coords.longitude}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&units=metric`
|
||||
);
|
||||
let weatherResBody: WeatherResponse = await weatherRes.json();
|
||||
setWeather({
|
||||
description: weatherResBody.weather[0].description,
|
||||
icon: weatherResBody.weather[0].id,
|
||||
main: weatherResBody.main,
|
||||
wind: weatherResBody.wind,
|
||||
name: weatherResBody.name,
|
||||
country: weatherResBody.sys.country,
|
||||
timestamp: weatherResBody.dt
|
||||
});
|
||||
let forecastRes: Response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/onecall?lat=${coords.latitude}&lon=${coords.longitude}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&exclude=minutely&units=metric`
|
||||
);
|
||||
let forecastResBody: ForecastResponse = await forecastRes.json();
|
||||
setForecast({
|
||||
hourly: forecastResBody.hourly,
|
||||
daily: forecastResBody.daily
|
||||
});
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
setError('Could not get weather data! Try again later');
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setLoading(false);
|
||||
setError('Geolocation not active! Try searching for a city instead');
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
setError('');
|
||||
setLoading(true);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
return !loading ? (
|
||||
<Wrapper>
|
||||
{
|
||||
weather && forecast && !error ? (
|
||||
<>
|
||||
<div className='main'>
|
||||
<h2>{weather.name}, {weather.country}</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.main.temp + '°C'}
|
||||
temp={weather.main.temp > 25 ? 'hot' : weather.main.temp < 20 ? 'cold' : null}
|
||||
icon={weather.icon}
|
||||
description={weather.description}
|
||||
/>
|
||||
</div>
|
||||
<div className='daily-forecast'>
|
||||
<h2>Daily Forecast</h2>
|
||||
<div className='slider'>
|
||||
<motion.img
|
||||
src={LeftArrow}
|
||||
alt='Left slider arrow'
|
||||
onTap={() => {
|
||||
if (dailyForecastGrid <= forecast.daily.length / 2 - 1) setDailyForecastGrid(dailyForecastGrid + 1);
|
||||
}}
|
||||
/>
|
||||
<div className='slider-background'>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ transform: `translateX(${dailyForecastGrid * 10}rem)` }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className='forecast-grid'
|
||||
>
|
||||
{
|
||||
forecast.daily.map(day => (
|
||||
<Card
|
||||
key={day.dt}
|
||||
date={new Date(day.dt * 1000)}
|
||||
time={false}
|
||||
data={day.temp.min + '°C/' + day.temp.max + '°C'}
|
||||
temp={day.temp.max > 25 ? 'hot' : day.temp.max < 20 ? 'cold' : null}
|
||||
icon={day.weather[0].id}
|
||||
description={day.weather[0].description}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.img
|
||||
src={RightArrow}
|
||||
alt='Right slider arrow'
|
||||
onTap={() => {
|
||||
if (dailyForecastGrid >= - forecast.daily.length / 2 + 1) setDailyForecastGrid(dailyForecastGrid - 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='hourly-forecast'>
|
||||
<h2>Hourly Forecast</h2>
|
||||
<div className='slider'>
|
||||
<motion.img
|
||||
src={LeftArrow}
|
||||
alt='Left slider arrow'
|
||||
onTap={() => {
|
||||
if (hourlyForecastGrid <= forecast.hourly.length / 2 - 1) setHourlyForecastGrid(hourlyForecastGrid + 1);
|
||||
}}
|
||||
/>
|
||||
<div className='slider-background'>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ transform: `translateX(${hourlyForecastGrid * 10}rem)` }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className='forecast-grid'
|
||||
>
|
||||
{
|
||||
forecast.hourly.map(hour => (
|
||||
<Card
|
||||
key={hour.dt}
|
||||
date={new Date(hour.dt * 1000)}
|
||||
time={true}
|
||||
data={hour.temp + '°C'}
|
||||
temp={hour.temp > 25 ? 'hot' : hour.temp < 20 ? 'cold' : null}
|
||||
icon={hour.weather[0].id}
|
||||
description={hour.weather[0].description}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.img
|
||||
src={RightArrow}
|
||||
alt='Right slider arrow'
|
||||
onTap={() => {
|
||||
if (hourlyForecastGrid >= - forecast.hourly.length / 2 + 1) setHourlyForecastGrid(hourlyForecastGrid - 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='wind'>
|
||||
<h2>Wind</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.wind.speed + 'km/h'}
|
||||
icon={781}
|
||||
/>
|
||||
</div>
|
||||
<div className='humidity'>
|
||||
<h2>Humidity</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.main.humidity + '%'}
|
||||
icon={741}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className='error'>
|
||||
<h2>{error}</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Wrapper>
|
||||
) : (
|
||||
<Wrapper>
|
||||
<Loader />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,205 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Wrapper } from './styles';
|
||||
import { Weather, WeatherResponse } from '../../types/weather';
|
||||
import { Forecast, ForecastResponse } from '../../types/forecast';
|
||||
import Loader from '../../components/Loader';
|
||||
import Card from '../../components/Card';
|
||||
import LeftArrow from '../../assets/left-arrow.svg';
|
||||
import RightArrow from '../../assets/right-arrow.svg';
|
||||
|
||||
const Home: React.FC = () => {
|
||||
const [weather, setWeather] = useState<Weather | null>(null);
|
||||
const [forecast, setForecast] = useState<Forecast | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [dailyForecastGrid, setDailyForecastGrid] = useState<number>(0);
|
||||
const [hourlyForecastGrid, setHourlyForecastGrid] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.geolocation)
|
||||
setError('Geolocation not supported in this browser! Try searching for a city instead');
|
||||
else
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async ({ coords }) => {
|
||||
try {
|
||||
let weatherRes: Response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/weather?lat=${coords.latitude}&lon=${coords.longitude}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&units=metric`
|
||||
);
|
||||
let weatherResBody: WeatherResponse = await weatherRes.json();
|
||||
setWeather({
|
||||
description: weatherResBody.weather[0].description,
|
||||
icon: weatherResBody.weather[0].id,
|
||||
main: weatherResBody.main,
|
||||
wind: weatherResBody.wind,
|
||||
name: weatherResBody.name,
|
||||
country: weatherResBody.sys.country,
|
||||
timestamp: weatherResBody.dt
|
||||
});
|
||||
let forecastRes: Response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/onecall?lat=${coords.latitude}&lon=${coords.longitude}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&exclude=minutely&units=metric`
|
||||
);
|
||||
let forecastResBody: ForecastResponse = await forecastRes.json();
|
||||
setForecast({
|
||||
hourly: forecastResBody.hourly,
|
||||
daily: forecastResBody.daily
|
||||
});
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
setError('Could not get weather data! Try again later');
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setLoading(false);
|
||||
setError('Geolocation not active! Try searching for a city instead');
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
setError('');
|
||||
setLoading(true);
|
||||
};
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
return !loading ? (
|
||||
<Wrapper>
|
||||
{weather && forecast && !error ? (
|
||||
<>
|
||||
<div className='main'>
|
||||
<h2>
|
||||
{weather.name}, {weather.country}
|
||||
</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.main.temp + '°C'}
|
||||
temp={weather.main.temp > 25 ? 'hot' : weather.main.temp < 20 ? 'cold' : null}
|
||||
icon={weather.icon}
|
||||
description={weather.description}
|
||||
/>
|
||||
</div>
|
||||
<div className='daily-forecast'>
|
||||
<h2>Daily Forecast</h2>
|
||||
<div className='slider'>
|
||||
<motion.img
|
||||
src={LeftArrow}
|
||||
alt='Left slider arrow'
|
||||
onTap={() => {
|
||||
if (dailyForecastGrid <= forecast.daily.length / 2 - 1)
|
||||
setDailyForecastGrid(dailyForecastGrid + 1);
|
||||
}}
|
||||
onMouseDown={() => {
|
||||
if (dailyForecastGrid <= forecast.daily.length / 2 - 1)
|
||||
setDailyForecastGrid(dailyForecastGrid + 1);
|
||||
}}
|
||||
/>
|
||||
<div className='slider-background'>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ transform: `translateX(${dailyForecastGrid * 10}rem)` }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className='forecast-grid'
|
||||
>
|
||||
{forecast.daily.map(day => (
|
||||
<Card
|
||||
key={day.dt}
|
||||
date={new Date(day.dt * 1000)}
|
||||
time={false}
|
||||
data={day.temp.min + '°C/' + day.temp.max + '°C'}
|
||||
temp={day.temp.max > 25 ? 'hot' : day.temp.max < 20 ? 'cold' : null}
|
||||
icon={day.weather[0].id}
|
||||
description={day.weather[0].description}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.img
|
||||
src={RightArrow}
|
||||
alt='Right slider arrow'
|
||||
onTap={() => {
|
||||
if (dailyForecastGrid >= -forecast.daily.length / 2 + 1)
|
||||
setDailyForecastGrid(dailyForecastGrid - 1);
|
||||
}}
|
||||
onMouseDown={() => {
|
||||
if (dailyForecastGrid >= -forecast.daily.length / 2 + 1)
|
||||
setDailyForecastGrid(dailyForecastGrid - 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='hourly-forecast'>
|
||||
<h2>Hourly Forecast</h2>
|
||||
<div className='slider'>
|
||||
<motion.img
|
||||
src={LeftArrow}
|
||||
alt='Left slider arrow'
|
||||
onTap={() => {
|
||||
if (hourlyForecastGrid <= forecast.hourly.length / 2 - 1)
|
||||
setHourlyForecastGrid(hourlyForecastGrid + 1);
|
||||
}}
|
||||
/>
|
||||
<div className='slider-background'>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ transform: `translateX(${hourlyForecastGrid * 10}rem)` }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className='forecast-grid'
|
||||
>
|
||||
{forecast.hourly.map(hour => (
|
||||
<Card
|
||||
key={hour.dt}
|
||||
date={new Date(hour.dt * 1000)}
|
||||
time={true}
|
||||
data={hour.temp + '°C'}
|
||||
temp={hour.temp > 25 ? 'hot' : hour.temp < 20 ? 'cold' : null}
|
||||
icon={hour.weather[0].id}
|
||||
description={hour.weather[0].description}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.img
|
||||
src={RightArrow}
|
||||
alt='Right slider arrow'
|
||||
onTap={() => {
|
||||
if (hourlyForecastGrid >= -forecast.hourly.length / 2 + 1)
|
||||
setHourlyForecastGrid(hourlyForecastGrid - 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='wind'>
|
||||
<h2>Wind</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.wind.speed + 'km/h'}
|
||||
icon={781}
|
||||
/>
|
||||
</div>
|
||||
<div className='humidity'>
|
||||
<h2>Humidity</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.main.humidity + '%'}
|
||||
icon={741}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className='error'>
|
||||
<h2>{error}</h2>
|
||||
</div>
|
||||
)}
|
||||
</Wrapper>
|
||||
) : (
|
||||
<Wrapper>
|
||||
<Loader />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
min-height: 85vh;
|
||||
padding: 2rem 0rem;
|
||||
display: grid;
|
||||
row-gap: 3rem;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main,
|
||||
.wind,
|
||||
.humidity {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.slider-background {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.forecast-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 10rem);
|
||||
grid-auto-flow: column;
|
||||
column-gap: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 85vh;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const NotFound: React.FC = () => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<h2>404 Page not found</h2>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotFound;
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Wrapper } from './styles';
|
||||
|
||||
const NotFound: React.FC = () => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<h2>404 Page not found</h2>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
height: 85vh;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
`;
|
||||
@@ -1,297 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Loader from '../components/Loader';
|
||||
import Card from '../components/Card';
|
||||
import LeftArrow from '../assets/left-arrow.svg';
|
||||
import RightArrow from '../assets/right-arrow.svg';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
min-height: 85vh;
|
||||
padding: 2rem 0rem;
|
||||
display: grid;
|
||||
row-gap: 3rem;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main, .wind, .humidity {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.slider-background {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.forecast-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 10rem);
|
||||
grid-auto-flow: column;
|
||||
column-gap: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface WeatherResponse {
|
||||
weather: Array<{ id: number, description: string }>,
|
||||
main: { temp: number, humidity: number },
|
||||
wind: { speed: number },
|
||||
name: string,
|
||||
sys: {
|
||||
country: string
|
||||
},
|
||||
dt: number,
|
||||
coord: {
|
||||
lat: number,
|
||||
lon: number
|
||||
}
|
||||
};
|
||||
|
||||
interface ForecastResponse {
|
||||
hourly: Array<{
|
||||
dt: number,
|
||||
temp: number,
|
||||
weather: Array<{ id: number, description: string }>
|
||||
}>,
|
||||
daily: Array<{
|
||||
dt: number,
|
||||
temp: { min: number, max: number },
|
||||
weather: Array<{ id: number, description: string }>
|
||||
}>
|
||||
};
|
||||
|
||||
interface Weather {
|
||||
description: string,
|
||||
icon: number,
|
||||
main: {
|
||||
temp: number,
|
||||
humidity: number
|
||||
},
|
||||
wind: {
|
||||
speed: number
|
||||
},
|
||||
country: string,
|
||||
timestamp: number,
|
||||
name: string
|
||||
};
|
||||
|
||||
interface Forecast {
|
||||
hourly: Array<{
|
||||
dt: number,
|
||||
temp: number,
|
||||
weather: Array<{ id: number, description: string }>
|
||||
}>,
|
||||
daily: Array<{
|
||||
dt: number,
|
||||
temp: { min: number, max: number },
|
||||
weather: Array<{ id: number, description: string }>
|
||||
}>
|
||||
};
|
||||
|
||||
const Search: React.FC = () => {
|
||||
const [ weather, setWeather ] = useState<Weather | null>(null);
|
||||
const [ forecast, setForecast ] = useState<Forecast | null>(null);
|
||||
const [ loading, setLoading ] = useState<boolean>(true);
|
||||
const [ error, setError ] = useState<string>('');
|
||||
const { query } = useParams<{ query: string }>();
|
||||
const [ dailyForecastGrid, setDailyForecastGrid ] = useState<number>(0);
|
||||
const [ hourlyForecastGrid, setHourlyForecastGrid ] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
let weatherRes: Response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/weather?q=${query}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&units=metric`
|
||||
);
|
||||
let weatherResBody: WeatherResponse = await weatherRes.json();
|
||||
setWeather({
|
||||
description: weatherResBody.weather[0].description,
|
||||
icon: weatherResBody.weather[0].id,
|
||||
main: weatherResBody.main,
|
||||
wind: weatherResBody.wind,
|
||||
name: weatherResBody.name,
|
||||
country: weatherResBody.sys.country,
|
||||
timestamp: weatherResBody.dt,
|
||||
});
|
||||
let coords = weatherResBody.coord;
|
||||
let forecastRes: Response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/onecall?lat=${coords.lat}&lon=${coords.lon}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&exclude=minutely&units=metric`
|
||||
);
|
||||
let forecastResBody: ForecastResponse = await forecastRes.json();
|
||||
setForecast({
|
||||
hourly: forecastResBody.hourly,
|
||||
daily: forecastResBody.daily
|
||||
});
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
setError('Could not find any weather data! Try again later');
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
setError('');
|
||||
setLoading(true);
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
return !loading ? (
|
||||
<Wrapper>
|
||||
{
|
||||
weather && forecast && !error ? (
|
||||
<>
|
||||
<div className='main'>
|
||||
<h2>{weather.name}, {weather.country}</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.main.temp + '°C'}
|
||||
temp={weather.main.temp > 25 ? 'hot' : weather.main.temp < 20 ? 'cold' : null}
|
||||
icon={weather.icon}
|
||||
description={weather.description}
|
||||
/>
|
||||
</div>
|
||||
<div className='daily-forecast'>
|
||||
<h2>Daily Forecast</h2>
|
||||
<div className='slider'>
|
||||
<motion.img
|
||||
src={LeftArrow}
|
||||
alt='Left slider arrow'
|
||||
onTap={() => {
|
||||
if (dailyForecastGrid <= forecast.daily.length / 2 - 1) setDailyForecastGrid(dailyForecastGrid + 1);
|
||||
}}
|
||||
/>
|
||||
<div className='slider-background'>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ transform: `translateX(${dailyForecastGrid * 10}rem)` }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className='forecast-grid'
|
||||
>
|
||||
{
|
||||
forecast.daily.map(day => (
|
||||
<Card
|
||||
key={day.dt}
|
||||
date={new Date(day.dt * 1000)}
|
||||
time={false}
|
||||
data={day.temp.min + '°C/' + day.temp.max + '°C'}
|
||||
temp={day.temp.max > 25 ? 'hot' : day.temp.max < 20 ? 'cold' : null}
|
||||
icon={day.weather[0].id}
|
||||
description={day.weather[0].description}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.img
|
||||
src={RightArrow}
|
||||
alt='Right slider arrow'
|
||||
onTap={() => {
|
||||
if (dailyForecastGrid >= - forecast.daily.length / 2 + 1) setDailyForecastGrid(dailyForecastGrid - 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='hourly-forecast'>
|
||||
<h2>Hourly Forecast</h2>
|
||||
<div className='slider'>
|
||||
<motion.img
|
||||
src={LeftArrow}
|
||||
alt='Left slider arrow'
|
||||
onTap={() => {
|
||||
if (hourlyForecastGrid <= forecast.hourly.length / 2 - 1) setHourlyForecastGrid(hourlyForecastGrid + 1);
|
||||
}}
|
||||
/>
|
||||
<div className='slider-background'>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ transform: `translateX(${hourlyForecastGrid * 10}rem)` }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className='forecast-grid'
|
||||
>
|
||||
{
|
||||
forecast.hourly.map(hour => (
|
||||
<Card
|
||||
key={hour.dt}
|
||||
date={new Date(hour.dt * 1000)}
|
||||
time={true}
|
||||
data={hour.temp + '°C'}
|
||||
temp={hour.temp > 25 ? 'hot' : hour.temp < 20 ? 'cold' : null}
|
||||
icon={hour.weather[0].id}
|
||||
description={hour.weather[0].description}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.img
|
||||
src={RightArrow}
|
||||
alt='Right slider arrow'
|
||||
onTap={() => {
|
||||
if (hourlyForecastGrid >= - forecast.hourly.length / 2 + 1) setHourlyForecastGrid(hourlyForecastGrid - 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='wind'>
|
||||
<h2>Wind</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.wind.speed + 'km/h'}
|
||||
icon={781}
|
||||
/>
|
||||
</div>
|
||||
<div className='humidity'>
|
||||
<h2>Humidity</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.main.humidity + '%'}
|
||||
icon={741}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className='error'>
|
||||
<h2>{error}</h2>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Wrapper>
|
||||
) : (
|
||||
<Wrapper>
|
||||
<Loader />
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
||||
@@ -0,0 +1,190 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Wrapper } from './styles';
|
||||
import { Weather, WeatherSearchResponse } from '../../types/weather';
|
||||
import { Forecast, ForecastResponse } from '../../types/forecast';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import Loader from '../../components/Loader';
|
||||
import Card from '../../components/Card';
|
||||
import LeftArrow from '../../assets/left-arrow.svg';
|
||||
import RightArrow from '../../assets/right-arrow.svg';
|
||||
|
||||
const Search: React.FC = () => {
|
||||
const [weather, setWeather] = useState<Weather | null>(null);
|
||||
const [forecast, setForecast] = useState<Forecast | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
const { query } = useParams<{ query: string }>();
|
||||
const [dailyForecastGrid, setDailyForecastGrid] = useState<number>(0);
|
||||
const [hourlyForecastGrid, setHourlyForecastGrid] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
let weatherRes: Response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/weather?q=${query}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&units=metric`
|
||||
);
|
||||
let weatherResBody: WeatherSearchResponse = await weatherRes.json();
|
||||
setWeather({
|
||||
description: weatherResBody.weather[0].description,
|
||||
icon: weatherResBody.weather[0].id,
|
||||
main: weatherResBody.main,
|
||||
wind: weatherResBody.wind,
|
||||
name: weatherResBody.name,
|
||||
country: weatherResBody.sys.country,
|
||||
timestamp: weatherResBody.dt
|
||||
});
|
||||
let coords = weatherResBody.coord;
|
||||
let forecastRes: Response = await fetch(
|
||||
`https://api.openweathermap.org/data/2.5/onecall?lat=${coords.lat}&lon=${coords.lon}&appid=${process.env.REACT_APP_WEATHER_API_KEY}&exclude=minutely&units=metric`
|
||||
);
|
||||
let forecastResBody: ForecastResponse = await forecastRes.json();
|
||||
setForecast({
|
||||
hourly: forecastResBody.hourly,
|
||||
daily: forecastResBody.daily
|
||||
});
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setLoading(false);
|
||||
setError('Could not find any weather data! Try again later');
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
setError('');
|
||||
setLoading(true);
|
||||
};
|
||||
}, [query]);
|
||||
|
||||
return !loading ? (
|
||||
<Wrapper>
|
||||
{weather && forecast && !error ? (
|
||||
<>
|
||||
<div className='main'>
|
||||
<h2>
|
||||
{weather.name}, {weather.country}
|
||||
</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.main.temp + '°C'}
|
||||
temp={weather.main.temp > 25 ? 'hot' : weather.main.temp < 20 ? 'cold' : null}
|
||||
icon={weather.icon}
|
||||
description={weather.description}
|
||||
/>
|
||||
</div>
|
||||
<div className='daily-forecast'>
|
||||
<h2>Daily Forecast</h2>
|
||||
<div className='slider'>
|
||||
<motion.img
|
||||
src={LeftArrow}
|
||||
alt='Left slider arrow'
|
||||
onTap={() => {
|
||||
if (dailyForecastGrid <= forecast.daily.length / 2 - 1)
|
||||
setDailyForecastGrid(dailyForecastGrid + 1);
|
||||
}}
|
||||
/>
|
||||
<div className='slider-background'>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ transform: `translateX(${dailyForecastGrid * 10}rem)` }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className='forecast-grid'
|
||||
>
|
||||
{forecast.daily.map(day => (
|
||||
<Card
|
||||
key={day.dt}
|
||||
date={new Date(day.dt * 1000)}
|
||||
time={false}
|
||||
data={day.temp.min + '°C/' + day.temp.max + '°C'}
|
||||
temp={day.temp.max > 25 ? 'hot' : day.temp.max < 20 ? 'cold' : null}
|
||||
icon={day.weather[0].id}
|
||||
description={day.weather[0].description}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.img
|
||||
src={RightArrow}
|
||||
alt='Right slider arrow'
|
||||
onTap={() => {
|
||||
if (dailyForecastGrid >= -forecast.daily.length / 2 + 1)
|
||||
setDailyForecastGrid(dailyForecastGrid - 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='hourly-forecast'>
|
||||
<h2>Hourly Forecast</h2>
|
||||
<div className='slider'>
|
||||
<motion.img
|
||||
src={LeftArrow}
|
||||
alt='Left slider arrow'
|
||||
onTap={() => {
|
||||
if (hourlyForecastGrid <= forecast.hourly.length / 2 - 1)
|
||||
setHourlyForecastGrid(hourlyForecastGrid + 1);
|
||||
}}
|
||||
/>
|
||||
<div className='slider-background'>
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ transform: `translateX(${hourlyForecastGrid * 10}rem)` }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className='forecast-grid'
|
||||
>
|
||||
{forecast.hourly.map(hour => (
|
||||
<Card
|
||||
key={hour.dt}
|
||||
date={new Date(hour.dt * 1000)}
|
||||
time={true}
|
||||
data={hour.temp + '°C'}
|
||||
temp={hour.temp > 25 ? 'hot' : hour.temp < 20 ? 'cold' : null}
|
||||
icon={hour.weather[0].id}
|
||||
description={hour.weather[0].description}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.img
|
||||
src={RightArrow}
|
||||
alt='Right slider arrow'
|
||||
onTap={() => {
|
||||
if (hourlyForecastGrid >= -forecast.hourly.length / 2 + 1)
|
||||
setHourlyForecastGrid(hourlyForecastGrid - 1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='wind'>
|
||||
<h2>Wind</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.wind.speed + 'km/h'}
|
||||
icon={781}
|
||||
/>
|
||||
</div>
|
||||
<div className='humidity'>
|
||||
<h2>Humidity</h2>
|
||||
<Card
|
||||
date={new Date(weather.timestamp * 1000)}
|
||||
time={false}
|
||||
data={weather.main.humidity + '%'}
|
||||
icon={741}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className='error'>
|
||||
<h2>{error}</h2>
|
||||
</div>
|
||||
)}
|
||||
</Wrapper>
|
||||
) : (
|
||||
<Wrapper>
|
||||
<Loader />
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Search;
|
||||
@@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
min-height: 85vh;
|
||||
padding: 2rem 0rem;
|
||||
display: grid;
|
||||
row-gap: 3rem;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main,
|
||||
.wind,
|
||||
.humidity {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.slider-background {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.forecast-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, 10rem);
|
||||
grid-auto-flow: column;
|
||||
column-gap: 2rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,25 @@
|
||||
export type Forecast = {
|
||||
hourly: Array<{
|
||||
dt: number;
|
||||
temp: number;
|
||||
weather: Array<{ id: number; description: string }>;
|
||||
}>;
|
||||
daily: Array<{
|
||||
dt: number;
|
||||
temp: { min: number; max: number };
|
||||
weather: Array<{ id: number; description: string }>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ForecastResponse = {
|
||||
hourly: Array<{
|
||||
dt: number;
|
||||
temp: number;
|
||||
weather: Array<{ id: number; description: string }>;
|
||||
}>;
|
||||
daily: Array<{
|
||||
dt: number;
|
||||
temp: { min: number; max: number };
|
||||
weather: Array<{ id: number; description: string }>;
|
||||
}>;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
export type Weather = {
|
||||
description: string;
|
||||
icon: number;
|
||||
main: {
|
||||
temp: number;
|
||||
humidity: number;
|
||||
};
|
||||
wind: {
|
||||
speed: number;
|
||||
};
|
||||
country: string;
|
||||
timestamp: number;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type WeatherResponse = {
|
||||
weather: Array<{ id: number; description: string }>;
|
||||
main: { temp: number; humidity: number };
|
||||
wind: { speed: number };
|
||||
name: string;
|
||||
sys: {
|
||||
country: string;
|
||||
};
|
||||
dt: number;
|
||||
};
|
||||
|
||||
export type WeatherSearchResponse = {
|
||||
weather: Array<{ id: number; description: string }>;
|
||||
main: { temp: number; humidity: number };
|
||||
wind: { speed: number };
|
||||
name: string;
|
||||
sys: {
|
||||
country: string;
|
||||
};
|
||||
dt: number;
|
||||
coord: {
|
||||
lat: number;
|
||||
lon: number;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user