diff --git a/.firebase/hosting.ZGlzdA.cache b/.firebase/hosting.ZGlzdA.cache new file mode 100644 index 0000000..09a102f --- /dev/null +++ b/.firebase/hosting.ZGlzdA.cache @@ -0,0 +1,6 @@ +weather-svgrepo-com.svg,1713467456584,b5ef9675393b62be946cf1635266f65b6e2520235befc8b8a7a6b43a7893e280 +index.html,1713471967482,06adbed1124a5e9c31b4297e219a967562d2f6c0306347848f68aa1c9d32ab5b +assets/index-BEli0i5a.css,1713471967482,9a675d403d0867e56dabe124867d89aa2d5354bb1f7532bc8d57795f5b9bc402 +assets/mountain-CPGhdAE3.jpg,1713471967482,833c74bff9365a206ad53364f7ffa8f6815c554a5891c98c266960f5f1cc1bc7 +assets/cloudy-sky-from-above-B2n94znt.jpg,1713471967482,721494ff5059154314a2df30b9abe4641d55082cb77bdab9a7017184500aa38e +assets/index-DCDJ4Ley.js,1713471967482,c2eceb6b33fe57c4f7e16e8094bb0b6784a955860982ebdda09ed9d43e7bfdef diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..63f4863 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "weatherapp-eric-muganga" + } +} diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml new file mode 100644 index 0000000..b20c748 --- /dev/null +++ b/.github/workflows/firebase-hosting-merge.yml @@ -0,0 +1,20 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on merge +on: + push: + branches: + - main +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_WEATHERAPP_ERIC_MUGANGA }} + channelId: live + projectId: weatherapp-eric-muganga diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml new file mode 100644 index 0000000..157326d --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -0,0 +1,21 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on PR +on: pull_request +permissions: + checks: write + contents: read + pull-requests: write +jobs: + build_and_preview: + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm run build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_WEATHERAPP_ERIC_MUGANGA }} + projectId: weatherapp-eric-muganga diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..2c33c29 --- /dev/null +++ b/firebase.json @@ -0,0 +1,16 @@ +{ + "hosting": { + "public": "dist", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/package-lock.json b/package-lock.json index 760e7c9..39f1128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,11 @@ "react-dom": "^18.2.0", "react-icons": "^5.0.1", "react-redux": "^9.1.1", - "react-router-dom": "^6.22.3" + "react-responsive-carousel": "^3.2.23", + "react-router-dom": "^6.22.3", + "react-select": "^5.8.0", + "react-slick": "^0.30.2", + "slick-carousel": "^1.8.1" }, "devDependencies": { "@types/react": "^18.2.66", @@ -2637,14 +2641,6 @@ "url": "https://github.com/sponsors/kossnocorp" } }, - "node_modules/date-fns-tz": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.1.3.tgz", - "integrity": "sha512-ZfbMu+nbzW0mEzC8VZrLiSWvUIaI3aRHeq33mTe7Y38UctKukgqPR4nTDwcwS4d64Gf8GghnVsroBuMY3eiTeA==", - "peerDependencies": { - "date-fns": "^3.0.0" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2769,6 +2765,11 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/enquire.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/enquire.js/-/enquire.js-2.1.6.tgz", + "integrity": "sha512-/KujNpO+PT63F7Hlpu4h3pE3TokKRHN26JYmQpPyjkRD/N57R7bPDNojMXdi7uveAKjYB7yQnartCxZnFWr0Xw==" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -4177,6 +4178,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "peer": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4217,6 +4224,14 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4283,6 +4298,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4314,6 +4334,11 @@ "resolved": "https://registry.npmjs.org/material-ripple-effects/-/material-ripple-effects-2.0.1.tgz", "integrity": "sha512-hHlUkZAuXbP94lu02VgrPidbZ3hBtgXBtjlwR8APNqOIgDZMV8MCIcsclL8FmGJQHvnORyvoQgC965vPsiyXLQ==" }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4997,6 +5022,17 @@ "react": "^18.2.0" } }, + "node_modules/react-easy-swipe": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/react-easy-swipe/-/react-easy-swipe-0.0.21.tgz", + "integrity": "sha512-OeR2jAxdoqUMHIn/nS9fgreI5hSpgGoL5ezdal4+oO7YSSgJR8ga+PkYGJrSrJ9MKlPcQjMQXnketrD7WNmNsg==", + "dependencies": { + "prop-types": "^15.5.8" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/react-icons": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.0.1.tgz", @@ -5036,6 +5072,16 @@ } } }, + "node_modules/react-responsive-carousel": { + "version": "3.2.23", + "resolved": "https://registry.npmjs.org/react-responsive-carousel/-/react-responsive-carousel-3.2.23.tgz", + "integrity": "sha512-pqJLsBaKHWJhw/ItODgbVoziR2z4lpcJg+YwmRlSk4rKH32VE633mAtZZ9kDXjy4wFO+pgUZmDKPsPe1fPmHCg==", + "dependencies": { + "classnames": "^2.2.5", + "prop-types": "^15.5.8", + "react-easy-swipe": "^0.0.21" + } + }, "node_modules/react-router": { "version": "6.22.3", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz", @@ -5066,6 +5112,42 @@ "react-dom": ">=16.8" } }, + "node_modules/react-select": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.8.0.tgz", + "integrity": "sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-slick": { + "version": "0.30.2", + "resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.30.2.tgz", + "integrity": "sha512-XvQJi7mRHuiU3b9irsqS9SGIgftIfdV5/tNcURTb5LdIokRA5kIIx3l4rlq2XYHfxcSntXapoRg/GxaVOM1yfg==", + "dependencies": { + "classnames": "^2.2.5", + "enquire.js": "^2.1.6", + "json2mq": "^0.2.0", + "lodash.debounce": "^4.0.8", + "resize-observer-polyfill": "^1.5.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -5164,6 +5246,11 @@ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz", "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -5406,6 +5493,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slick-carousel": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz", + "integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA==", + "peerDependencies": { + "jquery": ">=1.8.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -5423,6 +5518,11 @@ "node": ">=0.10.0" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5978,6 +6078,19 @@ "punycode": "^2.1.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/package.json b/package.json index e558db3..dfecb33 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,11 @@ "react-dom": "^18.2.0", "react-icons": "^5.0.1", "react-redux": "^9.1.1", - "react-router-dom": "^6.22.3" + "react-responsive-carousel": "^3.2.23", + "react-router-dom": "^6.22.3", + "react-select": "^5.8.0", + "react-slick": "^0.30.2", + "slick-carousel": "^1.8.1" }, "devDependencies": { "@types/react": "^18.2.66", diff --git a/src/assets/react.svg b/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/AddCityDrawer.jsx b/src/components/AddCityDrawer.jsx index 6a158ca..3463d7e 100644 --- a/src/components/AddCityDrawer.jsx +++ b/src/components/AddCityDrawer.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/prop-types */ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import { useDispatch } from "react-redux"; import axios from "axios"; @@ -22,73 +22,110 @@ const fetchCountries = async () => { const { data } = await axios.get( "https://countriesnow.space/api/v0.1/countries" ); - + //console.log(data.data); return data.data; }; +const fetchUSStates = async () => { + const response = await axios.post( + "https://countriesnow.space/api/v0.1/countries/states", + { + country: "United States", + } + ); + //console.log(response.data); + return response.data.data.states; +}; + const AddCityDrawer = ({ isOpen, onClose }) => { const dispatch = useDispatch(); + + const [selected, setSelected] = useState({ country: "", city: "" }); + const [selectedState, setSelectedState] = useState(""); + const [openSnackbar, setOpenSnackbar] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); + const [snackbarSeverity, setSnackbarSeverity] = useState("info"); + const { data: countries, isLoading, isError, + error, } = useQuery({ queryKey: ["countries"], queryFn: fetchCountries, }); - const [selected, setSelected] = useState({ country: "", city: "" }); - const [openSnackbar, setOpenSnackbar] = useState(false); - const [snackbarMessage, setSnackbarMessage] = useState(""); - const [snackbarSeverity, setSnackbarSeverity] = useState("info"); + const { + data: states = [], + isFetching: isFetchingStates, + isError: isErrorStates, + error: statesError, + } = useQuery({ + queryKey: ["USStates"], + queryFn: fetchUSStates, + enabled: selected.country === "US", // Execute only if the selected country is 'US' + }); - // if (loading) { - // return

Loading...

; - // } + useEffect(() => { + if (selected.country !== "US") { + setSelectedState(""); + } + }, [selected.country]); const handleInputChange = (event) => { - let { name, value } = event.target || {}; - - setSelected((prev) => ({ - ...prev, - [name]: value, - })); - - //console.log(`${name} selected:`, value); + const { name, value } = event.target; + if (selected[name] !== value) { + // Only update if different + setSelected((prev) => ({ + ...prev, + [name]: value, + })); + } }; const handleSubmit = async (event) => { event.preventDefault(); - if (!selected.country || !selected.city) { + if (!selected.country && (!selected.city || !selectedState)) { setSnackbarSeverity("error"); setSnackbarMessage("Please fill the city and the country field"); setOpenSnackbar(true); return; } - // Simulate adding a city + let location = selected.city; + if (selected.country === "US" && selectedState) { + location = selectedState; // Use the state name if a state is selected in the US + } + + // Simulating adding a city try { - //console.log(`City ${selected.city} added successfully`); - dispatch(fetchWeatherDataForCity(selected.city)); + await dispatch(fetchWeatherDataForCity(location)); setSelected({ country: "", city: "" }); // Reset form + setSelectedState(""); setSnackbarSeverity("success"); - setSnackbarMessage(`City ${selected.city} added successfully`); + setSnackbarMessage(`City ${selected.city} added successfully.`); setOpenSnackbar(true); - onClose(); // Close drawer on success + setTimeout(() => { + onClose(); // Close drawer on success + }, 800); } catch (error) { setSnackbarSeverity("error"); setSnackbarMessage( - "Occurred an error while adding the city. Please try again." + "An error occurred while adding the city. Please try again." ); setOpenSnackbar(true); } }; if (isLoading) return ; - if (isError) { - setSnackbarSeverity("error"); - setSnackbarMessage("Failed to load countries data."); - setOpenSnackbar(true); + if (isError) return {error.message}; + if (isErrorStates) { + return ( + + Failed to load states: {statesError.message} + + ); } return ( @@ -122,7 +159,7 @@ const AddCityDrawer = ({ isOpen, onClose }) => { name="country" value={selected.country} onChange={handleInputChange} - className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-2 border-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md" + className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-2 border-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm lg:text-base rounded-md" > {countries?.map((country) => ( @@ -133,31 +170,60 @@ const AddCityDrawer = ({ isOpen, onClose }) => { - - - - + {selected.country === "US" ? ( + + + + + ) : ( + + + + + )}