├── .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 | App logo 3 |
4 | 5 | ![Landing page screenshot](./public/screenshot.png) 6 | 7 | [![build](https://github.com/denniskigen/react-weather/actions/workflows/ci.yml/badge.svg)](https://github.com/denniskigen/react-weather/actions/workflows/validate.yml) ![Deployment status](https://img.shields.io/github/deployments/denniskigen/react-weather/production?label=vercel&logo=vercel&logoColor=white) [![Coverage Status](https://coveralls.io/repos/github/denniskigen/react-weather/badge.svg?branch=main)](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 |
12 |
13 |

14 | About ReactWeather 15 |

16 |

17 | ReactWeather is a beautiful weather app built on top of the{' '} 18 | 24 | OpenWeatherMap API 25 | 26 | . 27 |

28 |

29 | It is a labor of ❤️ open-source project by me,{' '} 30 | 36 | Dennis Kigen 37 | 38 | , a software developer, writer and maker of cool stuff. 39 |

40 |

41 | It runs on{' '} 42 | 48 | React 49 | {' '} 50 | and{' '} 51 | 57 | TailwindCSS 58 | 59 | . It uses{' '} 60 | 66 | Erik Flowers' weather icons 67 | {' '} 68 | and is hosted on{' '} 69 | 75 | Vercel 76 | 77 | . If you like the project, please fork it on{' '} 78 | 84 | GitHub 85 | {' '} 86 | and leave a star! 87 |

88 |

89 | Thanks for swinging by. Enjoy the rest of your day! 90 |

91 |
92 |
93 |
94 | 99 | Buy Me A Coffee 104 | 105 |
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 |
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 | --------------------------------------------------------------------------------