├── src ├── App.css ├── vite-env.d.ts ├── index.css ├── App.tsx ├── main.tsx ├── types │ └── index.ts ├── UI │ ├── Spinner.tsx │ ├── DotsLoading.tsx │ ├── DotsLoading.css │ ├── Spinner.css │ └── weather.tsx └── store │ └── index.ts ├── postcss.config.js ├── tsconfig.json ├── vite.config.ts ├── tailwind.config.js ├── .gitignore ├── tsconfig.node.json ├── index.html ├── .eslintrc.cjs ├── tsconfig.app.json ├── package.json ├── README.md └── public └── vite.svg /src/App.css: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import Weather from "./UI/weather"; 3 | 4 | function App() { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | .env -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface WeatherData { 2 | name: string; 3 | main: { 4 | temp: number; 5 | feels_like: number; 6 | humidity: number; 7 | }; 8 | weather: { 9 | description: string; 10 | main: string; 11 | }[]; 12 | wind: { 13 | speed: number; 14 | }; 15 | sys: { 16 | sunrise: number; 17 | sunset: number; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "skipLibCheck": true, 6 | "module": "ESNext", 7 | "moduleResolution": "bundler", 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "noEmit": true 11 | }, 12 | "include": ["vite.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Weather App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/UI/Spinner.tsx: -------------------------------------------------------------------------------- 1 | // Spinner.js 2 | import React from "react"; 3 | import "./Spinner.css"; // Import the CSS file for keyframes 4 | 5 | const Spinner = () => { 6 | return ( 7 |
8 | {Array.from({ length: 12 }).map((_, index) => ( 9 |
13 | ))} 14 |
15 | ); 16 | }; 17 | 18 | export default Spinner; 19 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /src/UI/DotsLoading.tsx: -------------------------------------------------------------------------------- 1 | // DotsLoading.js 2 | import React from "react"; 3 | import "./DotsLoading.css"; // Import the CSS file for keyframes 4 | 5 | const DotsLoading = () => { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default DotsLoading; 18 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | export const weatherIconMap: { [key: string]: string } = { 2 | Thunderstorm: "ri-thunderstorms-line", 3 | Drizzle: "ri-drizzle-line", 4 | Rain: "ri-rainy-line", 5 | Snow: "ri-snowy-line", 6 | Mist: "ri-foggy-line", 7 | Smoke: "ri-smoke-line", 8 | Haze: "ri-haze-line", 9 | Dust: "ri-dust-line", 10 | Fog: "ri-foggy-line", 11 | Sand: "ri-sandstorm-line", 12 | Ash: "ri-volcano-line", 13 | Squall: "ri-windy-line", 14 | Tornado: "ri-tornado-line", 15 | Clear: "ri-sun-line", 16 | Clouds: "ri-cloudy-line", 17 | }; 18 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 8 | "module": "ESNext", 9 | "skipLibCheck": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "moduleDetection": "force", 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | 20 | /* Linting */ 21 | "strict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /src/UI/DotsLoading.css: -------------------------------------------------------------------------------- 1 | /* DotsLoading.css */ 2 | .dot { 3 | @apply h-5 w-5 mr-2 rounded-full bg-blue-200; 4 | animation: pulse 1.5s infinite ease-in-out; 5 | } 6 | 7 | .dot:last-child { 8 | @apply mr-0; 9 | } 10 | 11 | .dot:nth-child(1) { 12 | animation-delay: -0.3s; 13 | } 14 | 15 | .dot:nth-child(2) { 16 | animation-delay: -0.1s; 17 | } 18 | 19 | .dot:nth-child(3) { 20 | animation-delay: 0.1s; 21 | } 22 | 23 | @keyframes pulse { 24 | 0% { 25 | transform: scale(0.8); 26 | background-color: #b3d4fc; 27 | box-shadow: 0 0 0 0 rgba(178, 212, 252, 0.7); 28 | } 29 | 30 | 50% { 31 | transform: scale(1.2); 32 | background-color: #6793fb; 33 | box-shadow: 0 0 0 10px rgba(178, 212, 252, 0); 34 | } 35 | 36 | 100% { 37 | transform: scale(0.8); 38 | background-color: #b3d4fc; 39 | box-shadow: 0 0 0 0 rgba(178, 212, 252, 0.7); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weather-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@types/axios": "^0.14.0", 14 | "axios": "^1.7.2", 15 | "dotenv": "^16.4.5", 16 | "react": "^18.3.1", 17 | "react-dom": "^18.3.1", 18 | "remixicon": "^4.3.0" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.3.3", 22 | "@types/react-dom": "^18.3.0", 23 | "@typescript-eslint/eslint-plugin": "^7.13.1", 24 | "@typescript-eslint/parser": "^7.13.1", 25 | "@vitejs/plugin-react": "^4.3.1", 26 | "autoprefixer": "^10.4.19", 27 | "eslint": "^8.57.0", 28 | "eslint-plugin-react-hooks": "^4.6.2", 29 | "eslint-plugin-react-refresh": "^0.4.7", 30 | "postcss": "^8.4.39", 31 | "tailwindcss": "^3.4.4", 32 | "typescript": "^5.2.2", 33 | "vite": "^5.3.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default { 18 | // other rules... 19 | parserOptions: { 20 | ecmaVersion: 'latest', 21 | sourceType: 'module', 22 | project: ['./tsconfig.json', './tsconfig.node.json'], 23 | tsconfigRootDir: __dirname, 24 | }, 25 | } 26 | ``` 27 | 28 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 29 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 31 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/UI/Spinner.css: -------------------------------------------------------------------------------- 1 | /* Spinner.css */ 2 | .spinner { 3 | font-size: 28px; 4 | position: relative; 5 | display: inline-block; 6 | width: 1em; 7 | height: 1em; 8 | } 9 | 10 | .center { 11 | position: absolute; 12 | left: 0; 13 | right: 0; 14 | top: 0; 15 | bottom: 0; 16 | margin: auto; 17 | } 18 | 19 | .spinner .spinner-blade { 20 | position: absolute; 21 | left: 0.4629em; 22 | bottom: 0; 23 | width: 0.074em; 24 | height: 0.2777em; 25 | border-radius: 0.0555em; 26 | background-color: transparent; 27 | transform-origin: center -0.2222em; 28 | animation: spinner-fade9234 1s infinite linear; 29 | } 30 | 31 | .spinner .spinner-blade-1 { 32 | animation-delay: 0s; 33 | transform: rotate(0deg); 34 | } 35 | .spinner .spinner-blade-2 { 36 | animation-delay: 0.083s; 37 | transform: rotate(30deg); 38 | } 39 | .spinner .spinner-blade-3 { 40 | animation-delay: 0.166s; 41 | transform: rotate(60deg); 42 | } 43 | .spinner .spinner-blade-4 { 44 | animation-delay: 0.249s; 45 | transform: rotate(90deg); 46 | } 47 | .spinner .spinner-blade-5 { 48 | animation-delay: 0.332s; 49 | transform: rotate(120deg); 50 | } 51 | .spinner .spinner-blade-6 { 52 | animation-delay: 0.415s; 53 | transform: rotate(150deg); 54 | } 55 | .spinner .spinner-blade-7 { 56 | animation-delay: 0.498s; 57 | transform: rotate(180deg); 58 | } 59 | .spinner .spinner-blade-8 { 60 | animation-delay: 0.581s; 61 | transform: rotate(210deg); 62 | } 63 | .spinner .spinner-blade-9 { 64 | animation-delay: 0.664s; 65 | transform: rotate(240deg); 66 | } 67 | .spinner .spinner-blade-10 { 68 | animation-delay: 0.747s; 69 | transform: rotate(270deg); 70 | } 71 | .spinner .spinner-blade-11 { 72 | animation-delay: 0.83s; 73 | transform: rotate(300deg); 74 | } 75 | .spinner .spinner-blade-12 { 76 | animation-delay: 0.913s; 77 | transform: rotate(330deg); 78 | } 79 | 80 | @keyframes spinner-fade9234 { 81 | 0% { 82 | background-color: #69717d; 83 | } 84 | 100% { 85 | background-color: transparent; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/UI/weather.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | import "remixicon/fonts/remixicon.css"; 4 | import DotsLoading from "./DotsLoading"; 5 | import { WeatherData } from "../types"; 6 | import { weatherIconMap } from "../store"; 7 | 8 | const Weather: React.FC = () => { 9 | const [weather, setWeather] = useState(null); 10 | const [loading, setLoading] = useState(true); 11 | const [location, setLocation] = useState("Kolkata"); // Default location 12 | const [searchTerm, setSearchTerm] = useState(location); 13 | const apiKey = import.meta.env.VITE_API_KEY; 14 | 15 | const fetchWeather = async (loc: string) => { 16 | setLoading(true); 17 | try { 18 | const response = await axios.get( 19 | "https://api.openweathermap.org/data/2.5/weather", 20 | { 21 | params: { 22 | q: loc, 23 | appid: apiKey, 24 | units: "metric", 25 | }, 26 | } 27 | ); 28 | setWeather(response.data); 29 | } catch (error) { 30 | setWeather(null); // Clear previous weather data 31 | alert("Failed to fetch weather data. Please try again."); // Inform the user of the error 32 | } finally { 33 | setLoading(false); 34 | } 35 | }; 36 | 37 | useEffect(() => { 38 | fetchWeather(location); 39 | }, [location]); 40 | 41 | const handleSearch = () => { 42 | if (searchTerm.trim()) { 43 | setLocation(searchTerm); 44 | } else { 45 | alert("Please enter a valid location."); 46 | } 47 | }; 48 | 49 | const getWeatherIcon = (weatherMain: string) => { 50 | return weatherIconMap[weatherMain] || "ri-question-line"; 51 | }; 52 | 53 | const isDaytime = () => { 54 | if (!weather) return true; 55 | 56 | const currentTime = Math.floor(Date.now() / 1000); 57 | return ( 58 | currentTime >= weather.sys.sunrise && currentTime < weather.sys.sunset 59 | ); 60 | }; 61 | 62 | const formatTime = (timestamp: number) => { 63 | const date = new Date(timestamp * 1000); 64 | return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); 65 | }; 66 | 67 | return ( 68 |
75 |
76 | setSearchTerm(e.target.value)} 80 | placeholder="Enter location" 81 | className="outline-none flex-grow px-4 py-2 border rounded-l-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500" 82 | /> 83 | 89 |
90 |
91 |
92 | {loading ? ( 93 |
94 | 95 |
96 | ) : ( 97 | weather && ( 98 | <> 99 |
100 |

101 | {weather.name} 102 |

103 |
104 | 109 |

110 | {weather.weather[0].description.toUpperCase()} 111 |

112 |
113 |
114 |
115 |
116 | 117 |

118 | Temp: {weather.main.temp} °C 119 |

120 |
121 |
122 | 123 |

124 | Feels Like: {weather.main.feels_like} °C 125 |

126 |
127 |
128 | 129 |

130 | Humidity: {weather.main.humidity}% 131 |

132 |
133 |
134 | 135 |

136 | Wind Speed: {weather.wind.speed} m/s 137 |

138 |
139 |
140 | 141 |

142 | Sunrise: {formatTime(weather.sys.sunrise)} 143 |

144 |
145 |
146 | 147 |

148 | Sunset: {formatTime(weather.sys.sunset)} 149 |

150 |
151 |
152 | 153 | ) 154 | )} 155 |
156 |
157 | 160 |
161 | ); 162 | }; 163 | 164 | export default Weather; 165 | --------------------------------------------------------------------------------