├── 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 |
--------------------------------------------------------------------------------