├── .eslintrc
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .husky
└── pre-commit
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── babel.config.js
├── index.html
├── jest.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── logo.svg
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
├── screen.png
└── screenshot.png
├── setup-tests.js
├── src
├── __tests__
│ ├── about.test.js
│ ├── app.test.js
│ ├── forecast-card.test.js
│ ├── navbar.test.js
│ ├── search.test.js
│ └── weather-card.test.js
├── components
│ ├── about.jsx
│ ├── app.jsx
│ ├── footer.jsx
│ ├── forecast-card.jsx
│ ├── layout.jsx
│ ├── loading.jsx
│ ├── navbar.jsx
│ ├── search.jsx
│ ├── theme-context.jsx
│ ├── theme-toggle.jsx
│ ├── units-toggle.jsx
│ └── weather-card.jsx
├── hooks
│ └── useWeather.js
├── icons.js
├── index.css
├── index.jsx
├── lib
│ └── fetcher.js
├── recommendations.js
├── test
│ └── app-test-utils.js
└── weather-data.js
├── tailwind.config.js
├── turbo.json
└── vite.config.mjs
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "parser": "@babel/eslint-parser",
7 | "parserOptions": {
8 | "sourceType": "module"
9 | },
10 | "extends": [
11 | "eslint:recommended",
12 | "plugin:react/recommended",
13 | "plugin:jest/recommended"
14 | ],
15 | "plugins": ["react", "jest", "testing-library"],
16 | "rules": {},
17 | "settings": {
18 | "react": {
19 | "version": "detect"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 | on:
3 | push:
4 | branches: ['main']
5 | pull_request:
6 | branches: ['main']
7 |
8 | env:
9 | TURBO_API: 'http://127.0.0.1:9080'
10 | TURBO_TOKEN: ${{ secrets.TURBO_SERVER_TOKEN }}
11 | TURBO_TEAM: ${{ github.repository_owner }}
12 |
13 | jobs:
14 | build:
15 | name: 'Build'
16 | runs-on: ubuntu-latest
17 | strategy:
18 | matrix:
19 | node-version: [18]
20 | steps:
21 | - name: ⬇️ Checkout repo
22 | uses: actions/checkout@v4
23 | - uses: pnpm/action-setup@v2
24 | with:
25 | version: 7.0.0
26 | - name: Use Node.js ${{ matrix.node-version }}
27 | uses: actions/setup-node@v4
28 | with:
29 | node-version: ${{ matrix.node-version }}
30 | cache: 'pnpm'
31 |
32 | - name: 📀 Install dependencies
33 | run: pnpm install
34 |
35 | - name: Setup a local cache server for Turborepo
36 | uses: felixmosh/turborepo-gh-artifacts@v2
37 | with:
38 | repo-token: ${{ secrets.GITHUB_TOKEN }}
39 | server-token: ${{ secrets.TURBO_SERVER_TOKEN }}
40 |
41 | - name: 🧹 Lint and Test
42 | run: pnpm turbo lint check coverage --color
43 |
44 | - name: ⏫ Upload coverage
45 | uses: coverallsapp/github-action@master
46 | with:
47 | github-token: ${{ secrets.GITHUB_TOKEN }}
48 |
49 | - name: 👷♀️ Build
50 | run: pnpm turbo build --color --concurrency=5
51 |
52 | - name: ⏫ Upload build artifacts
53 | uses: actions/upload-artifact@v4
54 | with:
55 | name: build
56 | path: |
57 | build
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # CI/CD
9 | .vercel
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /build
16 |
17 | # misc
18 | .DS_Store
19 | .env
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | .turbo/
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | pnpm lint
5 | pnpm format
6 | pnpm check
7 | pnpm coverage
8 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | auto-install-peers = true
2 | strict-peer-dependencies = false
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | coverage
4 | public
5 | .vscode/
6 | .vercel
7 | validate.yml
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "trailingComma": "all",
4 | "tabWidth": 2,
5 | "endOfLine": "lf",
6 | "singleQuote": true
7 | }
8 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": [
3 | "javascript",
4 | "javascriptreact",
5 | { "language": "typescript", "autoFix": true },
6 | { "language": "typescriptreact", "autoFix": true }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Dennis Kigen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | 
6 |
7 | [](https://github.com/denniskigen/react-weather/actions/workflows/validate.yml)  [](https://coveralls.io/github/denniskigen/react-weather?branch=main)
8 |
9 | [React Weather](https://react-weather.denniskigen.com) is a beautiful weather app built on top of the [OpenWeatherMap API](https://openweathermap.org/api).
10 |
11 | ## Getting started
12 |
13 | - Sign up over at [openweathermap.org](https://openweathermap.org) and get an API key.
14 | - Fork the project and clone it locally.
15 | - Install dependencies using [pnpm](https://pnpm.io/installation):
16 |
17 | ```sh
18 | pnpm install
19 | ```
20 |
21 | - Create a file at the root of the project called `.env.local` with the following contents:
22 |
23 | ```
24 | VITE_API_URL = 'https://api.openweathermap.org/data/2.5'
25 | VITE_API_KEY = The API key you obtained from openweathermap.org
26 | VITE_ICON_URL = 'https://openweathermap.org/img/w'
27 | ```
28 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | const presets =
3 | process.env['NODE_ENV'] !== 'production'
4 | ? [
5 | ['@babel/preset-env', { targets: { node: 'current' } }],
6 | [
7 | '@babel/preset-react',
8 | {
9 | runtime: 'automatic',
10 | },
11 | ],
12 | ['babel-preset-vite'],
13 | ]
14 | : null;
15 |
16 | const plugins = [];
17 |
18 | module.exports = {
19 | presets,
20 | plugins,
21 | };
22 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
19 |
26 |
27 |
28 |
32 |
33 |
37 |
38 | React Weather
39 |
40 |
41 |
42 |
43 |
44 |
45 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | /*
3 | * For a detailed explanation regarding each configuration property, visit:
4 | * https://jestjs.io/docs/configuration
5 | */
6 |
7 | module.exports = {
8 | // All imported modules in your tests should be mocked automatically
9 | // automock: false,
10 |
11 | // Stop running tests after `n` failures
12 | // bail: 0,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/private/var/folders/5b/6_rzf9t52ms0k1fjn9f8m1_r0000gn/T/jest_dx",
16 |
17 | // Automatically clear mock calls, instances and results before every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | collectCoverage: true,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | collectCoverageFrom: ['src/components/*.{js,jsx}'],
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: 'coverage',
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | // coveragePathIgnorePatterns: [
31 | // "/node_modules/"
32 | // ],
33 |
34 | // Indicates which provider should be used to instrument code for coverage
35 | // coverageProvider: "babel",
36 |
37 | // A list of reporter names that Jest uses when writing coverage reports
38 | // coverageReporters: [
39 | // "json",
40 | // "text",
41 | // "lcov",
42 | // "clover"
43 | // ],
44 |
45 | // An object that configures minimum threshold enforcement for coverage results
46 | // coverageThreshold: undefined,
47 |
48 | // A path to a custom dependency extractor
49 | // dependencyExtractor: undefined,
50 |
51 | // Make calling deprecated APIs throw helpful error messages
52 | // errorOnDeprecated: false,
53 |
54 | // Force coverage collection from ignored files using an array of glob patterns
55 | // forceCoverageMatch: [],
56 |
57 | // A path to a module which exports an async function that is triggered once before all test suites
58 | // globalSetup: undefined,
59 |
60 | // A path to a module which exports an async function that is triggered once after all test suites
61 | // globalTeardown: undefined,
62 |
63 | // A set of global variables that need to be available in all test environments
64 | // globals: {},
65 |
66 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
67 | // maxWorkers: "50%",
68 |
69 | // An array of directory names to be searched recursively up from the requiring module's location
70 | // moduleDirectories: [
71 | // "node_modules"
72 | // ],
73 |
74 | // An array of file extensions your modules use
75 | // moduleFileExtensions: [
76 | // "js",
77 | // "jsx",
78 | // "ts",
79 | // "tsx",
80 | // "json",
81 | // "node"
82 | // ],
83 |
84 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
85 | moduleNameMapper: {
86 | // '^lodash-es$': 'lodash',
87 | },
88 |
89 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
90 | // modulePathIgnorePatterns: [],
91 |
92 | // Activates notifications for test results
93 | // notify: false,
94 |
95 | // An enum that specifies notification mode. Requires { notify: true }
96 | // notifyMode: "failure-change",
97 |
98 | // A preset that is used as a base for Jest's configuration
99 | // preset: undefined,
100 |
101 | // Run tests from one or more projects
102 | // projects: undefined,
103 |
104 | // Use this configuration option to add custom reporters to Jest
105 | reporters: ['default', 'github-actions'],
106 |
107 | // Automatically reset mock state before every test
108 | // resetMocks: false,
109 |
110 | // Reset the module registry before running each individual test
111 | // resetModules: false,
112 |
113 | // A path to a custom resolver
114 | // resolver: undefined,
115 |
116 | // Automatically restore mock state and implementation before every test
117 | // restoreMocks: false,
118 |
119 | // The root directory that Jest should scan for tests and modules within
120 | // rootDir: undefined,
121 |
122 | // A list of paths to directories that Jest should use to search for files in
123 | roots: ['/src'],
124 |
125 | // Allows you to use a custom runner instead of Jest's default test runner
126 | // runner: "jest-runner",
127 |
128 | // The paths to modules that run some code to configure or set up the testing environment before each test
129 | // setupFiles: [],
130 |
131 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
132 | setupFilesAfterEnv: ['/setup-tests.js'],
133 |
134 | // The number of seconds after which a test is considered as slow and reported as such in the results.
135 | // slowTestThreshold: 5,
136 |
137 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
138 | // snapshotSerializers: [],
139 |
140 | // The test environment that will be used for testing
141 | testEnvironment: 'jsdom',
142 |
143 | // Options that will be passed to the testEnvironment
144 | // testEnvironmentOptions: {},
145 |
146 | // Adds a location field to test results
147 | // testLocationInResults: false,
148 |
149 | // The glob patterns Jest uses to detect test files
150 | // testMatch: [
151 | // "**/__tests__/**/*.[jt]s?(x)",
152 | // "**/?(*.)+(spec|test).[tj]s?(x)"
153 | // ],
154 |
155 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
156 | // testPathIgnorePatterns: [
157 | // "/node_modules/"
158 | // ],
159 |
160 | // The regexp pattern or array of patterns that Jest uses to detect test files
161 | // testRegex: [],
162 |
163 | // This option allows the use of a custom results processor
164 | // testResultsProcessor: undefined,
165 |
166 | // This option allows use of a custom test runner
167 | // testRunner: "jest-circus/runner",
168 |
169 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
170 | // testURL: "http://localhost",
171 |
172 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
173 | // timers: "real",
174 |
175 | // A map from regular expressions to paths to transformers
176 | transform: {
177 | '^.+\\.(js|jsx)$': '/node_modules/babel-jest',
178 | },
179 |
180 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
181 | transformIgnorePatterns: ['/node_modules/(?!lodash-es)'],
182 |
183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
184 | // unmockedModulePathPatterns: undefined,
185 |
186 | // Indicates whether each individual test should be reported during the run
187 | // verbose: undefined,
188 |
189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
190 | // watchPathIgnorePatterns: [],
191 |
192 | // Whether to use watchman for file crawling
193 | // watchman: true,
194 | };
195 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-weather",
3 | "version": "1.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "TIMING=1 eslint src --ext js",
10 | "format": "prettier --write \"**/*.+(js|json|css|html)\"",
11 | "check": "prettier --check \"**/*.+(js|json|css|html)\"",
12 | "coverage": "jest --coverage",
13 | "test": "jest",
14 | "test:watch": "jest --watchAll",
15 | "prepare": "husky install"
16 | },
17 | "browserslist": {
18 | "production": [
19 | ">0.2%",
20 | "not dead",
21 | "not op_mini all"
22 | ],
23 | "development": [
24 | "last 1 chrome version",
25 | "last 1 firefox version",
26 | "last 1 safari version"
27 | ]
28 | },
29 | "dependencies": {
30 | "@tailwindcss/typography": "^0.5.10",
31 | "dayjs": "^1.11.10",
32 | "lodash-es": "^4.17.21",
33 | "prop-types": "^15.8.1",
34 | "react": "^18.2.0",
35 | "react-dom": "^18.2.0",
36 | "react-icons": "^4.12.0",
37 | "react-router-dom": "^6.21.1",
38 | "swr": "^2.2.4",
39 | "vercel-toast": "^1.8.0"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.23.7",
43 | "@babel/eslint-parser": "^7.23.3",
44 | "@babel/preset-env": "^7.23.7",
45 | "@babel/preset-react": "^7.23.3",
46 | "@testing-library/jest-dom": "^6.1.6",
47 | "@testing-library/react": "^14.1.2",
48 | "@testing-library/user-event": "^14.5.2",
49 | "@vitejs/plugin-react": "^4.2.1",
50 | "autoprefixer": "^10.4.16",
51 | "babel-jest": "^29.7.0",
52 | "babel-plugin-transform-vite-meta-env": "^1.0.3",
53 | "babel-preset-vite": "^1.1.2",
54 | "eslint": "^8.56.0",
55 | "eslint-plugin-jest": "^27.6.0",
56 | "eslint-plugin-jest-dom": "^5.1.0",
57 | "eslint-plugin-react": "^7.33.2",
58 | "eslint-plugin-react-hooks": "^4.6.0",
59 | "eslint-plugin-testing-library": "^6.2.0",
60 | "husky": "^8.0.3",
61 | "jest": "^29.7.0",
62 | "jest-environment-jsdom": "^29.7.0",
63 | "jest-fetch-mock": "^3.0.3",
64 | "postcss": "^8.4.32",
65 | "prettier": "^3.1.1",
66 | "prettier-plugin-tailwindcss": "^0.5.10",
67 | "pretty-quick": "^3.1.3",
68 | "react-test-renderer": "^18.2.0",
69 | "tailwindcss": "^3.4.0",
70 | "turbo": "^1.11.2",
71 | "vite": "^5.0.10"
72 | },
73 | "husky": {
74 | "hooks": {
75 | "pre-commit": "pretty-quick --staged && pnpm run lint && pnpm run format && pnpm run check && pnpm run coverage"
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | plugins: {
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React Weather",
3 | "name": "React Weather",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/public/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/screen.png
--------------------------------------------------------------------------------
/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denniskigen/react-weather/34a1287be3712556c35be9e5dd112006acc2b1e6/public/screenshot.png
--------------------------------------------------------------------------------
/setup-tests.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | import '@testing-library/jest-dom';
3 |
4 | import.meta.env.VITE_API_URL = 'https://api.openweathermap.org/data/2.5';
5 | import.meta.env.VITE_API_KEY = 'ed09485536a14ea098e9de03ecff2d4d';
6 |
7 | beforeAll(() =>
8 | Object.defineProperty(window, 'matchMedia', {
9 | writable: true,
10 | value: jest.fn().mockImplementation((query) => ({
11 | matches: false,
12 | media: query,
13 | onchange: null,
14 | addListener: jest.fn(), // Deprecated
15 | removeListener: jest.fn(), // Deprecated
16 | addEventListener: jest.fn(),
17 | removeEventListener: jest.fn(),
18 | dispatchEvent: jest.fn(),
19 | })),
20 | }),
21 | );
22 |
--------------------------------------------------------------------------------
/src/__tests__/about.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '../test/app-test-utils';
3 | import { BrowserRouter } from 'react-router-dom';
4 |
5 | import About from '../components/about';
6 |
7 | describe('About', () => {
8 | test('renders the about page', async () => {
9 | renderAbout();
10 |
11 | expect(
12 | screen.getByRole('heading', { name: /about reactweather/i }),
13 | ).toBeInTheDocument();
14 | expect(
15 | screen.getByRole('link', { name: /openweathermap api/i }),
16 | ).toBeInTheDocument();
17 | expect(
18 | screen.getByRole('link', { name: /tailwindcss/i }),
19 | ).toBeInTheDocument();
20 | expect(
21 | screen.getByRole('link', { name: /erik flowers' weather icons/i }),
22 | ).toBeInTheDocument();
23 | expect(screen.getByRole('link', { name: /vercel/i })).toBeInTheDocument();
24 | expect(screen.getByAltText(/buy me a coffee/i)).toBeInTheDocument();
25 | });
26 | });
27 |
28 | function renderAbout() {
29 | render(
30 |
31 |
32 | ,
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/__tests__/app.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, userEvent } from '../test/app-test-utils';
3 | import App from '../components/app';
4 |
5 | jest.mock('lodash-es/debounce', () => jest.fn((fn) => fn));
6 |
7 | describe('App', () => {
8 | test('renders a light theme by default', async () => {
9 | const user = userEvent.setup();
10 | renderApp();
11 |
12 | const themeToggle = screen.getByTitle(/dark theme/i);
13 |
14 | expect(themeToggle).toBeInTheDocument();
15 |
16 | await user.click(themeToggle);
17 |
18 | expect(screen.queryByTitle('dark theme')).not.toBeInTheDocument();
19 | expect(screen.getByTitle('light theme')).toBeInTheDocument();
20 | });
21 |
22 | test('navigates through the app when navbar links are clicked', async () => {
23 | const user = userEvent.setup();
24 | renderApp();
25 |
26 | const aboutLink = screen.getByText(/about/i);
27 | const leftClick = { button: 0 };
28 |
29 | await user.click(aboutLink, leftClick);
30 |
31 | expect(
32 | screen.getByRole('heading', { name: /about reactweather/i }),
33 | ).toBeInTheDocument();
34 | expect(
35 | screen.getByText(/ReactWeather is a beautiful weather app/i),
36 | ).toBeInTheDocument();
37 |
38 | const homeLink = screen.getAllByRole('heading', {
39 | name: /^reactweather$/i,
40 | })[0];
41 |
42 | await user.click(homeLink, leftClick);
43 |
44 | expect(screen.queryByText(/about reactweather/i)).not.toBeInTheDocument();
45 | expect(screen.getByRole('search')).toBeInTheDocument();
46 | expect(
47 | screen.getByPlaceholderText(/search for a location/i),
48 | ).toBeInTheDocument();
49 | });
50 |
51 | test('toggles units between celsius and fahrenheit', async () => {
52 | const user = userEvent.setup();
53 | renderApp();
54 |
55 | let openToggleUnitsMenuButton = screen.getByRole('button', {
56 | name: /open units toggle menu/i,
57 | });
58 |
59 | await user.click(openToggleUnitsMenuButton);
60 |
61 | let toggleUnitsMenu = screen.queryByRole('menuitem', {
62 | name: /change units/i,
63 | });
64 |
65 | expect(toggleUnitsMenu).toHaveTextContent(/Imperial \(F°, mph\)/);
66 |
67 | await user.click(toggleUnitsMenu);
68 |
69 | openToggleUnitsMenuButton = screen.queryByRole('button', {
70 | name: /open units toggle menu/i,
71 | });
72 |
73 | await user.click(openToggleUnitsMenuButton);
74 |
75 | toggleUnitsMenu = screen.queryByRole('menuitem', {
76 | name: /change units/i,
77 | });
78 |
79 | expect(toggleUnitsMenu).toHaveTextContent(/Metric \(C°, m\/s\)/);
80 | });
81 | });
82 |
83 | function renderApp() {
84 | render();
85 | }
86 |
--------------------------------------------------------------------------------
/src/__tests__/forecast-card.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '../test/app-test-utils';
3 | import { mockForecast } from '../weather-data';
4 | import ForecastCard from '../components/forecast-card';
5 |
6 | const testProps = {
7 | location: 'Eldoret',
8 | units: 'metric',
9 | };
10 |
11 | jest.mock('../hooks/useWeather', () => {
12 | const actualModule = jest.requireActual('../hooks/useWeather');
13 |
14 | return {
15 | ...actualModule,
16 | useWeather: jest.fn().mockImplementation(() => ({
17 | forecast: mockForecast,
18 | isError: null,
19 | isLoading: false,
20 | })),
21 | };
22 | });
23 |
24 | describe('ForecastCard', () => {
25 | test('renders the weekly forecast for the specified location', () => {
26 | renderForecast(testProps);
27 |
28 | const forecast = screen.getAllByRole('listitem').map((listItem) => {
29 | return listItem.textContent;
30 | });
31 |
32 | const expected = [
33 | 'Wednesday14° / 14°',
34 | 'Thursday15° / 15°',
35 | 'Friday17° / 17°',
36 | 'Saturday13° / 13°',
37 | 'Sunday16° / 16°',
38 | ];
39 |
40 | expect(forecast).toEqual(expect.arrayContaining(expected));
41 | });
42 | });
43 |
44 | function renderForecast(testProps) {
45 | render();
46 | }
47 |
--------------------------------------------------------------------------------
/src/__tests__/navbar.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter } from 'react-router-dom';
3 | import { render, screen, userEvent, waitFor } from '../test/app-test-utils';
4 | import Navbar from '../components/navbar';
5 |
6 | describe('NavBar', () => {
7 | test('renders the navbar', async () => {
8 | renderNavbar();
9 |
10 | expect(
11 | screen.getByRole('button', { name: /open main menu/i }),
12 | ).toBeInTheDocument();
13 | expect(screen.getByRole('link', { name: /about/i })).toBeInTheDocument();
14 | expect(
15 | screen.getByRole('link', { name: /react weather on github/i }),
16 | ).toBeInTheDocument();
17 | expect(screen.getByRole('switch')).toBeInTheDocument();
18 | });
19 |
20 | test('clicking the button toggles displaying the main menu', async () => {
21 | const user = userEvent.setup();
22 | renderNavbar();
23 |
24 | const toggleButton = screen.getByRole('button', {
25 | name: /open main menu/i,
26 | });
27 |
28 | await waitFor(() => user.click(toggleButton));
29 | expect(
30 | screen.queryByRole('button', { name: /open main menu/i }),
31 | ).not.toBeInTheDocument();
32 | expect(
33 | screen.queryByRole('button', { name: /close main menu/i }),
34 | ).toBeInTheDocument();
35 |
36 | await waitFor(() => user.click(toggleButton));
37 | expect(
38 | screen.queryByRole('button', { name: /close main menu/i }),
39 | ).not.toBeInTheDocument();
40 | expect(
41 | screen.queryByRole('button', { name: /open main menu/i }),
42 | ).toBeInTheDocument();
43 | });
44 | });
45 |
46 | function renderNavbar() {
47 | render(
48 |
49 |
50 | ,
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/__tests__/search.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '../test/app-test-utils';
3 | import Search from '../components/search';
4 |
5 | const testProps = {
6 | error: new Error(''),
7 | location: 'Test Location',
8 | onLocationChange: () => {},
9 | isSearching: false,
10 | };
11 |
12 | describe('Search', () => {
13 | test('renders a search box above the weather card', async () => {
14 | renderSearch();
15 |
16 | expect(screen.getByRole('search')).toBeInTheDocument();
17 | expect(
18 | screen.getByPlaceholderText(/search for a location/i),
19 | ).toBeInTheDocument();
20 | });
21 | });
22 |
23 | function renderSearch() {
24 | render();
25 | }
26 |
--------------------------------------------------------------------------------
/src/__tests__/weather-card.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '../test/app-test-utils';
3 | import { mockWeather } from '../weather-data';
4 | import WeatherCard from '../components/weather-card';
5 |
6 | const testProps = {
7 | location: 'Eldoret',
8 | units: 'metric',
9 | };
10 |
11 | jest.mock('../hooks/useWeather', () => {
12 | const actualModule = jest.requireActual('../hooks/useWeather');
13 |
14 | return {
15 | ...actualModule,
16 | useWeather: jest.fn().mockImplementation(() => ({
17 | weather: mockWeather,
18 | isError: null,
19 | isLoading: false,
20 | })),
21 | };
22 | });
23 |
24 | describe('WeatherCard', () => {
25 | test('renders the WeatherCard', () => {
26 | renderWeatherCard();
27 |
28 | expect(screen.getByText(/eldoret, ke/i)).toBeInTheDocument();
29 | expect(screen.getByText(/few clouds/i)).toBeInTheDocument();
30 | expect(screen.getByText(/19/i)).toBeInTheDocument();
31 | expect(screen.getByText(/feels like 17°/i)).toBeInTheDocument();
32 | expect(screen.getByText(/24m\/s winds/i)).toBeInTheDocument();
33 | expect(screen.getByText(/68% humidity/i)).toBeInTheDocument();
34 | });
35 | });
36 |
37 | function renderWeatherCard() {
38 | render();
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/about.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Layout from './layout';
3 |
4 | const About = () => {
5 | return (
6 | <>
7 |
8 |
93 |
106 |
107 | >
108 | );
109 | };
110 |
111 | export default About;
112 |
--------------------------------------------------------------------------------
/src/components/app.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
3 | import debounce from 'lodash-es/debounce';
4 |
5 | import About from './about';
6 | import Layout from './layout';
7 | import Search from './search';
8 | import ForecastCard from './forecast-card';
9 | import WeatherCard from './weather-card';
10 | import UnitsToggle from './units-toggle';
11 |
12 | const searchTimeoutInMs = 500;
13 |
14 | export default function App() {
15 | const [location, setLocation] = React.useState('Eldoret');
16 | const [isSearching, setIsSearching] = React.useState(false);
17 | const [units, setUnits] = React.useState('metric');
18 |
19 | const debounceSearch = React.useMemo(
20 | () =>
21 | debounce((searchTerm) => {
22 | setLocation(searchTerm);
23 | }, searchTimeoutInMs),
24 | [],
25 | );
26 |
27 | const handleLocationChange = (location) => {
28 | setIsSearching(true);
29 | if (location) {
30 | debounceSearch(location);
31 | }
32 | setIsSearching(false);
33 | };
34 |
35 | const handleUnitsChange = (newUnits) => {
36 | setUnits(newUnits);
37 | };
38 |
39 | return (
40 |
41 |
42 |
43 |
47 |
48 |
49 |
53 | handleLocationChange(event.target.value)
54 | }
55 | />
56 |
57 |
58 |
59 |
60 |
64 |
65 |
66 |
67 | }
68 | />
69 | } />
70 |
71 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Footer = () => {
4 | return (
5 |
22 | );
23 | };
24 |
25 | export default Footer;
26 |
--------------------------------------------------------------------------------
/src/components/forecast-card.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import dayjs from 'dayjs';
3 | import PropTypes from 'prop-types';
4 | import { useWeather } from '../hooks/useWeather';
5 | import Loading from './loading';
6 |
7 | const Forecast = ({ location, units }) => {
8 | const { forecast, isLoading, isError } = useWeather(
9 | 'forecast',
10 | location,
11 | units,
12 | );
13 |
14 | if (isLoading || isError) return ;
15 | return (
16 | <>
17 |
18 |
19 | {forecast.map((item, index) => {
20 | return (
21 |
22 | -
23 |
24 | {dayjs(item.dt_txt).format('dddd')}
25 |
26 |
27 |
28 |
29 |
30 | {item.min}° / {item.max}°
31 |
32 |
33 |
34 | );
35 | })}
36 |
37 |
38 | >
39 | );
40 | };
41 |
42 | Forecast.propTypes = {
43 | location: PropTypes.string.isRequired,
44 | units: PropTypes.string.isRequired,
45 | };
46 |
47 | export default Forecast;
48 |
--------------------------------------------------------------------------------
/src/components/layout.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import Footer from './footer';
5 | import NavBar from './navbar';
6 |
7 | const Layout = ({ children }) => {
8 | return (
9 |
10 |
11 |
{children}
12 |
13 |
14 | );
15 | };
16 |
17 | Layout.propTypes = {
18 | children: PropTypes.node,
19 | };
20 |
21 | export default Layout;
22 |
--------------------------------------------------------------------------------
/src/components/loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loading = () => {
4 | return (
5 |
24 | );
25 | };
26 |
27 | export default Loading;
28 |
--------------------------------------------------------------------------------
/src/components/navbar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Toggle from './theme-toggle';
4 |
5 | const NavBar = () => {
6 | const [isMenuOpened, setIsMenuOpened] = React.useState(false);
7 |
8 | const toggleButton = () => {
9 | setIsMenuOpened(!isMenuOpened);
10 | };
11 |
12 | return (
13 |
173 | );
174 | };
175 |
176 | export default NavBar;
177 |
--------------------------------------------------------------------------------
/src/components/search.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const Search = ({ isSearching, onLocationChange }) => {
5 | return (
6 |
7 |
8 |
14 |
21 |
22 |
29 | {isSearching ? (
30 |
36 | Search for a location
37 |
46 |
51 |
52 | ) : null}
53 |
54 |
55 | );
56 | };
57 |
58 | Search.propTypes = {
59 | isSearching: PropTypes.bool.isRequired,
60 | onLocationChange: PropTypes.func.isRequired,
61 | };
62 |
63 | export default Search;
64 |
--------------------------------------------------------------------------------
/src/components/theme-context.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const getInitialTheme = () => {
5 | if (typeof window !== 'undefined' && window.localStorage) {
6 | const storedPrefs = window.localStorage.getItem('color-theme');
7 | if (typeof storedPrefs === 'string') {
8 | return storedPrefs;
9 | }
10 | const userMedia = window.matchMedia('(prefers-color-scheme: dark)');
11 | if (userMedia?.matches) {
12 | return 'dark';
13 | }
14 | }
15 | // If you want to use dark theme as the default, return 'dark' instead
16 | return 'light';
17 | };
18 |
19 | export const ThemeContext = React.createContext();
20 |
21 | export const ThemeProvider = ({ initialTheme, children }) => {
22 | const [theme, setTheme] = React.useState(getInitialTheme);
23 |
24 | const rawSetTheme = (rawTheme) => {
25 | const root = window.document.documentElement;
26 | const isDark = rawTheme === 'dark';
27 | root.classList.remove(isDark ? 'light' : 'dark');
28 | root.classList.add(rawTheme);
29 | localStorage.setItem('color', rawTheme);
30 | };
31 |
32 | if (initialTheme) {
33 | rawSetTheme(initialTheme);
34 | }
35 |
36 | React.useEffect(() => {
37 | rawSetTheme(theme);
38 | }, [theme]);
39 |
40 | return (
41 |
42 | {children}
43 |
44 | );
45 | };
46 |
47 | ThemeProvider.propTypes = {
48 | initialTheme: PropTypes.string.isRequired,
49 | children: PropTypes.node,
50 | };
51 |
--------------------------------------------------------------------------------
/src/components/theme-toggle.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { HiMoon, HiSun } from 'react-icons/hi';
3 | import { ThemeContext } from './theme-context';
4 |
5 | const Toggle = () => {
6 | const { theme, setTheme } = React.useContext(ThemeContext);
7 |
8 | return (
9 |
13 | {theme === 'dark' ? (
14 | setTheme(theme === 'dark' ? 'light' : 'dark')}
16 | className="cursor-pointer fill-yellow-300 text-2xl"
17 | title="light theme"
18 | />
19 | ) : (
20 | setTheme(theme === 'dark' ? 'light' : 'dark')}
22 | className="cursor-pointer fill-slate-600 text-2xl"
23 | title="dark theme"
24 | />
25 | )}
26 |
27 | );
28 | };
29 |
30 | export default Toggle;
31 |
--------------------------------------------------------------------------------
/src/components/units-toggle.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | const UnitsToggle = ({ units, onUnitsChange }) => {
5 | const [isSettingsMenuOpen, setIsSettingsMenuOpen] = React.useState(false);
6 | const [isMetric, setIsMetric] = React.useState(
7 | units.match(/metric/i) ? true : false,
8 | );
9 |
10 | const toggleSettingsMenu = () => {
11 | setIsSettingsMenuOpen(!isSettingsMenuOpen);
12 | };
13 |
14 | const handleChange = () => {
15 | onUnitsChange(units.match(/metric/i) ? 'imperial' : 'metric');
16 | setIsMetric(!isMetric);
17 | toggleSettingsMenu();
18 | };
19 |
20 | return (
21 |
22 |
54 | {isSettingsMenuOpen ? (
55 |
61 |
62 | -
67 | Change units
68 |
69 |
70 | {isMetric ? 'Imperial' : 'Metric'}{' '}
71 | {isMetric ? `(F°, mph)` : `(C°, m/s)`}
72 |
73 |
74 |
75 |
76 | ) : null}
77 |
78 | );
79 | };
80 |
81 | UnitsToggle.propTypes = {
82 | units: PropTypes.string.isRequired,
83 | onUnitsChange: PropTypes.func.isRequired,
84 | };
85 |
86 | export default UnitsToggle;
87 |
--------------------------------------------------------------------------------
/src/components/weather-card.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import dayjs from 'dayjs';
3 | import utc from 'dayjs/plugin/utc';
4 | dayjs.extend(utc);
5 | import Loading from './loading';
6 | import { useWeather } from '../hooks/useWeather';
7 | import PropTypes from 'prop-types';
8 |
9 | const WeatherCard = ({ location, units }) => {
10 | const isMetric = units.match(/metric/i);
11 |
12 | const { weather, isLoading, isError } = useWeather(
13 | 'weather',
14 | location,
15 | units,
16 | );
17 |
18 | if (isLoading || isError) return ;
19 | return (
20 | <>
21 |
22 |
23 |
24 | {weather.location}, {weather.country}
25 |
26 |
27 | {dayjs(weather.date).format('dddd')},{' '}
28 | {dayjs
29 | .utc(weather.date)
30 | .utcOffset(weather.timezone)
31 | .format('h:mm A')}
32 | , {weather.description}
33 |
34 |
35 |
36 |
37 | {weather.temperature}°
38 |
39 | Feels like {weather.feels_like}°
40 | {' '}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {weather.wind_speed}
50 | {isMetric ? `m/s` : `mph`} winds
51 |
52 |
53 |
54 | {weather.humidity}% humidity
55 |
56 |
57 |
58 | {weather.weatherRecommendation}
59 |
60 |
61 | >
62 | );
63 | };
64 |
65 | export default WeatherCard;
66 |
67 | WeatherCard.propTypes = {
68 | location: PropTypes.string,
69 | units: PropTypes.string,
70 | };
71 |
--------------------------------------------------------------------------------
/src/hooks/useWeather.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import utc from 'dayjs/plugin/utc';
3 | dayjs.extend(utc);
4 | import useSWR from 'swr';
5 | import fetcher from '../lib/fetcher';
6 | import recommendations from '../recommendations';
7 | import weatherIcons from '../icons.js';
8 |
9 | const iconPrefix = `wi wi-`;
10 | const API_KEY = import.meta.env.VITE_API_KEY;
11 | const BASE_URL = import.meta.env.VITE_API_URL;
12 |
13 | if (!BASE_URL || !API_KEY) {
14 | throw new Error('API URL or Key is not defined in environment variables');
15 | }
16 |
17 | export function useWeather(endpoint, location, units) {
18 | const apiEndpoint = `?q=${location}&units=${units}&APPID=${API_KEY}`;
19 |
20 | const { data, error, isLoading } = useSWR(
21 | `${BASE_URL}/${endpoint}/${apiEndpoint}`,
22 | fetcher,
23 | );
24 |
25 | const { weather, list } = data || {};
26 |
27 | return endpoint === 'weather'
28 | ? {
29 | weather: weather ? mapResponseProperties(data) : null,
30 | isLoading,
31 | isError: error,
32 | }
33 | : {
34 | forecast: list
35 | ? list
36 | .filter((f) => f.dt_txt.match(/09:00:00/))
37 | .map(mapResponseProperties)
38 | : null,
39 | isError: error,
40 | isLoading,
41 | };
42 | }
43 |
44 | function mapResponseProperties(data) {
45 | const mapped = {
46 | location: data.name,
47 | condition: data.cod,
48 | country: data.sys.country,
49 | date: data.dt,
50 | description: data.weather[0].description,
51 | feels_like: Math.round(data.main.feels_like),
52 | humidity: data.main.humidity,
53 | icon_id: data.weather[0].id,
54 | sunrise: data.sys.sunrise,
55 | sunset: data.sys.sunset,
56 | temperature: Math.round(data.main.temp),
57 | timezone: data.timezone / 3600, // convert from seconds to hours
58 | wind_speed: Math.round(data.wind.speed * 3.6), // convert from m/s to km/h
59 | };
60 |
61 | // Add extra properties for the five day forecast: dt_txt, icon, min, max
62 | if (data.dt_txt) {
63 | mapped.dt_txt = data.dt_txt;
64 | mapped.forecastIcon = iconPrefix + weatherIcons['day'][mapped.icon_id].icon;
65 | }
66 |
67 | if (mapped.sunset || mapped.sunrise) {
68 | mapped.currentTime = dayjs
69 | .utc(dayjs.unix(mapped.date))
70 | .utcOffset(mapped.timezone)
71 | .format();
72 | mapped.sunrise = dayjs
73 | .utc(dayjs.unix(mapped.sunrise))
74 | .utcOffset(mapped.timezone)
75 | .format();
76 | mapped.sunset = dayjs
77 | .utc(dayjs.unix(mapped.sunset))
78 | .utcOffset(mapped.timezone)
79 | .format();
80 | mapped.isDay =
81 | mapped.currentTime > mapped.sunrise && mapped.currentTime < mapped.sunset
82 | ? true
83 | : false;
84 | mapped.weatherIcon =
85 | iconPrefix +
86 | weatherIcons[mapped.isDay ? 'day' : 'night'][mapped.icon_id].icon;
87 | mapped.weatherRecommendation =
88 | recommendations[mapped.isDay ? 'day' : 'night'][
89 | mapped.icon_id
90 | ].recommendation;
91 | }
92 |
93 | if (data.weather[0].description) {
94 | mapped.description =
95 | data.weather[0].description.charAt(0).toUpperCase() +
96 | data.weather[0].description.slice(1);
97 | }
98 |
99 | if (data.main.temp_min && data.main.temp_max) {
100 | mapped.max = Math.round(data.main.temp_max);
101 | mapped.min = Math.round(data.main.temp_min);
102 | }
103 |
104 | // remove undefined fields
105 | Object.entries(mapped).map(
106 | ([key, value]) => value === undefined && delete mapped[key],
107 | );
108 |
109 | return mapped;
110 | }
111 |
--------------------------------------------------------------------------------
/src/icons.js:
--------------------------------------------------------------------------------
1 | const weatherIcons = {
2 | day: {
3 | '200': {
4 | label: 'thunderstorm with light rain',
5 | icon: 'thunderstorm',
6 | },
7 | '201': {
8 | label: 'thunderstorm with rain',
9 | icon: 'thunderstorm',
10 | },
11 | '202': {
12 | label: 'thunderstorm with heavy rain',
13 | icon: 'thunderstorm',
14 | },
15 | '210': {
16 | label: 'light thunderstorm',
17 | icon: 'lightning',
18 | },
19 | '211': {
20 | label: 'thunderstorm',
21 | icon: 'lightning',
22 | },
23 | '212': {
24 | label: 'heavy thunderstorm',
25 | icon: 'lightning',
26 | },
27 | '221': {
28 | label: 'ragged thunderstorm',
29 | icon: 'lightning',
30 | },
31 | '230': {
32 | label: 'thunderstorm with light drizzle',
33 | icon: 'thunderstorm',
34 | },
35 | '231': {
36 | label: 'thunderstorm with drizzle',
37 | icon: 'thunderstorm',
38 | },
39 | '232': {
40 | label: 'thunderstorm with heavy drizzle',
41 | icon: 'thunderstorm',
42 | },
43 | '300': {
44 | label: 'light intensity drizzle',
45 | icon: 'sprinkle',
46 | },
47 | '301': {
48 | label: 'drizzle',
49 | icon: 'sprinkle',
50 | },
51 | '302': {
52 | label: 'heavy intensity drizzle',
53 | icon: 'rain',
54 | },
55 | '310': {
56 | label: 'light intensity drizzle rain',
57 | icon: 'rain',
58 | },
59 | '311': {
60 | label: 'drizzle rain',
61 | icon: 'rain',
62 | },
63 | '312': {
64 | label: 'heavy intensity drizzle rain',
65 | icon: 'rain',
66 | },
67 | '313': {
68 | label: 'shower rain and drizzle',
69 | icon: 'rain',
70 | },
71 | '314': {
72 | label: 'heavy shower rain and drizzle',
73 | icon: 'rain',
74 | },
75 | '321': {
76 | label: 'shower drizzle',
77 | icon: 'sprinkle',
78 | },
79 | '500': {
80 | label: 'light rain',
81 | icon: 'sprinkle',
82 | },
83 | '501': {
84 | label: 'moderate rain',
85 | icon: 'rain',
86 | },
87 | '502': {
88 | label: 'heavy intensity rain',
89 | icon: 'rain',
90 | },
91 | '503': {
92 | label: 'very heavy rain',
93 | icon: 'rain',
94 | },
95 | '504': {
96 | label: 'extreme rain',
97 | icon: 'rain',
98 | },
99 | '511': {
100 | label: 'freezing rain',
101 | icon: 'rain-mix',
102 | },
103 | '520': {
104 | label: 'light intensity shower rain',
105 | icon: 'showers',
106 | },
107 | '521': {
108 | label: 'shower rain',
109 | icon: 'showers',
110 | },
111 | '522': {
112 | label: 'heavy intensity shower rain',
113 | icon: 'showers',
114 | },
115 | '531': {
116 | label: 'ragged shower rain',
117 | icon: 'storm-showers',
118 | },
119 | '600': {
120 | label: 'light snow',
121 | icon: 'snow',
122 | },
123 | '601': {
124 | label: 'snow',
125 | icon: 'sleet',
126 | },
127 | '602': {
128 | label: 'heavy snow',
129 | icon: 'snow',
130 | },
131 | '611': {
132 | label: 'sleet',
133 | icon: 'rain-mix',
134 | },
135 | '612': {
136 | label: 'shower sleet',
137 | icon: 'rain-mix',
138 | },
139 | '615': {
140 | label: 'light rain and snow',
141 | icon: 'rain-mix',
142 | },
143 | '616': {
144 | label: 'rain and snow',
145 | icon: 'rain-mix',
146 | },
147 | '620': {
148 | label: 'light shower snow',
149 | icon: 'rain-mix',
150 | },
151 | '621': {
152 | label: 'shower snow',
153 | icon: 'snow',
154 | },
155 | '622': {
156 | label: 'heavy shower snow',
157 | icon: 'snow',
158 | },
159 | '701': {
160 | label: 'mist',
161 | icon: 'showers',
162 | },
163 | '711': {
164 | label: 'smoke',
165 | icon: 'smoke',
166 | },
167 | '721': {
168 | label: 'haze',
169 | icon: 'day-haze',
170 | },
171 | '731': {
172 | label: 'sand, dust whirls',
173 | icon: 'dust',
174 | },
175 | '741': {
176 | label: 'fog',
177 | icon: 'day-fog',
178 | },
179 | '751': {
180 | label: 'sand',
181 | icon: 'cloudy-gusts',
182 | },
183 | '761': {
184 | label: 'dust',
185 | icon: 'dust',
186 | },
187 | '762': {
188 | label: 'volcanic ash',
189 | icon: 'dust',
190 | },
191 | '771': {
192 | label: 'squalls',
193 | icon: 'cloudy-gusts',
194 | },
195 | '781': {
196 | label: 'tornado',
197 | icon: 'tornado',
198 | },
199 | '800': {
200 | label: 'clear sky',
201 | icon: 'day-sunny',
202 | },
203 | '801': {
204 | label: 'few clouds',
205 | icon: 'day-cloudy-gusts',
206 | },
207 | '802': {
208 | label: 'scattered clouds',
209 | icon: 'day-cloudy-gusts',
210 | },
211 | '803': {
212 | label: 'broken clouds',
213 | icon: 'day-cloudy-gusts',
214 | },
215 | '804': {
216 | label: 'overcast clouds',
217 | icon: 'day-sunny-overcast',
218 | },
219 | '900': {
220 | label: 'tornado',
221 | icon: 'tornado',
222 | },
223 | '901': {
224 | label: 'tropical storm',
225 | icon: 'storm-showers',
226 | },
227 | '902': {
228 | label: 'hurricane',
229 | icon: 'hurricane',
230 | },
231 | '903': {
232 | label: 'cold',
233 | icon: 'snowflake-cold',
234 | },
235 | '904': {
236 | label: 'hot',
237 | icon: 'hot',
238 | },
239 | '905': {
240 | label: 'windy',
241 | icon: 'windy',
242 | },
243 | '906': {
244 | label: 'hail',
245 | icon: 'hail',
246 | },
247 | '951': {
248 | label: 'calm',
249 | icon: 'sunny',
250 | },
251 | '952': {
252 | label: 'light breeze',
253 | icon: 'cloudy-gusts',
254 | },
255 | '953': {
256 | label: 'gentle breeze',
257 | icon: 'cloudy-gusts',
258 | },
259 | '954': {
260 | label: 'moderate breeze',
261 | icon: 'cloudy-gusts',
262 | },
263 | '955': {
264 | label: 'fresh breeze',
265 | icon: 'cloudy-gusts',
266 | },
267 | '956': {
268 | label: 'strong breeze',
269 | icon: 'cloudy-gusts',
270 | },
271 | '957': {
272 | label: 'high wind, near gale',
273 | icon: 'strong-wind',
274 | },
275 | '958': {
276 | label: 'gale',
277 | icon: 'cloudy-gusts',
278 | },
279 | '959': {
280 | label: 'severe gale',
281 | icon: 'cloudy-gusts',
282 | },
283 | '960': {
284 | label: 'storm',
285 | icon: 'thunderstorm',
286 | },
287 | '961': {
288 | label: 'violent storm',
289 | icon: 'thunderstorm',
290 | },
291 | '962': {
292 | label: 'hurricane',
293 | icon: 'cloudy-gusts',
294 | },
295 | },
296 | night: {
297 | '200': {
298 | label: 'thunderstorm with light rain',
299 | icon: 'night-alt-thunderstorm',
300 | },
301 | '201': {
302 | label: 'thunderstorm with rain',
303 | icon: 'night-alt-thunderstorm',
304 | },
305 | '202': {
306 | label: 'thunderstorm with heavy rain',
307 | icon: 'night-alt-thunderstorm',
308 | },
309 | '210': {
310 | label: 'light thunderstorm',
311 | icon: 'night-alt-lightning',
312 | },
313 | '211': {
314 | label: 'thunderstorm',
315 | icon: 'night-alt-lightning',
316 | },
317 | '212': {
318 | label: 'heavy thunderstorm',
319 | icon: 'night-alt-lightning',
320 | },
321 | '221': {
322 | label: 'ragged thunderstorm',
323 | icon: 'night-alt-lightning',
324 | },
325 | '230': {
326 | label: 'thunderstorm with light drizzle',
327 | icon: 'night-alt-thunderstorm',
328 | },
329 | '231': {
330 | label: 'thunderstorm with drizzle',
331 | icon: 'night-alt-thunderstorm',
332 | },
333 | '232': {
334 | label: 'thunderstorm with haeavy drizzle',
335 | icon: 'night-alt-thunderstorm',
336 | },
337 | '300': {
338 | label: 'light intensity drizzle',
339 | icon: 'night-alt-sprinkle',
340 | },
341 | '301': {
342 | label: 'drizzle',
343 | icon: 'night-alt-sprinkle',
344 | },
345 | '302': {
346 | label: 'heavy intensity drizzle',
347 | icon: 'night-alt-rain',
348 | },
349 | '310': {
350 | label: 'light intensity drizzle',
351 | icon: 'night-alt-rain',
352 | },
353 | '311': {
354 | label: 'drizzle rain',
355 | icon: 'night-alt-rain',
356 | },
357 | '312': {
358 | label: 'heavy intensity drizzle rain',
359 | icon: 'night-alt-rain',
360 | },
361 | '313': {
362 | label: 'shower rain and drizzle',
363 | icon: 'night-alt-rain',
364 | },
365 | '314': {
366 | label: 'heavy shower rain and drizzle',
367 | icon: 'night-alt-rain',
368 | },
369 | '321': {
370 | label: 'shower drizzle',
371 | icon: 'night-alt-sprinkle',
372 | },
373 | '500': {
374 | label: 'light rain',
375 | icon: 'night-alt-sprinkle',
376 | },
377 | '501': {
378 | label: 'moderate rain',
379 | icon: 'night-alt-rain',
380 | },
381 | '502': {
382 | label: 'heavy intensity rain',
383 | icon: 'night-alt-rain',
384 | },
385 | '503': {
386 | label: 'very heavy rain',
387 | icon: 'night-alt-rain',
388 | },
389 | '504': {
390 | label: 'extreme rain',
391 | icon: 'night-alt-rain',
392 | },
393 | '511': {
394 | label: 'freezing rain',
395 | icon: 'night-alt-rain-mix',
396 | },
397 | '520': {
398 | label: 'light intensity shower rain',
399 | icon: 'night-alt-showers',
400 | },
401 | '521': {
402 | label: 'shower rain',
403 | icon: 'night-alt-showers',
404 | },
405 | '522': {
406 | label: 'heavy intensity shower rain',
407 | icon: 'night-alt-showers',
408 | },
409 | '531': {
410 | label: 'ragged shower rain',
411 | icon: 'night-alt-storm-showers',
412 | },
413 | '600': {
414 | label: 'light snow',
415 | icon: 'night-alt-snow',
416 | },
417 | '601': {
418 | label: 'snow',
419 | icon: 'night-alt-sleet',
420 | },
421 | '602': {
422 | label: 'heavy snow',
423 | icon: 'night-alt-snow',
424 | },
425 | '611': {
426 | label: 'sleet',
427 | icon: 'night-alt-rain-mix',
428 | },
429 | '612': {
430 | label: 'light shower sleet',
431 | icon: 'night-alt-rain-mix',
432 | },
433 | '615': {
434 | label: 'light rain and snow',
435 | icon: 'night-alt-rain-mix',
436 | },
437 | '616': {
438 | label: 'rain and snow',
439 | icon: 'night-alt-rain-mix',
440 | },
441 | '620': {
442 | label: 'light shower snow',
443 | icon: 'night-alt-rain-mix',
444 | },
445 | '621': {
446 | label: 'shower snow',
447 | icon: 'night-alt-snow',
448 | },
449 | '622': {
450 | label: 'heavy shower snow',
451 | icon: 'night-alt-snow',
452 | },
453 | '701': {
454 | label: 'mist',
455 | icon: 'night-alt-showers',
456 | },
457 | '711': {
458 | label: 'smoke',
459 | icon: 'smoke',
460 | },
461 | '721': {
462 | label: 'haze',
463 | icon: 'day-haze',
464 | },
465 | '731': {
466 | label: 'sand/dust whirls',
467 | icon: 'dust',
468 | },
469 | '741': {
470 | label: 'fog',
471 | icon: 'night-fog',
472 | },
473 | '761': {
474 | label: 'sand',
475 | icon: 'dust',
476 | },
477 | '762': {
478 | label: 'dust',
479 | icon: 'dust',
480 | },
481 | '781': {
482 | label: 'volcanic ash',
483 | icon: 'tornado',
484 | },
485 | '800': {
486 | label: 'clear sky',
487 | icon: 'night-clear',
488 | },
489 | '801': {
490 | label: 'cloudy gusts',
491 | icon: 'night-alt-cloudy-gusts',
492 | },
493 | '802': {
494 | label: 'cloudy gusts',
495 | icon: 'night-alt-cloudy-gusts',
496 | },
497 | '803': {
498 | label: 'cloudy gusts',
499 | icon: 'night-alt-cloudy-gusts',
500 | },
501 | '804': {
502 | label: 'cloudy',
503 | icon: 'night-alt-cloudy',
504 | },
505 | '900': {
506 | label: 'tornado',
507 | icon: 'tornado',
508 | },
509 | '902': {
510 | label: 'hurricane',
511 | icon: 'hurricane',
512 | },
513 | '903': {
514 | label: 'snowflake-cold',
515 | icon: 'snowflake-cold',
516 | },
517 | '904': {
518 | label: 'hot',
519 | icon: 'hot',
520 | },
521 | '906': {
522 | label: 'hail',
523 | icon: 'night-alt-hail',
524 | },
525 | '957': {
526 | label: 'strong wind',
527 | icon: 'strong-wind',
528 | },
529 | },
530 | };
531 |
532 | export default weatherIcons;
533 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import { SWRConfig, preload } from 'swr';
4 | import { createToast, destroyAllToasts } from 'vercel-toast';
5 | import 'vercel-toast/dist/vercel-toast.css';
6 | import { ThemeProvider } from './components/theme-context';
7 | import fetcher from './lib/fetcher';
8 | import App from './components/app';
9 | import './index.css';
10 |
11 | const container = document.getElementById('root');
12 | const root = createRoot(container);
13 | const apiKey = import.meta.env.VITE_API_KEY;
14 | const apiUrl = import.meta.env.VITE_API_URL;
15 | const apiEndpoint = `?q=Eldoret&units=metric&APPID=${apiKey}`;
16 |
17 | preload(`${apiUrl}/weather/${apiEndpoint}`, fetcher);
18 | preload(`${apiUrl}/forecast/${apiEndpoint}`, fetcher);
19 |
20 | console.log('preloading');
21 |
22 | root.render(
23 | {
26 | if (error) {
27 | createToast(`Error: ${error.message}`, {
28 | type: 'error',
29 | });
30 | }
31 | },
32 | onSuccess: (data) => {
33 | if (data) {
34 | destroyAllToasts();
35 | }
36 | },
37 | shouldRetryOnError: false,
38 | }}
39 | >
40 |
41 |
42 |
43 | ,
44 | );
45 |
--------------------------------------------------------------------------------
/src/lib/fetcher.js:
--------------------------------------------------------------------------------
1 | export default async function Fetcher(url) {
2 | const res = await fetch(url);
3 |
4 | if (!res.ok) {
5 | const error = handleError(res.status);
6 | throw error;
7 | }
8 |
9 | return res.json();
10 | }
11 |
12 | function handleError(errorCode) {
13 | let error;
14 | switch (errorCode) {
15 | case 401:
16 | error = `It looks like the API did not authorize your request. Please ensure you have a valid API key.`;
17 | break;
18 | case 404:
19 | error = `No results found. Check your query again or try searching for a different location.`;
20 | break;
21 | case 429:
22 | error = `It looks like you've made too many requests to the server. Please wait a while before trying again.`;
23 | break;
24 | default:
25 | error = `Server error`;
26 | break;
27 | }
28 | return new Error(error);
29 | }
30 |
--------------------------------------------------------------------------------
/src/recommendations.js:
--------------------------------------------------------------------------------
1 | const recommendations = {
2 | day: {
3 | '200': {
4 | recommendation: 'Stormy with a bit of rain. Coat required',
5 | },
6 | '201': {
7 | recommendation: 'Stormy with showers. Grab a brolly on your way out',
8 | },
9 | '202': {
10 | recommendation:
11 | "It's going to be absolutely torrential out there today. Carry as many umbrellas as you can",
12 | },
13 | '210': {
14 | recommendation:
15 | "Light storm on the way. Good time for that spare lightning detector you've had lying around",
16 | },
17 | '211': {
18 | recommendation: 'A storm on the way. Mind your eardrums please.',
19 | },
20 | '212': {
21 | recommendation:
22 | 'Biblical weather today. Gumboots and a life jacket are the bare minimum',
23 | },
24 | '221': {
25 | recommendation:
26 | 'Horrid weather. Jackets, brollies, heck, you might even need a boat',
27 | },
28 | '230': {
29 | recommendation: "Storms and a bit of rain. Brolly + layers I'm afraid",
30 | },
31 | '231': {
32 | recommendation:
33 | "Rain + storms aka you'll look pretty daft going without a jacket today",
34 | },
35 | '232': {
36 | recommendation: 'Rainfall incoming. Umbrella for goodness sake!',
37 | },
38 | '300': {
39 | recommendation:
40 | 'Well, a bit of rain never killed anyone. Am I even legally allowed to say er... *frantically deletes*',
41 | },
42 | '301': {
43 | recommendation:
44 | 'Rain, but not terminal. Be sensible and wear a coat for heavens sake',
45 | },
46 | '302': {
47 | recommendation: "Rain, of the annoying variety. Don't get caught out",
48 | },
49 | '310': {
50 | recommendation:
51 | "The kind of rain you see people dance in in the movies. You'll be fine",
52 | },
53 | '311': {
54 | recommendation: 'A spot of rain. Nothing to be overly worried about',
55 | },
56 | '312': {
57 | recommendation: "More rain, I'm afraid. Coat or jacket as standard",
58 | },
59 | '313': {
60 | recommendation:
61 | "Showers forecasted. That means a free shower for you if you don't carry a brolly",
62 | },
63 | '314': {
64 | recommendation:
65 | 'Cats and dogs. Umbrella and a thick pair of wellies required.',
66 | },
67 | '321': {
68 | recommendation:
69 | 'Rain, but the sort you could survive by staying indoors. Umbrella optional',
70 | },
71 | '500': {
72 | recommendation:
73 | 'You can get away with just a sweater today. Disclaimer: I will not be held responsible for any rain-related mishaps',
74 | },
75 | '501': {
76 | recommendation: 'A bit more rain. Best carry an umbrella chap',
77 | },
78 | '502': {
79 | recommendation:
80 | 'Now this is getting annoying. Umbrella + jacket required',
81 | },
82 | '503': {
83 | recommendation:
84 | 'Biblical amounts of rain forecasted. Umbrella, jacket, coat, layers, life jacket, and standby boat as standard',
85 | },
86 | '504': {
87 | recommendation:
88 | "I'm delighted to tell you that you're not leaving the house today because you'd literally drown in the rain outside",
89 | },
90 | '511': {
91 | recommendation:
92 | "Rain, biting cold, soul-renching conditions, the works. You'd be well advised to seek cover and remain indoors",
93 | },
94 | '520': {
95 | recommendation:
96 | 'Showers forecasted. Carry an umbrella for goodness sake!',
97 | },
98 | '521': {
99 | recommendation:
100 | 'Showers forecasted. Please carry an umbrella for goodness sake',
101 | },
102 | '522': {
103 | recommendation:
104 | "You're getting absolutely drenched if you dare go without a military-grade jacket and umbrella combo today",
105 | },
106 | '531': {
107 | recommendation:
108 | "Ragged rains. You're going to need a jacket, wellies and industrial-strength fortitude to make it through this",
109 | },
110 | '600': {
111 | recommendation: "Light snow forecasted. You'll be fine",
112 | },
113 | '601': {
114 | recommendation:
115 | "Snow. But you'll live. Just carry a sweater for good measure",
116 | },
117 | '602': {
118 | recommendation:
119 | "It's blustery out there. Consider calling in sick at the office",
120 | },
121 | '611': {
122 | recommendation: 'Umbrella and coat as standard',
123 | },
124 | '612': {
125 | recommendation: 'Umbrella and coat as standard',
126 | },
127 | '613': {
128 | recommendation: 'Umbrella and coat as standard',
129 | },
130 | '615': {
131 | recommendation:
132 | "Umbrella and coat as standard. Don't say I didn't warn you",
133 | },
134 | '616': {
135 | recommendation:
136 | 'Umbrella + coat + a nice cup of a warm beverage recommended',
137 | },
138 | '620': {
139 | recommendation: 'A light jacket should do the trick',
140 | },
141 | '621': {
142 | recommendation:
143 | 'Rain + snow. Could be better. Could be worse. Jacket + brolly recommended',
144 | },
145 | '622': {
146 | recommendation:
147 | "It's an absolute warzone out there. Coat, umbrella, jacket and military-grade helmet required",
148 | },
149 | '701': {
150 | recommendation:
151 | "The air is thick with fog and there's just barely a light rain. Tread carefully",
152 | },
153 | '702': {
154 | recommendation: 'Do you smell burning? I smell burning...',
155 | },
156 | '711': {
157 | recommendation:
158 | 'Smoke warning - potential forest fire hazard. Be extremely vigilant and on the lookout for the most up to date weather advisories',
159 | },
160 | '721': {
161 | recommendation:
162 | "The sun's out but it's hazy. I've got work, but I'm lazy. Like a mini Eminem, right?",
163 | },
164 | '731': {
165 | recommendation:
166 | 'Like walking out in the Sahara is what it looks like out there. Tech wear the absolute bare minimum',
167 | },
168 | '741': {
169 | recommendation:
170 | 'Foggy weather. Visibility severely impacted. Take extreme care when driving if you must.',
171 | },
172 | '751': {
173 | recommendation:
174 | 'Potential sandstorms expected. Avoid the outdoors if you can',
175 | },
176 | '761': {
177 | recommendation:
178 | 'Potential dust storms expected. Take cover and remain safe',
179 | },
180 | '762': {
181 | recommendation: 'Volcanic ash advisory - stay indoors whenever possible',
182 | },
183 | '771': {
184 | recommendation: 'Snow squall warning - avoid the outdoors if you can',
185 | },
186 | '781': {
187 | recommendation:
188 | 'Tornado warning - take immediate cover at the first sign. Avoid windows.',
189 | },
190 | '800': {
191 | recommendation:
192 | "It's a beautiful day. Lather up on some sunscreen and maybe carry a hat too for good measure.",
193 | },
194 | '801': {
195 | recommendation:
196 | 'Great day for a hanging out the laundry and maybe a nice picnic date later :)',
197 | },
198 | '802': {
199 | recommendation:
200 | 'Love is in the air. Call that special someone up for a coffee',
201 | },
202 | '803': {
203 | recommendation: "'Netflix and chill' weather. It's pleasant outside",
204 | },
205 | '804': {
206 | recommendation: 'Great day for a run. Pack that gym bag',
207 | },
208 | },
209 | night: {
210 | '200': {
211 | recommendation: 'Stormy with a bit of rain. Coat required',
212 | },
213 | '201': {
214 | recommendation: 'Stormy with showers. Grab a brolly on your way out',
215 | },
216 | '202': {
217 | recommendation:
218 | "It's going to be absolutely torrential out there. Carry as many umbrellas as you can",
219 | },
220 | '210': {
221 | recommendation:
222 | "Light storm on the way. Good time for that spare lightning detector you've had lying around",
223 | },
224 | '211': {
225 | recommendation: 'A storm on the way. Mind your eardrums please.',
226 | },
227 | '212': {
228 | recommendation:
229 | 'Biblical weather today. Gumboots and a life jacket are the bare minimum',
230 | },
231 | '221': {
232 | recommendation:
233 | 'Horrid weather. Jackets, brollies, heck, you might even need a boat',
234 | },
235 | '230': {
236 | recommendation: "Storms and a bit of rain. Brolly + layers I'm afraid",
237 | },
238 | '231': {
239 | recommendation:
240 | "Rain + storms aka you'll look pretty daft going without a jacket today",
241 | },
242 | '232': {
243 | recommendation: 'Rainfall incoming. Umbrella for goodness sake!',
244 | },
245 | '300': {
246 | recommendation:
247 | 'Well, a bit of rain never killed anyone. Am I even legally allowed to say er... *frantically deletes*',
248 | },
249 | '301': {
250 | recommendation:
251 | 'Rain, but not terminal. Be sensible and wear a coat for heavens sake',
252 | },
253 | '302': {
254 | recommendation: "Rain, of the annoying variety. Don't get caught out",
255 | },
256 | '310': {
257 | recommendation:
258 | "The kind of rain you see people dance in in the movies. You'll be fine",
259 | },
260 | '311': {
261 | recommendation: 'A spot of rain. Nothing to be overly worried about',
262 | },
263 | '312': {
264 | recommendation: "More rain, I'm afraid. Coat or jacket as standard",
265 | },
266 | '313': {
267 | recommendation:
268 | "Showers forecasted. That means a free shower for you if you don't carry a brolly",
269 | },
270 | '314': {
271 | recommendation:
272 | 'Cats and dogs. Umbrella and a thick pair of wellies required.',
273 | },
274 | '321': {
275 | recommendation:
276 | 'Rain, but the sort you could survive by staying indoors. Umbrella optional',
277 | },
278 | '500': {
279 | recommendation:
280 | 'You can get away with just a sweater today. Disclaimer: I will not be held responsible for any rain-related mishaps',
281 | },
282 | '501': {
283 | recommendation: 'A bit more rain. Best carry an umbrella chap',
284 | },
285 | '502': {
286 | recommendation:
287 | 'Now this is getting annoying. Umbrella + jacket required',
288 | },
289 | '503': {
290 | recommendation:
291 | 'Biblical amounts of rain forecasted. Umbrella, jacket, coat, layers, life jacket, and standby boat as standard',
292 | },
293 | '504': {
294 | recommendation:
295 | "I'm delighted to tell you that you're not leaving the house today because you'd literally drown in the rain outside",
296 | },
297 | '511': {
298 | recommendation:
299 | "Rain, biting cold, soul-renching conditions, the works. You'd be well advised to seek cover and remain indoors",
300 | },
301 | '520': {
302 | recommendation:
303 | 'Showers forecasted. Carry an umbrella for goodness sake!',
304 | },
305 | '521': {
306 | recommendation:
307 | 'Showers forecasted. Please carry an umbrella for goodness sake',
308 | },
309 | '522': {
310 | recommendation:
311 | "You're getting absolutely drenched if you dare go without a military-grade jacket and umbrella combo today",
312 | },
313 | '531': {
314 | recommendation:
315 | "Ragged rains. You're going to need a jacket, wellies and industrial-strength fortitude to make it through this",
316 | },
317 | '600': {
318 | recommendation: "Light snow forecasted. You'll be fine",
319 | },
320 | '601': {
321 | recommendation:
322 | "Snow. But you'll live. Just carry a sweater for good measure",
323 | },
324 | '602': {
325 | recommendation:
326 | "It's blustery out there. Consider calling in sick at the office",
327 | },
328 | '611': {
329 | recommendation: 'Umbrella and coat as standard',
330 | },
331 | '615': {
332 | recommendation:
333 | "Umbrella and coat as standard. Don't say I didn't warn you",
334 | },
335 | '616': {
336 | recommendation:
337 | 'Umbrella + coat + a nice cup of a warm beverage recommended',
338 | },
339 | '620': {
340 | recommendation: 'A light jacket should do the trick',
341 | },
342 | '621': {
343 | recommendation:
344 | 'Rain + snow. Could be better. Could be worse. Jacket + brolly recommended',
345 | },
346 | '622': {
347 | recommendation:
348 | "It's an absolute warzone out there. Coat, umbrella, jacket and military-grade helmet required",
349 | },
350 | '701': {
351 | recommendation:
352 | "The air is thick with fog and there's just barely a light rain. Tread carefully",
353 | },
354 | '702': {
355 | recommendation: 'Do you smell burning? I smell burning...',
356 | },
357 | '711': {
358 | recommendation:
359 | 'Smoke warning - potential forest fire hazard. Be extremely vigilant and on the lookout for the most up to date weather advisories',
360 | },
361 | '721': {
362 | recommendation:
363 | "Silent night, hazy night... you'll need your standard-issue mil-spec night vision goggles",
364 | },
365 | '731': {
366 | recommendation:
367 | 'Like walking out in the Sahara is what it looks like out there. Tech wear the absolute bare minimum',
368 | },
369 | '741': {
370 | recommendation:
371 | 'Foggy weather. Visibility severely impacted. Take extreme care when driving if you must.',
372 | },
373 | '751': {
374 | recommendation:
375 | 'Potential sandstorms expected. Avoid the outdoors if you can',
376 | },
377 | '761': {
378 | recommendation:
379 | 'Potential dust storms expected. Take cover and remain safe',
380 | },
381 | '762': {
382 | recommendation: 'Volcanic ash advisory - stay indoors whenever possible',
383 | },
384 | '771': {
385 | recommendation: 'Snow squall warning - avoid the outdoors if you can',
386 | },
387 | '781': {
388 | recommendation:
389 | 'Tornado warning - take immediate cover at the first sign. Avoid windows.',
390 | },
391 | '800': {
392 | recommendation:
393 | 'Clear skies. Kind of night where you fire up stellarium and cozy up underneath the telescope',
394 | },
395 | '801': {
396 | recommendation:
397 | 'Cloudy skies on a blustery evening. Snuggle up with a hot cuppa',
398 | },
399 | '802': {
400 | recommendation: "Cloudy gusts forecasted. You'll wanna get a coat",
401 | },
402 | '803': {
403 | recommendation: 'Cloudy and blustery outside. Coat required',
404 | },
405 | '804': {
406 | recommendation: 'Cloudy with a chance of meatballs',
407 | },
408 | '900': {
409 | recommendation:
410 | '⚠️ Tornado warning ⚠️ Seek safe shelter immediately. stay away from doors, windows, outside walls and protect your head!',
411 | },
412 | '902': {
413 | recommendation:
414 | '⚠️ Hurricane warning ⚠️ Stay indoors and away from windows. Be on the lookout for the latest emergency response information',
415 | },
416 | '903': {
417 | recommendation:
418 | 'Snowflakes falling with a chilly wind blowing. Christmas already?',
419 | },
420 | '904': {
421 | recommendation:
422 | 'Absolutely blazing outside. Ice baths on rotation is the bare minimum',
423 | },
424 | '906': {
425 | recommendation: 'Hailstorm forecasted. Take cover',
426 | },
427 | '957': {
428 | recommendation: 'Strong winds. Hold on to something or someone strong',
429 | },
430 | },
431 | };
432 |
433 | export default recommendations;
434 |
--------------------------------------------------------------------------------
/src/test/app-test-utils.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { SWRConfig } from 'swr';
3 | import {
4 | act,
5 | render,
6 | screen,
7 | waitForElementToBeRemoved,
8 | } from '@testing-library/react';
9 | import userEvent from '@testing-library/user-event';
10 | import { ThemeProvider } from '../components/theme-context';
11 |
12 | // eslint-disable-next-line react/prop-types
13 | const Wrapper = ({ children }) => {
14 | return (
15 | new Map(), dedupingInterval: 0 }}>
16 | {children}
17 |
18 | );
19 | };
20 |
21 | const customRender = (ui, options) =>
22 | render(ui, { wrapper: Wrapper, ...options });
23 |
24 | const waitForLoadingToFinish = () =>
25 | waitForElementToBeRemoved(() => [...screen.queryAllByRole(/progressbar/i)], {
26 | timeout: 4000,
27 | });
28 |
29 | export * from '@testing-library/react';
30 | export { act, screen, userEvent, waitForLoadingToFinish };
31 | export { customRender as render };
32 |
--------------------------------------------------------------------------------
/src/weather-data.js:
--------------------------------------------------------------------------------
1 | export const mockWeather = {
2 | location: 'Eldoret',
3 | condition: 200,
4 | country: 'KE',
5 | date: 1579073776000,
6 | description: 'few clouds',
7 | feels_like: 17,
8 | humidity: 68,
9 | icon_id: 801,
10 | sunrise: 1610077360000,
11 | sunset: 1610120889000,
12 | temperature: 19,
13 | timezone: 180,
14 | wind_speed: 24,
15 | };
16 |
17 | export const mockForecast = [
18 | {
19 | date: 1579122000000,
20 | humidity: 85,
21 | icon_id: 801,
22 | feels_like: 13,
23 | temperature: 14,
24 | description: 'few clouds',
25 | wind_speed: 7,
26 | dt_txt: '2020-01-15 21:00:00',
27 | icon: '02n',
28 | max: 14,
29 | min: 14,
30 | },
31 | {
32 | date: 1579208400000,
33 | humidity: 77,
34 | icon_id: 802,
35 | feels_like: 13,
36 | temperature: 15,
37 | description: 'scattered clouds',
38 | wind_speed: 6,
39 | dt_txt: '2020-01-16 21:00:00',
40 | icon: '03n',
41 | max: 15,
42 | min: 15,
43 | },
44 | {
45 | date: 1579294800000,
46 | humidity: 53,
47 | icon_id: 803,
48 | feels_like: 15,
49 | temperature: 17,
50 | description: 'broken clouds',
51 | wind_speed: 5,
52 | dt_txt: '2020-01-17 21:00:00',
53 | icon: '04n',
54 | max: 17,
55 | min: 17,
56 | },
57 | {
58 | date: 1579381200000,
59 | humidity: 46,
60 | icon_id: 803,
61 | feels_like: 12,
62 | temperature: 13,
63 | description: 'broken clouds',
64 | wind_speed: 4,
65 | dt_txt: '2020-01-18 21:00:00',
66 | icon: '04n',
67 | max: 13,
68 | min: 13,
69 | },
70 | {
71 | date: 1579467600000,
72 | humidity: 52,
73 | icon_id: 804,
74 | feels_like: 15,
75 | temperature: 16,
76 | description: 'overcast clouds',
77 | wind_speed: 6,
78 | dt_txt: '2020-01-19 21:00:00',
79 | icon: '04n',
80 | max: 16,
81 | min: 16,
82 | },
83 | ];
84 |
85 | export const mockWeatherData = {
86 | base: 'stations',
87 | clouds: {
88 | all: 75,
89 | },
90 | cod: 200,
91 | coord: {
92 | lon: 35.27,
93 | lat: 0.52,
94 | },
95 | dt: 1572517487,
96 | id: 198629,
97 | main: {
98 | temp: 20,
99 | feels_like: 18,
100 | pressure: 1026,
101 | humidity: 49,
102 | temp_min: 20,
103 | temp_max: 20,
104 | },
105 | name: 'Eldoret',
106 | sys: {
107 | country: 'KE',
108 | id: 2541,
109 | sunrise: 1572491974,
110 | sunset: 1572535523,
111 | type: 1,
112 | },
113 | timezone: 10800,
114 | visibility: 10000,
115 | weather: [
116 | {
117 | id: 803,
118 | main: 'Clouds',
119 | description: 'broken clouds',
120 | icon: '04d',
121 | },
122 | ],
123 | wind: {
124 | deg: 130,
125 | speed: 8.2,
126 | },
127 | };
128 |
129 | export const mockForecastData = {
130 | location: {
131 | coord: {
132 | lat: 0.5198,
133 | lon: 35.2715,
134 | },
135 | country: 'KE',
136 | id: 198629,
137 | name: 'Eldoret',
138 | population: 218446,
139 | sunrise: 1572578373,
140 | sunset: 1572621920,
141 | timezone: 10800,
142 | },
143 | cod: '200',
144 | message: 0.0064,
145 | cnt: 40,
146 | list: [
147 | {
148 | dt: 1572609600,
149 | main: {
150 | temp: 24.58,
151 | temp_min: 22.25,
152 | temp_max: 24.58,
153 | pressure: 1010,
154 | sea_level: 1010,
155 | grnd_level: 792,
156 | humidity: 51,
157 | temp_kf: 2.33,
158 | },
159 | weather: [
160 | {
161 | id: 500,
162 | main: 'Rain',
163 | description: 'light rain',
164 | icon: '10d',
165 | },
166 | ],
167 | clouds: {
168 | all: 76,
169 | },
170 | wind: {
171 | speed: 2.47,
172 | deg: 59,
173 | },
174 | rain: {
175 | '3h': 1.06,
176 | },
177 | sys: {
178 | pod: 'd',
179 | },
180 | dt_txt: '2019-11-01 12:00:00',
181 | },
182 | {
183 | dt: 1572620400,
184 | main: {
185 | temp: 20.07,
186 | temp_min: 18.32,
187 | temp_max: 20.07,
188 | pressure: 1011,
189 | sea_level: 1011,
190 | grnd_level: 792,
191 | humidity: 67,
192 | temp_kf: 1.75,
193 | },
194 | weather: [
195 | {
196 | id: 500,
197 | main: 'Rain',
198 | description: 'light rain',
199 | icon: '10d',
200 | },
201 | ],
202 | clouds: {
203 | all: 100,
204 | },
205 | wind: {
206 | speed: 0.95,
207 | deg: 52,
208 | },
209 | rain: {
210 | '3h': 0.94,
211 | },
212 | sys: {
213 | pod: 'd',
214 | },
215 | dt_txt: '2019-11-01 15:00:00',
216 | },
217 | {
218 | dt: 1572631200,
219 | main: {
220 | temp: 16.81,
221 | temp_min: 15.65,
222 | temp_max: 16.81,
223 | pressure: 1013,
224 | sea_level: 1013,
225 | grnd_level: 794,
226 | humidity: 82,
227 | temp_kf: 1.16,
228 | },
229 | weather: [
230 | {
231 | id: 500,
232 | main: 'Rain',
233 | description: 'light rain',
234 | icon: '10n',
235 | },
236 | ],
237 | clouds: {
238 | all: 99,
239 | },
240 | wind: {
241 | speed: 1.67,
242 | deg: 141,
243 | },
244 | rain: {
245 | '3h': 0.37,
246 | },
247 | sys: {
248 | pod: 'n',
249 | },
250 | dt_txt: '2019-11-01 18:00:00',
251 | },
252 | {
253 | dt: 1572642000,
254 | main: {
255 | temp: 15,
256 | temp_min: 14.76,
257 | temp_max: 15,
258 | pressure: 1014,
259 | sea_level: 1014,
260 | grnd_level: 794,
261 | humidity: 87,
262 | temp_kf: 0.58,
263 | },
264 | weather: [
265 | {
266 | id: 500,
267 | main: 'Rain',
268 | description: 'light rain',
269 | icon: '10n',
270 | },
271 | ],
272 | clouds: {
273 | all: 93,
274 | },
275 | wind: {
276 | speed: 0.5,
277 | deg: 139,
278 | },
279 | rain: {
280 | '3h': 0.94,
281 | },
282 | sys: {
283 | pod: 'n',
284 | },
285 | dt_txt: '2019-11-01 21:00:00',
286 | },
287 | {
288 | dt: 1572652800,
289 | main: {
290 | temp: 13.64,
291 | temp_min: 13.64,
292 | temp_max: 13.64,
293 | pressure: 1013,
294 | sea_level: 1013,
295 | grnd_level: 793,
296 | humidity: 87,
297 | temp_kf: 0,
298 | },
299 | weather: [
300 | {
301 | id: 500,
302 | main: 'Rain',
303 | description: 'light rain',
304 | icon: '10n',
305 | },
306 | ],
307 | clouds: {
308 | all: 78,
309 | },
310 | wind: {
311 | speed: 0.62,
312 | deg: 173,
313 | },
314 | rain: {
315 | '3h': 0.12,
316 | },
317 | sys: {
318 | pod: 'n',
319 | },
320 | dt_txt: '2019-11-02 00:00:00',
321 | },
322 | {
323 | dt: 1572663600,
324 | main: {
325 | temp: 12.53,
326 | temp_min: 12.53,
327 | temp_max: 12.53,
328 | pressure: 1013,
329 | sea_level: 1013,
330 | grnd_level: 793,
331 | humidity: 92,
332 | temp_kf: 0,
333 | },
334 | weather: [
335 | {
336 | id: 800,
337 | main: 'Clear',
338 | description: 'clear sky',
339 | icon: '01n',
340 | },
341 | ],
342 | clouds: {
343 | all: 0,
344 | },
345 | wind: {
346 | speed: 0.12,
347 | deg: 115,
348 | },
349 | sys: {
350 | pod: 'n',
351 | },
352 | dt_txt: '2019-11-02 03:00:00',
353 | },
354 | {
355 | dt: 1572674400,
356 | main: {
357 | temp: 18.33,
358 | temp_min: 18.33,
359 | temp_max: 18.33,
360 | pressure: 1014,
361 | sea_level: 1014,
362 | grnd_level: 795,
363 | humidity: 71,
364 | temp_kf: 0,
365 | },
366 | weather: [
367 | {
368 | id: 800,
369 | main: 'Clear',
370 | description: 'clear sky',
371 | icon: '01d',
372 | },
373 | ],
374 | clouds: {
375 | all: 0,
376 | },
377 | wind: {
378 | speed: 2.33,
379 | deg: 78,
380 | },
381 | sys: {
382 | pod: 'd',
383 | },
384 | dt_txt: '2019-11-02 06:00:00',
385 | },
386 | {
387 | dt: 1572685200,
388 | main: {
389 | temp: 22.38,
390 | temp_min: 22.38,
391 | temp_max: 22.38,
392 | pressure: 1011,
393 | sea_level: 1011,
394 | grnd_level: 794,
395 | humidity: 52,
396 | temp_kf: 0,
397 | },
398 | weather: [
399 | {
400 | id: 800,
401 | main: 'Clear',
402 | description: 'clear sky',
403 | icon: '01d',
404 | },
405 | ],
406 | clouds: {
407 | all: 0,
408 | },
409 | wind: {
410 | speed: 3.2,
411 | deg: 64,
412 | },
413 | sys: {
414 | pod: 'd',
415 | },
416 | dt_txt: '2019-11-02 09:00:00',
417 | },
418 | {
419 | dt: 1572696000,
420 | main: {
421 | temp: 22.26,
422 | temp_min: 22.26,
423 | temp_max: 22.26,
424 | pressure: 1009,
425 | sea_level: 1009,
426 | grnd_level: 792,
427 | humidity: 53,
428 | temp_kf: 0,
429 | },
430 | weather: [
431 | {
432 | id: 500,
433 | main: 'Rain',
434 | description: 'light rain',
435 | icon: '10d',
436 | },
437 | ],
438 | clouds: {
439 | all: 15,
440 | },
441 | wind: {
442 | speed: 2.29,
443 | deg: 58,
444 | },
445 | rain: {
446 | '3h': 0.94,
447 | },
448 | sys: {
449 | pod: 'd',
450 | },
451 | dt_txt: '2019-11-02 12:00:00',
452 | },
453 | {
454 | dt: 1572706800,
455 | main: {
456 | temp: 18.77,
457 | temp_min: 18.77,
458 | temp_max: 18.77,
459 | pressure: 1010,
460 | sea_level: 1010,
461 | grnd_level: 792,
462 | humidity: 67,
463 | temp_kf: 0,
464 | },
465 | weather: [
466 | {
467 | id: 500,
468 | main: 'Rain',
469 | description: 'light rain',
470 | icon: '10d',
471 | },
472 | ],
473 | clouds: {
474 | all: 69,
475 | },
476 | wind: {
477 | speed: 2.93,
478 | deg: 42,
479 | },
480 | rain: {
481 | '3h': 1,
482 | },
483 | sys: {
484 | pod: 'd',
485 | },
486 | dt_txt: '2019-11-02 15:00:00',
487 | },
488 | {
489 | dt: 1572717600,
490 | main: {
491 | temp: 15.72,
492 | temp_min: 15.72,
493 | temp_max: 15.72,
494 | pressure: 1014,
495 | sea_level: 1014,
496 | grnd_level: 794,
497 | humidity: 85,
498 | temp_kf: 0,
499 | },
500 | weather: [
501 | {
502 | id: 500,
503 | main: 'Rain',
504 | description: 'light rain',
505 | icon: '10n',
506 | },
507 | ],
508 | clouds: {
509 | all: 80,
510 | },
511 | wind: {
512 | speed: 1.92,
513 | deg: 72,
514 | },
515 | rain: {
516 | '3h': 0.94,
517 | },
518 | sys: {
519 | pod: 'n',
520 | },
521 | dt_txt: '2019-11-02 18:00:00',
522 | },
523 | {
524 | dt: 1572728400,
525 | main: {
526 | temp: 14.35,
527 | temp_min: 14.35,
528 | temp_max: 14.35,
529 | pressure: 1014,
530 | sea_level: 1014,
531 | grnd_level: 794,
532 | humidity: 95,
533 | temp_kf: 0,
534 | },
535 | weather: [
536 | {
537 | id: 500,
538 | main: 'Rain',
539 | description: 'light rain',
540 | icon: '10n',
541 | },
542 | ],
543 | clouds: {
544 | all: 100,
545 | },
546 | wind: {
547 | speed: 0.96,
548 | deg: 128,
549 | },
550 | rain: {
551 | '3h': 1.69,
552 | },
553 | sys: {
554 | pod: 'n',
555 | },
556 | dt_txt: '2019-11-02 21:00:00',
557 | },
558 | {
559 | dt: 1572739200,
560 | main: {
561 | temp: 13.41,
562 | temp_min: 13.41,
563 | temp_max: 13.41,
564 | pressure: 1013,
565 | sea_level: 1013,
566 | grnd_level: 793,
567 | humidity: 92,
568 | temp_kf: 0,
569 | },
570 | weather: [
571 | {
572 | id: 500,
573 | main: 'Rain',
574 | description: 'light rain',
575 | icon: '10n',
576 | },
577 | ],
578 | clouds: {
579 | all: 84,
580 | },
581 | wind: {
582 | speed: 1.44,
583 | deg: 117,
584 | },
585 | rain: {
586 | '3h': 1.44,
587 | },
588 | sys: {
589 | pod: 'n',
590 | },
591 | dt_txt: '2019-11-03 00:00:00',
592 | },
593 | {
594 | dt: 1572750000,
595 | main: {
596 | temp: 12.71,
597 | temp_min: 12.71,
598 | temp_max: 12.71,
599 | pressure: 1014,
600 | sea_level: 1014,
601 | grnd_level: 794,
602 | humidity: 94,
603 | temp_kf: 0,
604 | },
605 | weather: [
606 | {
607 | id: 802,
608 | main: 'Clouds',
609 | description: 'scattered clouds',
610 | icon: '03n',
611 | },
612 | ],
613 | clouds: {
614 | all: 42,
615 | },
616 | wind: {
617 | speed: 0.99,
618 | deg: 113,
619 | },
620 | sys: {
621 | pod: 'n',
622 | },
623 | dt_txt: '2019-11-03 03:00:00',
624 | },
625 | {
626 | dt: 1572760800,
627 | main: {
628 | temp: 17.43,
629 | temp_min: 17.43,
630 | temp_max: 17.43,
631 | pressure: 1015,
632 | sea_level: 1015,
633 | grnd_level: 796,
634 | humidity: 74,
635 | temp_kf: 0,
636 | },
637 | weather: [
638 | {
639 | id: 801,
640 | main: 'Clouds',
641 | description: 'few clouds',
642 | icon: '02d',
643 | },
644 | ],
645 | clouds: {
646 | all: 24,
647 | },
648 | wind: {
649 | speed: 3.57,
650 | deg: 84,
651 | },
652 | sys: {
653 | pod: 'd',
654 | },
655 | dt_txt: '2019-11-03 06:00:00',
656 | },
657 | {
658 | dt: 1572771600,
659 | main: {
660 | temp: 22.05,
661 | temp_min: 22.05,
662 | temp_max: 22.05,
663 | pressure: 1012,
664 | sea_level: 1012,
665 | grnd_level: 794,
666 | humidity: 49,
667 | temp_kf: 0,
668 | },
669 | weather: [
670 | {
671 | id: 800,
672 | main: 'Clear',
673 | description: 'clear sky',
674 | icon: '01d',
675 | },
676 | ],
677 | clouds: {
678 | all: 0,
679 | },
680 | wind: {
681 | speed: 3.82,
682 | deg: 67,
683 | },
684 | sys: {
685 | pod: 'd',
686 | },
687 | dt_txt: '2019-11-03 09:00:00',
688 | },
689 | {
690 | dt: 1572782400,
691 | main: {
692 | temp: 23.11,
693 | temp_min: 23.11,
694 | temp_max: 23.11,
695 | pressure: 1010,
696 | sea_level: 1010,
697 | grnd_level: 792,
698 | humidity: 40,
699 | temp_kf: 0,
700 | },
701 | weather: [
702 | {
703 | id: 801,
704 | main: 'Clouds',
705 | description: 'few clouds',
706 | icon: '02d',
707 | },
708 | ],
709 | clouds: {
710 | all: 22,
711 | },
712 | wind: {
713 | speed: 3.46,
714 | deg: 63,
715 | },
716 | sys: {
717 | pod: 'd',
718 | },
719 | dt_txt: '2019-11-03 12:00:00',
720 | },
721 | {
722 | dt: 1572793200,
723 | main: {
724 | temp: 18.65,
725 | temp_min: 18.65,
726 | temp_max: 18.65,
727 | pressure: 1011,
728 | sea_level: 1011,
729 | grnd_level: 793,
730 | humidity: 60,
731 | temp_kf: 0,
732 | },
733 | weather: [
734 | {
735 | id: 804,
736 | main: 'Clouds',
737 | description: 'overcast clouds',
738 | icon: '04d',
739 | },
740 | ],
741 | clouds: {
742 | all: 100,
743 | },
744 | wind: {
745 | speed: 2.02,
746 | deg: 80,
747 | },
748 | sys: {
749 | pod: 'd',
750 | },
751 | dt_txt: '2019-11-03 15:00:00',
752 | },
753 | {
754 | dt: 1572804000,
755 | main: {
756 | temp: 16.54,
757 | temp_min: 16.54,
758 | temp_max: 16.54,
759 | pressure: 1015,
760 | sea_level: 1015,
761 | grnd_level: 795,
762 | humidity: 60,
763 | temp_kf: 0,
764 | },
765 | weather: [
766 | {
767 | id: 804,
768 | main: 'Clouds',
769 | description: 'overcast clouds',
770 | icon: '04n',
771 | },
772 | ],
773 | clouds: {
774 | all: 97,
775 | },
776 | wind: {
777 | speed: 2.05,
778 | deg: 70,
779 | },
780 | sys: {
781 | pod: 'n',
782 | },
783 | dt_txt: '2019-11-03 18:00:00',
784 | },
785 | {
786 | dt: 1572814800,
787 | main: {
788 | temp: 14.19,
789 | temp_min: 14.19,
790 | temp_max: 14.19,
791 | pressure: 1015,
792 | sea_level: 1015,
793 | grnd_level: 795,
794 | humidity: 72,
795 | temp_kf: 0,
796 | },
797 | weather: [
798 | {
799 | id: 803,
800 | main: 'Clouds',
801 | description: 'broken clouds',
802 | icon: '04n',
803 | },
804 | ],
805 | clouds: {
806 | all: 84,
807 | },
808 | wind: {
809 | speed: 1.4,
810 | deg: 56,
811 | },
812 | sys: {
813 | pod: 'n',
814 | },
815 | dt_txt: '2019-11-03 21:00:00',
816 | },
817 | {
818 | dt: 1572825600,
819 | main: {
820 | temp: 12.84,
821 | temp_min: 12.84,
822 | temp_max: 12.84,
823 | pressure: 1014,
824 | sea_level: 1014,
825 | grnd_level: 794,
826 | humidity: 82,
827 | temp_kf: 0,
828 | },
829 | weather: [
830 | {
831 | id: 803,
832 | main: 'Clouds',
833 | description: 'broken clouds',
834 | icon: '04n',
835 | },
836 | ],
837 | clouds: {
838 | all: 52,
839 | },
840 | wind: {
841 | speed: 1.36,
842 | deg: 94,
843 | },
844 | sys: {
845 | pod: 'n',
846 | },
847 | dt_txt: '2019-11-04 00:00:00',
848 | },
849 | {
850 | dt: 1572836400,
851 | main: {
852 | temp: 12.33,
853 | temp_min: 12.33,
854 | temp_max: 12.33,
855 | pressure: 1015,
856 | sea_level: 1015,
857 | grnd_level: 795,
858 | humidity: 86,
859 | temp_kf: 0,
860 | },
861 | weather: [
862 | {
863 | id: 800,
864 | main: 'Clear',
865 | description: 'clear sky',
866 | icon: '01n',
867 | },
868 | ],
869 | clouds: {
870 | all: 4,
871 | },
872 | wind: {
873 | speed: 1.84,
874 | deg: 90,
875 | },
876 | sys: {
877 | pod: 'n',
878 | },
879 | dt_txt: '2019-11-04 03:00:00',
880 | },
881 | {
882 | dt: 1572847200,
883 | main: {
884 | temp: 18.47,
885 | temp_min: 18.47,
886 | temp_max: 18.47,
887 | pressure: 1016,
888 | sea_level: 1016,
889 | grnd_level: 797,
890 | humidity: 61,
891 | temp_kf: 0,
892 | },
893 | weather: [
894 | {
895 | id: 801,
896 | main: 'Clouds',
897 | description: 'few clouds',
898 | icon: '02d',
899 | },
900 | ],
901 | clouds: {
902 | all: 18,
903 | },
904 | wind: {
905 | speed: 3.55,
906 | deg: 81,
907 | },
908 | sys: {
909 | pod: 'd',
910 | },
911 | dt_txt: '2019-11-04 06:00:00',
912 | },
913 | {
914 | dt: 1572858000,
915 | main: {
916 | temp: 22.19,
917 | temp_min: 22.19,
918 | temp_max: 22.19,
919 | pressure: 1013,
920 | sea_level: 1013,
921 | grnd_level: 795,
922 | humidity: 47,
923 | temp_kf: 0,
924 | },
925 | weather: [
926 | {
927 | id: 802,
928 | main: 'Clouds',
929 | description: 'scattered clouds',
930 | icon: '03d',
931 | },
932 | ],
933 | clouds: {
934 | all: 36,
935 | },
936 | wind: {
937 | speed: 3.7,
938 | deg: 66,
939 | },
940 | sys: {
941 | pod: 'd',
942 | },
943 | dt_txt: '2019-11-04 09:00:00',
944 | },
945 | {
946 | dt: 1572868800,
947 | main: {
948 | temp: 22.93,
949 | temp_min: 22.93,
950 | temp_max: 22.93,
951 | pressure: 1010,
952 | sea_level: 1010,
953 | grnd_level: 793,
954 | humidity: 42,
955 | temp_kf: 0,
956 | },
957 | weather: [
958 | {
959 | id: 802,
960 | main: 'Clouds',
961 | description: 'scattered clouds',
962 | icon: '03d',
963 | },
964 | ],
965 | clouds: {
966 | all: 34,
967 | },
968 | wind: {
969 | speed: 3.1,
970 | deg: 68,
971 | },
972 | sys: {
973 | pod: 'd',
974 | },
975 | dt_txt: '2019-11-04 12:00:00',
976 | },
977 | {
978 | dt: 1572879600,
979 | main: {
980 | temp: 18.99,
981 | temp_min: 18.99,
982 | temp_max: 18.99,
983 | pressure: 1012,
984 | sea_level: 1012,
985 | grnd_level: 793,
986 | humidity: 59,
987 | temp_kf: 0,
988 | },
989 | weather: [
990 | {
991 | id: 801,
992 | main: 'Clouds',
993 | description: 'few clouds',
994 | icon: '02d',
995 | },
996 | ],
997 | clouds: {
998 | all: 13,
999 | },
1000 | wind: {
1001 | speed: 1.58,
1002 | deg: 67,
1003 | },
1004 | sys: {
1005 | pod: 'd',
1006 | },
1007 | dt_txt: '2019-11-04 15:00:00',
1008 | },
1009 | {
1010 | dt: 1572890400,
1011 | main: {
1012 | temp: 16.9,
1013 | temp_min: 16.9,
1014 | temp_max: 16.9,
1015 | pressure: 1015,
1016 | sea_level: 1015,
1017 | grnd_level: 795,
1018 | humidity: 63,
1019 | temp_kf: 0,
1020 | },
1021 | weather: [
1022 | {
1023 | id: 802,
1024 | main: 'Clouds',
1025 | description: 'scattered clouds',
1026 | icon: '03n',
1027 | },
1028 | ],
1029 | clouds: {
1030 | all: 37,
1031 | },
1032 | wind: {
1033 | speed: 1.27,
1034 | deg: 83,
1035 | },
1036 | sys: {
1037 | pod: 'n',
1038 | },
1039 | dt_txt: '2019-11-04 18:00:00',
1040 | },
1041 | {
1042 | dt: 1572901200,
1043 | main: {
1044 | temp: 14.73,
1045 | temp_min: 14.73,
1046 | temp_max: 14.73,
1047 | pressure: 1014,
1048 | sea_level: 1014,
1049 | grnd_level: 794,
1050 | humidity: 78,
1051 | temp_kf: 0,
1052 | },
1053 | weather: [
1054 | {
1055 | id: 500,
1056 | main: 'Rain',
1057 | description: 'light rain',
1058 | icon: '10n',
1059 | },
1060 | ],
1061 | clouds: {
1062 | all: 74,
1063 | },
1064 | wind: {
1065 | speed: 1.63,
1066 | deg: 84,
1067 | },
1068 | rain: {
1069 | '3h': 0.13,
1070 | },
1071 | sys: {
1072 | pod: 'n',
1073 | },
1074 | dt_txt: '2019-11-04 21:00:00',
1075 | },
1076 | {
1077 | dt: 1572912000,
1078 | main: {
1079 | temp: 13.03,
1080 | temp_min: 13.03,
1081 | temp_max: 13.03,
1082 | pressure: 1013,
1083 | sea_level: 1013,
1084 | grnd_level: 793,
1085 | humidity: 87,
1086 | temp_kf: 0,
1087 | },
1088 | weather: [
1089 | {
1090 | id: 802,
1091 | main: 'Clouds',
1092 | description: 'scattered clouds',
1093 | icon: '03n',
1094 | },
1095 | ],
1096 | clouds: {
1097 | all: 39,
1098 | },
1099 | wind: {
1100 | speed: 0.8,
1101 | deg: 99,
1102 | },
1103 | sys: {
1104 | pod: 'n',
1105 | },
1106 | dt_txt: '2019-11-05 00:00:00',
1107 | },
1108 | {
1109 | dt: 1572922800,
1110 | main: {
1111 | temp: 12.41,
1112 | temp_min: 12.41,
1113 | temp_max: 12.41,
1114 | pressure: 1015,
1115 | sea_level: 1015,
1116 | grnd_level: 794,
1117 | humidity: 92,
1118 | temp_kf: 0,
1119 | },
1120 | weather: [
1121 | {
1122 | id: 800,
1123 | main: 'Clear',
1124 | description: 'clear sky',
1125 | icon: '01n',
1126 | },
1127 | ],
1128 | clouds: {
1129 | all: 0,
1130 | },
1131 | wind: {
1132 | speed: 0.96,
1133 | deg: 101,
1134 | },
1135 | sys: {
1136 | pod: 'n',
1137 | },
1138 | dt_txt: '2019-11-05 03:00:00',
1139 | },
1140 | {
1141 | dt: 1572933600,
1142 | main: {
1143 | temp: 17.96,
1144 | temp_min: 17.96,
1145 | temp_max: 17.96,
1146 | pressure: 1015,
1147 | sea_level: 1015,
1148 | grnd_level: 796,
1149 | humidity: 73,
1150 | temp_kf: 0,
1151 | },
1152 | weather: [
1153 | {
1154 | id: 500,
1155 | main: 'Rain',
1156 | description: 'light rain',
1157 | icon: '10d',
1158 | },
1159 | ],
1160 | clouds: {
1161 | all: 0,
1162 | },
1163 | wind: {
1164 | speed: 3.11,
1165 | deg: 81,
1166 | },
1167 | rain: {
1168 | '3h': 0.13,
1169 | },
1170 | sys: {
1171 | pod: 'd',
1172 | },
1173 | dt_txt: '2019-11-05 06:00:00',
1174 | },
1175 | {
1176 | dt: 1572944400,
1177 | main: {
1178 | temp: 19.98,
1179 | temp_min: 19.98,
1180 | temp_max: 19.98,
1181 | pressure: 1012,
1182 | sea_level: 1012,
1183 | grnd_level: 794,
1184 | humidity: 65,
1185 | temp_kf: 0,
1186 | },
1187 | weather: [
1188 | {
1189 | id: 500,
1190 | main: 'Rain',
1191 | description: 'light rain',
1192 | icon: '10d',
1193 | },
1194 | ],
1195 | clouds: {
1196 | all: 21,
1197 | },
1198 | wind: {
1199 | speed: 3.08,
1200 | deg: 72,
1201 | },
1202 | rain: {
1203 | '3h': 0.81,
1204 | },
1205 | sys: {
1206 | pod: 'd',
1207 | },
1208 | dt_txt: '2019-11-05 09:00:00',
1209 | },
1210 | {
1211 | dt: 1572955200,
1212 | main: {
1213 | temp: 21.15,
1214 | temp_min: 21.15,
1215 | temp_max: 21.15,
1216 | pressure: 1010,
1217 | sea_level: 1010,
1218 | grnd_level: 793,
1219 | humidity: 62,
1220 | temp_kf: 0,
1221 | },
1222 | weather: [
1223 | {
1224 | id: 500,
1225 | main: 'Rain',
1226 | description: 'light rain',
1227 | icon: '10d',
1228 | },
1229 | ],
1230 | clouds: {
1231 | all: 35,
1232 | },
1233 | wind: {
1234 | speed: 1.72,
1235 | deg: 80,
1236 | },
1237 | rain: {
1238 | '3h': 1.94,
1239 | },
1240 | sys: {
1241 | pod: 'd',
1242 | },
1243 | dt_txt: '2019-11-05 12:00:00',
1244 | },
1245 | {
1246 | dt: 1572966000,
1247 | main: {
1248 | temp: 17.82,
1249 | temp_min: 17.82,
1250 | temp_max: 17.82,
1251 | pressure: 1011,
1252 | sea_level: 1011,
1253 | grnd_level: 793,
1254 | humidity: 73,
1255 | temp_kf: 0,
1256 | },
1257 | weather: [
1258 | {
1259 | id: 500,
1260 | main: 'Rain',
1261 | description: 'light rain',
1262 | icon: '10d',
1263 | },
1264 | ],
1265 | clouds: {
1266 | all: 0,
1267 | },
1268 | wind: {
1269 | speed: 1.55,
1270 | deg: 205,
1271 | },
1272 | rain: {
1273 | '3h': 1.94,
1274 | },
1275 | sys: {
1276 | pod: 'd',
1277 | },
1278 | dt_txt: '2019-11-05 15:00:00',
1279 | },
1280 | {
1281 | dt: 1572976800,
1282 | main: {
1283 | temp: 16.09,
1284 | temp_min: 16.09,
1285 | temp_max: 16.09,
1286 | pressure: 1014,
1287 | sea_level: 1014,
1288 | grnd_level: 794,
1289 | humidity: 81,
1290 | temp_kf: 0,
1291 | },
1292 | weather: [
1293 | {
1294 | id: 500,
1295 | main: 'Rain',
1296 | description: 'light rain',
1297 | icon: '10n',
1298 | },
1299 | ],
1300 | clouds: {
1301 | all: 41,
1302 | },
1303 | wind: {
1304 | speed: 0.68,
1305 | deg: 203,
1306 | },
1307 | rain: {
1308 | '3h': 0.44,
1309 | },
1310 | sys: {
1311 | pod: 'n',
1312 | },
1313 | dt_txt: '2019-11-05 18:00:00',
1314 | },
1315 | {
1316 | dt: 1572987600,
1317 | main: {
1318 | temp: 14.27,
1319 | temp_min: 14.27,
1320 | temp_max: 14.27,
1321 | pressure: 1013,
1322 | sea_level: 1013,
1323 | grnd_level: 794,
1324 | humidity: 87,
1325 | temp_kf: 0,
1326 | },
1327 | weather: [
1328 | {
1329 | id: 500,
1330 | main: 'Rain',
1331 | description: 'light rain',
1332 | icon: '10n',
1333 | },
1334 | ],
1335 | clouds: {
1336 | all: 47,
1337 | },
1338 | wind: {
1339 | speed: 0.43,
1340 | deg: 75,
1341 | },
1342 | rain: {
1343 | '3h': 0.13,
1344 | },
1345 | sys: {
1346 | pod: 'n',
1347 | },
1348 | dt_txt: '2019-11-05 21:00:00',
1349 | },
1350 | {
1351 | dt: 1572998400,
1352 | main: {
1353 | temp: 13.03,
1354 | temp_min: 13.03,
1355 | temp_max: 13.03,
1356 | pressure: 1012,
1357 | sea_level: 1012,
1358 | grnd_level: 792,
1359 | humidity: 92,
1360 | temp_kf: 0,
1361 | },
1362 | weather: [
1363 | {
1364 | id: 500,
1365 | main: 'Rain',
1366 | description: 'light rain',
1367 | icon: '10n',
1368 | },
1369 | ],
1370 | clouds: {
1371 | all: 44,
1372 | },
1373 | wind: {
1374 | speed: 0.78,
1375 | deg: 122,
1376 | },
1377 | rain: {
1378 | '3h': 0.19,
1379 | },
1380 | sys: {
1381 | pod: 'n',
1382 | },
1383 | dt_txt: '2019-11-06 00:00:00',
1384 | },
1385 | {
1386 | dt: 1573009200,
1387 | main: {
1388 | temp: 12.66,
1389 | temp_min: 12.66,
1390 | temp_max: 12.66,
1391 | pressure: 1014,
1392 | sea_level: 1014,
1393 | grnd_level: 794,
1394 | humidity: 93,
1395 | temp_kf: 0,
1396 | },
1397 | weather: [
1398 | {
1399 | id: 500,
1400 | main: 'Rain',
1401 | description: 'light rain',
1402 | icon: '10n',
1403 | },
1404 | ],
1405 | clouds: {
1406 | all: 1,
1407 | },
1408 | wind: {
1409 | speed: 1.39,
1410 | deg: 105,
1411 | },
1412 | rain: {
1413 | '3h': 0.44,
1414 | },
1415 | sys: {
1416 | pod: 'n',
1417 | },
1418 | dt_txt: '2019-11-06 03:00:00',
1419 | },
1420 | {
1421 | dt: 1573020000,
1422 | main: {
1423 | temp: 17.38,
1424 | temp_min: 17.38,
1425 | temp_max: 17.38,
1426 | pressure: 1014,
1427 | sea_level: 1014,
1428 | grnd_level: 795,
1429 | humidity: 78,
1430 | temp_kf: 0,
1431 | },
1432 | weather: [
1433 | {
1434 | id: 500,
1435 | main: 'Rain',
1436 | description: 'light rain',
1437 | icon: '10d',
1438 | },
1439 | ],
1440 | clouds: {
1441 | all: 0,
1442 | },
1443 | wind: {
1444 | speed: 3.2,
1445 | deg: 86,
1446 | },
1447 | rain: {
1448 | '3h': 0.12,
1449 | },
1450 | sys: {
1451 | pod: 'd',
1452 | },
1453 | dt_txt: '2019-11-06 06:00:00',
1454 | },
1455 | {
1456 | dt: 1573030800,
1457 | main: {
1458 | temp: 21.36,
1459 | temp_min: 21.36,
1460 | temp_max: 21.36,
1461 | pressure: 1011,
1462 | sea_level: 1011,
1463 | grnd_level: 794,
1464 | humidity: 58,
1465 | temp_kf: 0,
1466 | },
1467 | weather: [
1468 | {
1469 | id: 500,
1470 | main: 'Rain',
1471 | description: 'light rain',
1472 | icon: '10d',
1473 | },
1474 | ],
1475 | clouds: {
1476 | all: 0,
1477 | },
1478 | wind: {
1479 | speed: 3.46,
1480 | deg: 70,
1481 | },
1482 | rain: {
1483 | '3h': 0.13,
1484 | },
1485 | sys: {
1486 | pod: 'd',
1487 | },
1488 | dt_txt: '2019-11-06 09:00:00',
1489 | },
1490 | ],
1491 | };
1492 |
1493 | export const mockSearchWeatherData = {
1494 | base: 'stations',
1495 | clouds: {
1496 | all: 20,
1497 | },
1498 | cod: 200,
1499 | coord: {
1500 | lon: 13.41,
1501 | lat: 52.52,
1502 | },
1503 | dt: 1588244935,
1504 | id: 2950159,
1505 | main: {
1506 | temp: 17.08,
1507 | feels_like: 14.51,
1508 | temp_min: 16,
1509 | temp_max: 18.33,
1510 | pressure: 1003,
1511 | humidity: 67,
1512 | },
1513 | name: 'Rio de janeiro',
1514 | sys: {
1515 | type: 1,
1516 | id: 1275,
1517 | country: 'BR',
1518 | sunrise: 1588217769,
1519 | sunset: 1588271443,
1520 | },
1521 | timezone: 7200,
1522 | visibility: 10000,
1523 | weather: [
1524 | {
1525 | id: 500,
1526 | main: 'Rain',
1527 | description: 'light rain',
1528 | icon: '10d',
1529 | },
1530 | ],
1531 | wind: {
1532 | speed: 4.1,
1533 | deg: 200,
1534 | },
1535 | };
1536 |
1537 | export const mockSearchForecastData = {
1538 | location: {
1539 | id: 2950159,
1540 | name: 'Rio de janeiro',
1541 | coord: {
1542 | lat: 52.5244,
1543 | lon: 13.4105,
1544 | },
1545 | country: 'BR',
1546 | population: 1000000,
1547 | timezone: 7200,
1548 | sunrise: 1588217768,
1549 | sunset: 1588271444,
1550 | },
1551 | cod: '200',
1552 | message: 0,
1553 | cnt: 40,
1554 | list: [
1555 | {
1556 | dt: 1588248000,
1557 | main: {
1558 | temp: 16.16,
1559 | feels_like: 13.17,
1560 | temp_min: 15.19,
1561 | temp_max: 16.16,
1562 | pressure: 1003,
1563 | sea_level: 1004,
1564 | grnd_level: 999,
1565 | humidity: 67,
1566 | temp_kf: 0.97,
1567 | },
1568 | weather: [
1569 | {
1570 | id: 500,
1571 | main: 'Rain',
1572 | description: 'light rain',
1573 | icon: '10d',
1574 | },
1575 | ],
1576 | clouds: {
1577 | all: 59,
1578 | },
1579 | wind: {
1580 | speed: 4.34,
1581 | deg: 200,
1582 | },
1583 | rain: {
1584 | '3h': 0.65,
1585 | },
1586 | sys: {
1587 | pod: 'd',
1588 | },
1589 | dt_txt: '2020-04-30 12:00:00',
1590 | },
1591 | {
1592 | dt: 1588258800,
1593 | main: {
1594 | temp: 17.06,
1595 | feels_like: 12.98,
1596 | temp_min: 17.05,
1597 | temp_max: 17.06,
1598 | pressure: 1003,
1599 | sea_level: 1003,
1600 | grnd_level: 998,
1601 | humidity: 60,
1602 | temp_kf: 0.01,
1603 | },
1604 | weather: [
1605 | {
1606 | id: 500,
1607 | main: 'Rain',
1608 | description: 'light rain',
1609 | icon: '10d',
1610 | },
1611 | ],
1612 | clouds: {
1613 | all: 83,
1614 | },
1615 | wind: {
1616 | speed: 5.6,
1617 | deg: 222,
1618 | },
1619 | rain: {
1620 | '3h': 0.62,
1621 | },
1622 | sys: {
1623 | pod: 'd',
1624 | },
1625 | dt_txt: '2020-04-30 15:00:00',
1626 | },
1627 | {
1628 | dt: 1588269600,
1629 | main: {
1630 | temp: 14.94,
1631 | feels_like: 12.63,
1632 | temp_min: 14.79,
1633 | temp_max: 14.94,
1634 | pressure: 1004,
1635 | sea_level: 1004,
1636 | grnd_level: 999,
1637 | humidity: 68,
1638 | temp_kf: 0.15,
1639 | },
1640 | weather: [
1641 | {
1642 | id: 500,
1643 | main: 'Rain',
1644 | description: 'light rain',
1645 | icon: '10d',
1646 | },
1647 | ],
1648 | clouds: {
1649 | all: 95,
1650 | },
1651 | wind: {
1652 | speed: 3.02,
1653 | deg: 231,
1654 | },
1655 | rain: {
1656 | '3h': 0.13,
1657 | },
1658 | sys: {
1659 | pod: 'd',
1660 | },
1661 | dt_txt: '2020-04-30 18:00:00',
1662 | },
1663 | {
1664 | dt: 1588280400,
1665 | main: {
1666 | temp: 12.48,
1667 | feels_like: 10.67,
1668 | temp_min: 12.44,
1669 | temp_max: 12.48,
1670 | pressure: 1003,
1671 | sea_level: 1003,
1672 | grnd_level: 998,
1673 | humidity: 79,
1674 | temp_kf: 0.04,
1675 | },
1676 | weather: [
1677 | {
1678 | id: 500,
1679 | main: 'Rain',
1680 | description: 'light rain',
1681 | icon: '10n',
1682 | },
1683 | ],
1684 | clouds: {
1685 | all: 99,
1686 | },
1687 | wind: {
1688 | speed: 2.25,
1689 | deg: 175,
1690 | },
1691 | rain: {
1692 | '3h': 0.34,
1693 | },
1694 | sys: {
1695 | pod: 'n',
1696 | },
1697 | dt_txt: '2020-04-30 21:00:00',
1698 | },
1699 | {
1700 | dt: 1588291200,
1701 | main: {
1702 | temp: 12.73,
1703 | feels_like: 8.99,
1704 | temp_min: 12.73,
1705 | temp_max: 12.73,
1706 | pressure: 1004,
1707 | sea_level: 1004,
1708 | grnd_level: 999,
1709 | humidity: 72,
1710 | temp_kf: 0,
1711 | },
1712 | weather: [
1713 | {
1714 | id: 500,
1715 | main: 'Rain',
1716 | description: 'light rain',
1717 | icon: '10n',
1718 | },
1719 | ],
1720 | clouds: {
1721 | all: 100,
1722 | },
1723 | wind: {
1724 | speed: 4.62,
1725 | deg: 208,
1726 | },
1727 | rain: {
1728 | '3h': 0.26,
1729 | },
1730 | sys: {
1731 | pod: 'n',
1732 | },
1733 | dt_txt: '2020-05-01 00:00:00',
1734 | },
1735 | {
1736 | dt: 1588302000,
1737 | main: {
1738 | temp: 10.58,
1739 | feels_like: 6.51,
1740 | temp_min: 10.58,
1741 | temp_max: 10.58,
1742 | pressure: 1003,
1743 | sea_level: 1003,
1744 | grnd_level: 998,
1745 | humidity: 74,
1746 | temp_kf: 0,
1747 | },
1748 | weather: [
1749 | {
1750 | id: 804,
1751 | main: 'Clouds',
1752 | description: 'overcast clouds',
1753 | icon: '04n',
1754 | },
1755 | ],
1756 | clouds: {
1757 | all: 100,
1758 | },
1759 | wind: {
1760 | speed: 4.55,
1761 | deg: 216,
1762 | },
1763 | sys: {
1764 | pod: 'n',
1765 | },
1766 | dt_txt: '2020-05-01 03:00:00',
1767 | },
1768 | {
1769 | dt: 1588312800,
1770 | main: {
1771 | temp: 10.98,
1772 | feels_like: 6.35,
1773 | temp_min: 10.98,
1774 | temp_max: 10.98,
1775 | pressure: 1004,
1776 | sea_level: 1004,
1777 | grnd_level: 999,
1778 | humidity: 72,
1779 | temp_kf: 0,
1780 | },
1781 | weather: [
1782 | {
1783 | id: 804,
1784 | main: 'Clouds',
1785 | description: 'overcast clouds',
1786 | icon: '04d',
1787 | },
1788 | ],
1789 | clouds: {
1790 | all: 100,
1791 | },
1792 | wind: {
1793 | speed: 5.34,
1794 | deg: 225,
1795 | },
1796 | sys: {
1797 | pod: 'd',
1798 | },
1799 | dt_txt: '2020-05-01 06:00:00',
1800 | },
1801 | {
1802 | dt: 1588323600,
1803 | main: {
1804 | temp: 14.55,
1805 | feels_like: 9.98,
1806 | temp_min: 14.55,
1807 | temp_max: 14.55,
1808 | pressure: 1004,
1809 | sea_level: 1004,
1810 | grnd_level: 998,
1811 | humidity: 49,
1812 | temp_kf: 0,
1813 | },
1814 | weather: [
1815 | {
1816 | id: 803,
1817 | main: 'Clouds',
1818 | description: 'broken clouds',
1819 | icon: '04d',
1820 | },
1821 | ],
1822 | clouds: {
1823 | all: 69,
1824 | },
1825 | wind: {
1826 | speed: 4.64,
1827 | deg: 236,
1828 | },
1829 | sys: {
1830 | pod: 'd',
1831 | },
1832 | dt_txt: '2020-05-01 09:00:00',
1833 | },
1834 | {
1835 | dt: 1588334400,
1836 | main: {
1837 | temp: 16.66,
1838 | feels_like: 11.57,
1839 | temp_min: 16.66,
1840 | temp_max: 16.66,
1841 | pressure: 1003,
1842 | sea_level: 1003,
1843 | grnd_level: 998,
1844 | humidity: 42,
1845 | temp_kf: 0,
1846 | },
1847 | weather: [
1848 | {
1849 | id: 500,
1850 | main: 'Rain',
1851 | description: 'light rain',
1852 | icon: '10d',
1853 | },
1854 | ],
1855 | clouds: {
1856 | all: 51,
1857 | },
1858 | wind: {
1859 | speed: 5.3,
1860 | deg: 232,
1861 | },
1862 | rain: {
1863 | '3h': 0.6,
1864 | },
1865 | sys: {
1866 | pod: 'd',
1867 | },
1868 | dt_txt: '2020-05-01 12:00:00',
1869 | },
1870 | {
1871 | dt: 1588345200,
1872 | main: {
1873 | temp: 17.15,
1874 | feels_like: 11.92,
1875 | temp_min: 17.15,
1876 | temp_max: 17.15,
1877 | pressure: 1001,
1878 | sea_level: 1001,
1879 | grnd_level: 996,
1880 | humidity: 40,
1881 | temp_kf: 0,
1882 | },
1883 | weather: [
1884 | {
1885 | id: 500,
1886 | main: 'Rain',
1887 | description: 'light rain',
1888 | icon: '10d',
1889 | },
1890 | ],
1891 | clouds: {
1892 | all: 48,
1893 | },
1894 | wind: {
1895 | speed: 5.44,
1896 | deg: 226,
1897 | },
1898 | rain: {
1899 | '3h': 0.19,
1900 | },
1901 | sys: {
1902 | pod: 'd',
1903 | },
1904 | dt_txt: '2020-05-01 15:00:00',
1905 | },
1906 | {
1907 | dt: 1588356000,
1908 | main: {
1909 | temp: 14.02,
1910 | feels_like: 9.78,
1911 | temp_min: 14.02,
1912 | temp_max: 14.02,
1913 | pressure: 1002,
1914 | sea_level: 1002,
1915 | grnd_level: 997,
1916 | humidity: 55,
1917 | temp_kf: 0,
1918 | },
1919 | weather: [
1920 | {
1921 | id: 500,
1922 | main: 'Rain',
1923 | description: 'light rain',
1924 | icon: '10d',
1925 | },
1926 | ],
1927 | clouds: {
1928 | all: 73,
1929 | },
1930 | wind: {
1931 | speed: 4.49,
1932 | deg: 226,
1933 | },
1934 | rain: {
1935 | '3h': 0.37,
1936 | },
1937 | sys: {
1938 | pod: 'd',
1939 | },
1940 | dt_txt: '2020-05-01 18:00:00',
1941 | },
1942 | {
1943 | dt: 1588366800,
1944 | main: {
1945 | temp: 11.6,
1946 | feels_like: 7.94,
1947 | temp_min: 11.6,
1948 | temp_max: 11.6,
1949 | pressure: 1002,
1950 | sea_level: 1002,
1951 | grnd_level: 997,
1952 | humidity: 65,
1953 | temp_kf: 0,
1954 | },
1955 | weather: [
1956 | {
1957 | id: 500,
1958 | main: 'Rain',
1959 | description: 'light rain',
1960 | icon: '10n',
1961 | },
1962 | ],
1963 | clouds: {
1964 | all: 62,
1965 | },
1966 | wind: {
1967 | speed: 3.69,
1968 | deg: 220,
1969 | },
1970 | rain: {
1971 | '3h': 0.15,
1972 | },
1973 | sys: {
1974 | pod: 'n',
1975 | },
1976 | dt_txt: '2020-05-01 21:00:00',
1977 | },
1978 | {
1979 | dt: 1588377600,
1980 | main: {
1981 | temp: 9.86,
1982 | feels_like: 6.02,
1983 | temp_min: 9.86,
1984 | temp_max: 9.86,
1985 | pressure: 1003,
1986 | sea_level: 1003,
1987 | grnd_level: 998,
1988 | humidity: 75,
1989 | temp_kf: 0,
1990 | },
1991 | weather: [
1992 | {
1993 | id: 802,
1994 | main: 'Clouds',
1995 | description: 'scattered clouds',
1996 | icon: '03n',
1997 | },
1998 | ],
1999 | clouds: {
2000 | all: 34,
2001 | },
2002 | wind: {
2003 | speed: 4.06,
2004 | deg: 233,
2005 | },
2006 | sys: {
2007 | pod: 'n',
2008 | },
2009 | dt_txt: '2020-05-02 00:00:00',
2010 | },
2011 | {
2012 | dt: 1588388400,
2013 | main: {
2014 | temp: 8.95,
2015 | feels_like: 5.1,
2016 | temp_min: 8.95,
2017 | temp_max: 8.95,
2018 | pressure: 1004,
2019 | sea_level: 1004,
2020 | grnd_level: 999,
2021 | humidity: 78,
2022 | temp_kf: 0,
2023 | },
2024 | weather: [
2025 | {
2026 | id: 802,
2027 | main: 'Clouds',
2028 | description: 'scattered clouds',
2029 | icon: '03n',
2030 | },
2031 | ],
2032 | clouds: {
2033 | all: 46,
2034 | },
2035 | wind: {
2036 | speed: 3.98,
2037 | deg: 232,
2038 | },
2039 | sys: {
2040 | pod: 'n',
2041 | },
2042 | dt_txt: '2020-05-02 03:00:00',
2043 | },
2044 | {
2045 | dt: 1588399200,
2046 | main: {
2047 | temp: 10.48,
2048 | feels_like: 6.79,
2049 | temp_min: 10.48,
2050 | temp_max: 10.48,
2051 | pressure: 1005,
2052 | sea_level: 1005,
2053 | grnd_level: 1000,
2054 | humidity: 73,
2055 | temp_kf: 0,
2056 | },
2057 | weather: [
2058 | {
2059 | id: 802,
2060 | main: 'Clouds',
2061 | description: 'scattered clouds',
2062 | icon: '03d',
2063 | },
2064 | ],
2065 | clouds: {
2066 | all: 43,
2067 | },
2068 | wind: {
2069 | speed: 3.91,
2070 | deg: 237,
2071 | },
2072 | sys: {
2073 | pod: 'd',
2074 | },
2075 | dt_txt: '2020-05-02 06:00:00',
2076 | },
2077 | {
2078 | dt: 1588410000,
2079 | main: {
2080 | temp: 14.08,
2081 | feels_like: 9.57,
2082 | temp_min: 14.08,
2083 | temp_max: 14.08,
2084 | pressure: 1005,
2085 | sea_level: 1005,
2086 | grnd_level: 1000,
2087 | humidity: 58,
2088 | temp_kf: 0,
2089 | },
2090 | weather: [
2091 | {
2092 | id: 500,
2093 | main: 'Rain',
2094 | description: 'light rain',
2095 | icon: '10d',
2096 | },
2097 | ],
2098 | clouds: {
2099 | all: 9,
2100 | },
2101 | wind: {
2102 | speed: 5.12,
2103 | deg: 245,
2104 | },
2105 | rain: {
2106 | '3h': 1.05,
2107 | },
2108 | sys: {
2109 | pod: 'd',
2110 | },
2111 | dt_txt: '2020-05-02 09:00:00',
2112 | },
2113 | {
2114 | dt: 1588420800,
2115 | main: {
2116 | temp: 14.98,
2117 | feels_like: 10.24,
2118 | temp_min: 14.98,
2119 | temp_max: 14.98,
2120 | pressure: 1005,
2121 | sea_level: 1005,
2122 | grnd_level: 1000,
2123 | humidity: 57,
2124 | temp_kf: 0,
2125 | },
2126 | weather: [
2127 | {
2128 | id: 500,
2129 | main: 'Rain',
2130 | description: 'light rain',
2131 | icon: '10d',
2132 | },
2133 | ],
2134 | clouds: {
2135 | all: 45,
2136 | },
2137 | wind: {
2138 | speed: 5.62,
2139 | deg: 267,
2140 | },
2141 | rain: {
2142 | '3h': 2.31,
2143 | },
2144 | sys: {
2145 | pod: 'd',
2146 | },
2147 | dt_txt: '2020-05-02 12:00:00',
2148 | },
2149 | {
2150 | dt: 1588431600,
2151 | main: {
2152 | temp: 14.75,
2153 | feels_like: 10.12,
2154 | temp_min: 14.75,
2155 | temp_max: 14.75,
2156 | pressure: 1004,
2157 | sea_level: 1004,
2158 | grnd_level: 999,
2159 | humidity: 53,
2160 | temp_kf: 0,
2161 | },
2162 | weather: [
2163 | {
2164 | id: 500,
2165 | main: 'Rain',
2166 | description: 'light rain',
2167 | icon: '10d',
2168 | },
2169 | ],
2170 | clouds: {
2171 | all: 96,
2172 | },
2173 | wind: {
2174 | speed: 5.09,
2175 | deg: 260,
2176 | },
2177 | rain: {
2178 | '3h': 1.26,
2179 | },
2180 | sys: {
2181 | pod: 'd',
2182 | },
2183 | dt_txt: '2020-05-02 15:00:00',
2184 | },
2185 | {
2186 | dt: 1588442400,
2187 | main: {
2188 | temp: 12.37,
2189 | feels_like: 8.24,
2190 | temp_min: 12.37,
2191 | temp_max: 12.37,
2192 | pressure: 1005,
2193 | sea_level: 1005,
2194 | grnd_level: 1000,
2195 | humidity: 62,
2196 | temp_kf: 0,
2197 | },
2198 | weather: [
2199 | {
2200 | id: 500,
2201 | main: 'Rain',
2202 | description: 'light rain',
2203 | icon: '10d',
2204 | },
2205 | ],
2206 | clouds: {
2207 | all: 97,
2208 | },
2209 | wind: {
2210 | speed: 4.38,
2211 | deg: 260,
2212 | },
2213 | rain: {
2214 | '3h': 0.82,
2215 | },
2216 | sys: {
2217 | pod: 'd',
2218 | },
2219 | dt_txt: '2020-05-02 18:00:00',
2220 | },
2221 | {
2222 | dt: 1588453200,
2223 | main: {
2224 | temp: 9.98,
2225 | feels_like: 6.26,
2226 | temp_min: 9.98,
2227 | temp_max: 9.98,
2228 | pressure: 1007,
2229 | sea_level: 1007,
2230 | grnd_level: 1002,
2231 | humidity: 77,
2232 | temp_kf: 0,
2233 | },
2234 | weather: [
2235 | {
2236 | id: 500,
2237 | main: 'Rain',
2238 | description: 'light rain',
2239 | icon: '10n',
2240 | },
2241 | ],
2242 | clouds: {
2243 | all: 100,
2244 | },
2245 | wind: {
2246 | speed: 4.05,
2247 | deg: 274,
2248 | },
2249 | rain: {
2250 | '3h': 0.17,
2251 | },
2252 | sys: {
2253 | pod: 'n',
2254 | },
2255 | dt_txt: '2020-05-02 21:00:00',
2256 | },
2257 | {
2258 | dt: 1588464000,
2259 | main: {
2260 | temp: 8.86,
2261 | feels_like: 4.71,
2262 | temp_min: 8.86,
2263 | temp_max: 8.86,
2264 | pressure: 1009,
2265 | sea_level: 1009,
2266 | grnd_level: 1004,
2267 | humidity: 82,
2268 | temp_kf: 0,
2269 | },
2270 | weather: [
2271 | {
2272 | id: 804,
2273 | main: 'Clouds',
2274 | description: 'overcast clouds',
2275 | icon: '04n',
2276 | },
2277 | ],
2278 | clouds: {
2279 | all: 100,
2280 | },
2281 | wind: {
2282 | speed: 4.6,
2283 | deg: 286,
2284 | },
2285 | sys: {
2286 | pod: 'n',
2287 | },
2288 | dt_txt: '2020-05-03 00:00:00',
2289 | },
2290 | {
2291 | dt: 1588474800,
2292 | main: {
2293 | temp: 7.3,
2294 | feels_like: 2.81,
2295 | temp_min: 7.3,
2296 | temp_max: 7.3,
2297 | pressure: 1011,
2298 | sea_level: 1011,
2299 | grnd_level: 1006,
2300 | humidity: 87,
2301 | temp_kf: 0,
2302 | },
2303 | weather: [
2304 | {
2305 | id: 803,
2306 | main: 'Clouds',
2307 | description: 'broken clouds',
2308 | icon: '04n',
2309 | },
2310 | ],
2311 | clouds: {
2312 | all: 63,
2313 | },
2314 | wind: {
2315 | speed: 4.89,
2316 | deg: 283,
2317 | },
2318 | sys: {
2319 | pod: 'n',
2320 | },
2321 | dt_txt: '2020-05-03 03:00:00',
2322 | },
2323 | {
2324 | dt: 1588485600,
2325 | main: {
2326 | temp: 8.04,
2327 | feels_like: 3.02,
2328 | temp_min: 8.04,
2329 | temp_max: 8.04,
2330 | pressure: 1014,
2331 | sea_level: 1014,
2332 | grnd_level: 1008,
2333 | humidity: 83,
2334 | temp_kf: 0,
2335 | },
2336 | weather: [
2337 | {
2338 | id: 803,
2339 | main: 'Clouds',
2340 | description: 'broken clouds',
2341 | icon: '04d',
2342 | },
2343 | ],
2344 | clouds: {
2345 | all: 82,
2346 | },
2347 | wind: {
2348 | speed: 5.66,
2349 | deg: 289,
2350 | },
2351 | sys: {
2352 | pod: 'd',
2353 | },
2354 | dt_txt: '2020-05-03 06:00:00',
2355 | },
2356 | {
2357 | dt: 1588496400,
2358 | main: {
2359 | temp: 9.59,
2360 | feels_like: 4.9,
2361 | temp_min: 9.59,
2362 | temp_max: 9.59,
2363 | pressure: 1016,
2364 | sea_level: 1016,
2365 | grnd_level: 1010,
2366 | humidity: 74,
2367 | temp_kf: 0,
2368 | },
2369 | weather: [
2370 | {
2371 | id: 804,
2372 | main: 'Clouds',
2373 | description: 'overcast clouds',
2374 | icon: '04d',
2375 | },
2376 | ],
2377 | clouds: {
2378 | all: 100,
2379 | },
2380 | wind: {
2381 | speed: 5.14,
2382 | deg: 277,
2383 | },
2384 | sys: {
2385 | pod: 'd',
2386 | },
2387 | dt_txt: '2020-05-03 09:00:00',
2388 | },
2389 | {
2390 | dt: 1588507200,
2391 | main: {
2392 | temp: 13.31,
2393 | feels_like: 8.93,
2394 | temp_min: 13.31,
2395 | temp_max: 13.31,
2396 | pressure: 1016,
2397 | sea_level: 1016,
2398 | grnd_level: 1011,
2399 | humidity: 54,
2400 | temp_kf: 0,
2401 | },
2402 | weather: [
2403 | {
2404 | id: 804,
2405 | main: 'Clouds',
2406 | description: 'overcast clouds',
2407 | icon: '04d',
2408 | },
2409 | ],
2410 | clouds: {
2411 | all: 97,
2412 | },
2413 | wind: {
2414 | speed: 4.43,
2415 | deg: 277,
2416 | },
2417 | sys: {
2418 | pod: 'd',
2419 | },
2420 | dt_txt: '2020-05-03 12:00:00',
2421 | },
2422 | {
2423 | dt: 1588518000,
2424 | main: {
2425 | temp: 14.56,
2426 | feels_like: 9.96,
2427 | temp_min: 14.56,
2428 | temp_max: 14.56,
2429 | pressure: 1016,
2430 | sea_level: 1016,
2431 | grnd_level: 1011,
2432 | humidity: 50,
2433 | temp_kf: 0,
2434 | },
2435 | weather: [
2436 | {
2437 | id: 500,
2438 | main: 'Rain',
2439 | description: 'light rain',
2440 | icon: '10d',
2441 | },
2442 | ],
2443 | clouds: {
2444 | all: 66,
2445 | },
2446 | wind: {
2447 | speed: 4.75,
2448 | deg: 270,
2449 | },
2450 | rain: {
2451 | '3h': 0.14,
2452 | },
2453 | sys: {
2454 | pod: 'd',
2455 | },
2456 | dt_txt: '2020-05-03 15:00:00',
2457 | },
2458 | {
2459 | dt: 1588528800,
2460 | main: {
2461 | temp: 12.31,
2462 | feels_like: 8.89,
2463 | temp_min: 12.31,
2464 | temp_max: 12.31,
2465 | pressure: 1017,
2466 | sea_level: 1017,
2467 | grnd_level: 1012,
2468 | humidity: 67,
2469 | temp_kf: 0,
2470 | },
2471 | weather: [
2472 | {
2473 | id: 802,
2474 | main: 'Clouds',
2475 | description: 'scattered clouds',
2476 | icon: '03d',
2477 | },
2478 | ],
2479 | clouds: {
2480 | all: 37,
2481 | },
2482 | wind: {
2483 | speed: 3.68,
2484 | deg: 268,
2485 | },
2486 | sys: {
2487 | pod: 'd',
2488 | },
2489 | dt_txt: '2020-05-03 18:00:00',
2490 | },
2491 | {
2492 | dt: 1588539600,
2493 | main: {
2494 | temp: 9.55,
2495 | feels_like: 6.67,
2496 | temp_min: 9.55,
2497 | temp_max: 9.55,
2498 | pressure: 1019,
2499 | sea_level: 1019,
2500 | grnd_level: 1014,
2501 | humidity: 84,
2502 | temp_kf: 0,
2503 | },
2504 | weather: [
2505 | {
2506 | id: 800,
2507 | main: 'Clear',
2508 | description: 'clear sky',
2509 | icon: '01n',
2510 | },
2511 | ],
2512 | clouds: {
2513 | all: 1,
2514 | },
2515 | wind: {
2516 | speed: 3.11,
2517 | deg: 303,
2518 | },
2519 | sys: {
2520 | pod: 'n',
2521 | },
2522 | dt_txt: '2020-05-03 21:00:00',
2523 | },
2524 | {
2525 | dt: 1588550400,
2526 | main: {
2527 | temp: 8.23,
2528 | feels_like: 5.43,
2529 | temp_min: 8.23,
2530 | temp_max: 8.23,
2531 | pressure: 1020,
2532 | sea_level: 1020,
2533 | grnd_level: 1015,
2534 | humidity: 77,
2535 | temp_kf: 0,
2536 | },
2537 | weather: [
2538 | {
2539 | id: 800,
2540 | main: 'Clear',
2541 | description: 'clear sky',
2542 | icon: '01n',
2543 | },
2544 | ],
2545 | clouds: {
2546 | all: 9,
2547 | },
2548 | wind: {
2549 | speed: 2.23,
2550 | deg: 314,
2551 | },
2552 | sys: {
2553 | pod: 'n',
2554 | },
2555 | dt_txt: '2020-05-04 00:00:00',
2556 | },
2557 | {
2558 | dt: 1588561200,
2559 | main: {
2560 | temp: 6.71,
2561 | feels_like: 4.11,
2562 | temp_min: 6.71,
2563 | temp_max: 6.71,
2564 | pressure: 1020,
2565 | sea_level: 1020,
2566 | grnd_level: 1015,
2567 | humidity: 84,
2568 | temp_kf: 0,
2569 | },
2570 | weather: [
2571 | {
2572 | id: 800,
2573 | main: 'Clear',
2574 | description: 'clear sky',
2575 | icon: '01n',
2576 | },
2577 | ],
2578 | clouds: {
2579 | all: 7,
2580 | },
2581 | wind: {
2582 | speed: 1.89,
2583 | deg: 294,
2584 | },
2585 | sys: {
2586 | pod: 'n',
2587 | },
2588 | dt_txt: '2020-05-04 03:00:00',
2589 | },
2590 | {
2591 | dt: 1588572000,
2592 | main: {
2593 | temp: 8.46,
2594 | feels_like: 5.47,
2595 | temp_min: 8.46,
2596 | temp_max: 8.46,
2597 | pressure: 1021,
2598 | sea_level: 1021,
2599 | grnd_level: 1016,
2600 | humidity: 69,
2601 | temp_kf: 0,
2602 | },
2603 | weather: [
2604 | {
2605 | id: 801,
2606 | main: 'Clouds',
2607 | description: 'few clouds',
2608 | icon: '02d',
2609 | },
2610 | ],
2611 | clouds: {
2612 | all: 18,
2613 | },
2614 | wind: {
2615 | speed: 2.15,
2616 | deg: 295,
2617 | },
2618 | sys: {
2619 | pod: 'd',
2620 | },
2621 | dt_txt: '2020-05-04 06:00:00',
2622 | },
2623 | {
2624 | dt: 1588582800,
2625 | main: {
2626 | temp: 13.15,
2627 | feels_like: 9.54,
2628 | temp_min: 13.15,
2629 | temp_max: 13.15,
2630 | pressure: 1021,
2631 | sea_level: 1021,
2632 | grnd_level: 1016,
2633 | humidity: 39,
2634 | temp_kf: 0,
2635 | },
2636 | weather: [
2637 | {
2638 | id: 803,
2639 | main: 'Clouds',
2640 | description: 'broken clouds',
2641 | icon: '04d',
2642 | },
2643 | ],
2644 | clouds: {
2645 | all: 81,
2646 | },
2647 | wind: {
2648 | speed: 2.22,
2649 | deg: 293,
2650 | },
2651 | sys: {
2652 | pod: 'd',
2653 | },
2654 | dt_txt: '2020-05-04 09:00:00',
2655 | },
2656 | {
2657 | dt: 1588593600,
2658 | main: {
2659 | temp: 16.2,
2660 | feels_like: 12.74,
2661 | temp_min: 16.2,
2662 | temp_max: 16.2,
2663 | pressure: 1020,
2664 | sea_level: 1020,
2665 | grnd_level: 1015,
2666 | humidity: 31,
2667 | temp_kf: 0,
2668 | },
2669 | weather: [
2670 | {
2671 | id: 804,
2672 | main: 'Clouds',
2673 | description: 'overcast clouds',
2674 | icon: '04d',
2675 | },
2676 | ],
2677 | clouds: {
2678 | all: 85,
2679 | },
2680 | wind: {
2681 | speed: 1.92,
2682 | deg: 288,
2683 | },
2684 | sys: {
2685 | pod: 'd',
2686 | },
2687 | dt_txt: '2020-05-04 12:00:00',
2688 | },
2689 | {
2690 | dt: 1588604400,
2691 | main: {
2692 | temp: 16.41,
2693 | feels_like: 12.63,
2694 | temp_min: 16.41,
2695 | temp_max: 16.41,
2696 | pressure: 1019,
2697 | sea_level: 1019,
2698 | grnd_level: 1014,
2699 | humidity: 31,
2700 | temp_kf: 0,
2701 | },
2702 | weather: [
2703 | {
2704 | id: 804,
2705 | main: 'Clouds',
2706 | description: 'overcast clouds',
2707 | icon: '04d',
2708 | },
2709 | ],
2710 | clouds: {
2711 | all: 100,
2712 | },
2713 | wind: {
2714 | speed: 2.41,
2715 | deg: 306,
2716 | },
2717 | sys: {
2718 | pod: 'd',
2719 | },
2720 | dt_txt: '2020-05-04 15:00:00',
2721 | },
2722 | {
2723 | dt: 1588615200,
2724 | main: {
2725 | temp: 13.74,
2726 | feels_like: 10.05,
2727 | temp_min: 13.74,
2728 | temp_max: 13.74,
2729 | pressure: 1020,
2730 | sea_level: 1020,
2731 | grnd_level: 1014,
2732 | humidity: 42,
2733 | temp_kf: 0,
2734 | },
2735 | weather: [
2736 | {
2737 | id: 804,
2738 | main: 'Clouds',
2739 | description: 'overcast clouds',
2740 | icon: '04d',
2741 | },
2742 | ],
2743 | clouds: {
2744 | all: 99,
2745 | },
2746 | wind: {
2747 | speed: 2.66,
2748 | deg: 336,
2749 | },
2750 | sys: {
2751 | pod: 'd',
2752 | },
2753 | dt_txt: '2020-05-04 18:00:00',
2754 | },
2755 | {
2756 | dt: 1588626000,
2757 | main: {
2758 | temp: 10.68,
2759 | feels_like: 6.86,
2760 | temp_min: 10.68,
2761 | temp_max: 10.68,
2762 | pressure: 1021,
2763 | sea_level: 1021,
2764 | grnd_level: 1015,
2765 | humidity: 58,
2766 | temp_kf: 0,
2767 | },
2768 | weather: [
2769 | {
2770 | id: 804,
2771 | main: 'Clouds',
2772 | description: 'overcast clouds',
2773 | icon: '04n',
2774 | },
2775 | ],
2776 | clouds: {
2777 | all: 86,
2778 | },
2779 | wind: {
2780 | speed: 3.25,
2781 | deg: 33,
2782 | },
2783 | sys: {
2784 | pod: 'n',
2785 | },
2786 | dt_txt: '2020-05-04 21:00:00',
2787 | },
2788 | {
2789 | dt: 1588636800,
2790 | main: {
2791 | temp: 8.55,
2792 | feels_like: 4.79,
2793 | temp_min: 8.55,
2794 | temp_max: 8.55,
2795 | pressure: 1021,
2796 | sea_level: 1021,
2797 | grnd_level: 1016,
2798 | humidity: 65,
2799 | temp_kf: 0,
2800 | },
2801 | weather: [
2802 | {
2803 | id: 804,
2804 | main: 'Clouds',
2805 | description: 'overcast clouds',
2806 | icon: '04n',
2807 | },
2808 | ],
2809 | clouds: {
2810 | all: 86,
2811 | },
2812 | wind: {
2813 | speed: 3.07,
2814 | deg: 54,
2815 | },
2816 | sys: {
2817 | pod: 'n',
2818 | },
2819 | dt_txt: '2020-05-05 00:00:00',
2820 | },
2821 | {
2822 | dt: 1588647600,
2823 | main: {
2824 | temp: 6.96,
2825 | feels_like: 3.33,
2826 | temp_min: 6.96,
2827 | temp_max: 6.96,
2828 | pressure: 1021,
2829 | sea_level: 1021,
2830 | grnd_level: 1016,
2831 | humidity: 68,
2832 | temp_kf: 0,
2833 | },
2834 | weather: [
2835 | {
2836 | id: 800,
2837 | main: 'Clear',
2838 | description: 'clear sky',
2839 | icon: '01n',
2840 | },
2841 | ],
2842 | clouds: {
2843 | all: 10,
2844 | },
2845 | wind: {
2846 | speed: 2.67,
2847 | deg: 42,
2848 | },
2849 | sys: {
2850 | pod: 'n',
2851 | },
2852 | dt_txt: '2020-05-05 03:00:00',
2853 | },
2854 | {
2855 | dt: 1588658400,
2856 | main: {
2857 | temp: 9.07,
2858 | feels_like: 6.05,
2859 | temp_min: 9.07,
2860 | temp_max: 9.07,
2861 | pressure: 1022,
2862 | sea_level: 1022,
2863 | grnd_level: 1016,
2864 | humidity: 59,
2865 | temp_kf: 0,
2866 | },
2867 | weather: [
2868 | {
2869 | id: 800,
2870 | main: 'Clear',
2871 | description: 'clear sky',
2872 | icon: '01d',
2873 | },
2874 | ],
2875 | clouds: {
2876 | all: 5,
2877 | },
2878 | wind: {
2879 | speed: 1.81,
2880 | deg: 26,
2881 | },
2882 | sys: {
2883 | pod: 'd',
2884 | },
2885 | dt_txt: '2020-05-05 06:00:00',
2886 | },
2887 | {
2888 | dt: 1588669200,
2889 | main: {
2890 | temp: 13.73,
2891 | feels_like: 10.13,
2892 | temp_min: 13.73,
2893 | temp_max: 13.73,
2894 | pressure: 1022,
2895 | sea_level: 1022,
2896 | grnd_level: 1017,
2897 | humidity: 38,
2898 | temp_kf: 0,
2899 | },
2900 | weather: [
2901 | {
2902 | id: 800,
2903 | main: 'Clear',
2904 | description: 'clear sky',
2905 | icon: '01d',
2906 | },
2907 | ],
2908 | clouds: {
2909 | all: 0,
2910 | },
2911 | wind: {
2912 | speed: 2.23,
2913 | deg: 25,
2914 | },
2915 | sys: {
2916 | pod: 'd',
2917 | },
2918 | dt_txt: '2020-05-05 09:00:00',
2919 | },
2920 | ],
2921 | };
2922 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = {
3 | content: ['./index.html', './src/**/*.{js,jsx}'],
4 | darkMode: 'class', // or 'media' or 'class'
5 | theme: {
6 | extend: {},
7 | },
8 | variants: {
9 | extend: {},
10 | },
11 | plugins: [require('@tailwindcss/typography')],
12 | };
13 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turborepo.org/schema.json",
3 | "pipeline": {
4 | "build": {
5 | "outputs": ["build/**"]
6 | },
7 | "check": {
8 | "outputs": []
9 | },
10 | "lint": {
11 | "outputs": []
12 | },
13 | "coverage": {
14 | "dependsOn": [],
15 | "outputs": []
16 | },
17 | "test": {
18 | "dependsOn": [],
19 | "outputs": []
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/vite.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | export default defineConfig({
5 | build: {
6 | outDir: 'build',
7 | },
8 | plugins: [react()],
9 | });
10 |
--------------------------------------------------------------------------------