diff --git a/src/App.jsx b/src/App.jsx index a6807a3..9b3b288 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import { motion } from "framer-motion"; +import { motion } from "motion/react"; import "./App.css"; import CallToAction from "./components/custom/CallToAction"; import FeaturesSection from "./components/custom/FeaturesSection"; diff --git a/src/components/custom/CallToAction.jsx b/src/components/custom/CallToAction.jsx index 369238a..5f6a1da 100644 --- a/src/components/custom/CallToAction.jsx +++ b/src/components/custom/CallToAction.jsx @@ -1,5 +1,5 @@ import { Button } from "@/components/ui/button"; -import { motion } from "framer-motion"; +import { motion } from "motion/react"; import { Link } from "react-router-dom"; const CallToAction = () => { diff --git a/src/components/custom/FeaturesSection.jsx b/src/components/custom/FeaturesSection.jsx index af7990f..d242f1a 100644 --- a/src/components/custom/FeaturesSection.jsx +++ b/src/components/custom/FeaturesSection.jsx @@ -1,4 +1,4 @@ -import { motion } from "framer-motion"; +import { motion } from "motion/react"; import { Calendar, Compass, Map, Shield, Smartphone } from "lucide-react"; diff --git a/src/components/custom/Header.jsx b/src/components/custom/Header.jsx index 737f016..b59524f 100644 --- a/src/components/custom/Header.jsx +++ b/src/components/custom/Header.jsx @@ -1,18 +1,12 @@ -import { googleLogout, useGoogleLogin } from "@react-oauth/google"; -import axios from "axios"; -import { AnimatePresence, motion } from "framer-motion"; +import { googleLogout } from "@react-oauth/google"; +import { AnimatePresence, motion } from "motion/react"; import { Menu } from "lucide-react"; import { useState } from "react"; -import { FcGoogle } from "react-icons/fc"; +import { Link } from "react-router-dom"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, -} from "@/components/ui/dialog"; +import LoginDialog from "@/components/custom/LoginDialog"; import { Popover, PopoverContent, @@ -25,6 +19,7 @@ import { SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; +import { useGoogleAuth } from "@/hooks/useGoogleAuth"; const fadeInUp = { initial: { opacity: 0, y: -20 }, @@ -43,40 +38,16 @@ export default function Header() { } }); const [openDialog, setOpenDialog] = useState(false); - const [loading, setLoading] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false); - const login = useGoogleLogin({ - onSuccess: (codeResp) => GetUserProfile(codeResp), - onError: (error) => console.log(error), + const { loading, login } = useGoogleAuth({ + onSuccess: (userData) => { + setUser(userData); + setOpenDialog(false); + toast.success("Successfully signed in!"); + }, }); - const GetUserProfile = (tokenInfo) => { - setLoading(true); - axios - .get( - `https://www.googleapis.com/oauth2/v1/userinfo?access_token=${tokenInfo?.access_token}`, - { - headers: { - Authorization: `Bearer ${tokenInfo?.access_token}`, - Accept: "Application/json", - }, - } - ) - .then((res) => { - localStorage.setItem("user", JSON.stringify(res.data)); - setUser(res.data); - setOpenDialog(false); - setLoading(false); - toast.success("Successfully signed in!"); - }) - .catch((error) => { - console.error("Error fetching user profile:", error); - setLoading(false); - toast.error("Failed to sign in. Please try again."); - }); - }; - const handleLogout = () => { googleLogout(); localStorage.removeItem("user"); @@ -86,28 +57,24 @@ export default function Header() { window.location.reload(); }; - // useEffect(() => { - // console.log("user logged in"); - // }, [user]); - const NavItems = () => ( <> - + - - + + - + ); @@ -119,7 +86,7 @@ export default function Header() { variants={fadeInUp} >
- + - + {user ? ( @@ -238,43 +205,12 @@ export default function Header() {
- - - -
-
-
- ✈️ -

- Wander AI -

-
- {/* Decorative Circles */} -
-
-
- - -
-

Welcome Back!

-

- Sign in to save your trips and access your personalized - itineraries across devices. -

-
- - -
- - -
+ ); } diff --git a/src/components/custom/Hero.jsx b/src/components/custom/Hero.jsx index e0c7800..7240660 100644 --- a/src/components/custom/Hero.jsx +++ b/src/components/custom/Hero.jsx @@ -1,5 +1,5 @@ import { Button } from "@/components/ui/button"; -import { motion } from "framer-motion"; +import { motion } from "motion/react"; import { Link } from "react-router-dom"; const Hero = () => { diff --git a/src/components/custom/HowItWorks.jsx b/src/components/custom/HowItWorks.jsx index e6b8157..63ee09e 100644 --- a/src/components/custom/HowItWorks.jsx +++ b/src/components/custom/HowItWorks.jsx @@ -1,4 +1,4 @@ -import { motion } from "framer-motion"; +import { motion } from "motion/react"; import { ArrowRight, Calendar, MapPin, Search, Sparkles } from "lucide-react"; const steps = [ diff --git a/src/components/custom/LoginDialog.jsx b/src/components/custom/LoginDialog.jsx new file mode 100644 index 0000000..4bfaa75 --- /dev/null +++ b/src/components/custom/LoginDialog.jsx @@ -0,0 +1,49 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, +} from "@/components/ui/dialog"; +import { FcGoogle } from "react-icons/fc"; + +export default function LoginDialog({ open, onOpenChange, onLogin, loading }) { + return ( + + + +
+
+
+ ✈️ +

+ Wander AI +

+
+
+
+
+ + +
+

Welcome Back!

+

+ Sign in to save your trips and access your personalized + itineraries across devices. +

+
+ + +
+ + +
+ ); +} diff --git a/src/create-trip/index.jsx b/src/create-trip/index.jsx index bf1a20e..6caa79e 100644 --- a/src/create-trip/index.jsx +++ b/src/create-trip/index.jsx @@ -1,22 +1,16 @@ import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, -} from "@/components/ui/dialog"; +import LoginDialog from "@/components/custom/LoginDialog"; import { AI_Prompt, SelectBudgetOptions, SelectTravelsList, } from "@/constants/options"; +import { useGoogleAuth } from "@/hooks/useGoogleAuth"; import { chatSession } from "@/service/AIModal"; import { db } from "@/service/firebaseConfig"; -import { useGoogleLogin } from "@react-oauth/google"; -import axios from "axios"; import { doc, setDoc } from "firebase/firestore"; import { AnimatePresence, motion } from "motion/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import GooglePlacesAutocomplete from "react-google-places-autocomplete"; import { AiOutlineLoading3Quarters, @@ -30,7 +24,6 @@ import { FaUserFriends, FaWallet, } from "react-icons/fa"; -import { FcGoogle } from "react-icons/fc"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; @@ -64,20 +57,21 @@ const blobVariants = { }; export default function CreateTrip() { - const [user, setUser] = useState(JSON.parse(localStorage.getItem("user"))); const [place, setPlace] = useState(); - const [formData, setFormData] = useState({ - noOfDays: 3, - }); + const [formData, setFormData] = useState({ noOfDays: 3 }); const [openDialog, setOpenDialog] = useState(false); const [loading, setLoading] = useState(false); const navigate = useNavigate(); + const { loading: authLoading, login } = useGoogleAuth({ + onSuccess: () => { + setOpenDialog(false); + onGenerateTrip(); + }, + }); + const handleInputChange = (name, value) => { - setFormData({ - ...formData, - [name]: value, - }); + setFormData({ ...formData, [name]: value }); }; const handleDaysChange = (increment) => { @@ -88,33 +82,6 @@ export default function CreateTrip() { } }; - useEffect(() => { - console.log(formData); - }, [formData]); - - const login = useGoogleLogin({ - onSuccess: (codeResp) => GetUserProfile(codeResp), - onError: (error) => console.log(error), - }); - - const GetUserProfile = (tokenInfo) => { - axios - .get( - `https://www.googleapis.com/oauth2/v1/userinfo?access_token=${tokenInfo?.access_token}`, - { - headers: { - Authorization: `Bearer ${tokenInfo?.access_token}`, - Accept: "Application/json", - }, - } - ) - .then((res) => { - localStorage.setItem("user", JSON.stringify(res.data)); - setOpenDialog(false); - onGenerateTrip(); - }); - }; - const onGenerateTrip = async () => { const user = localStorage.getItem("user"); @@ -123,28 +90,21 @@ export default function CreateTrip() { return; } - if ( - (formData?.noOfDays > 10 && !formData?.location) || // Reasonable restriction - !formData?.budget || - !formData?.traveler - ) { + if (!formData?.location || !formData?.budget || !formData?.traveler) { toast.error("Please fill all details correctly!"); return; } setLoading(true); - const FINAL_PROMPT = AI_Prompt.replace( - "{location}", - formData?.location.label - ) + const FINAL_PROMPT = AI_Prompt + .replace("{location}", formData?.location.label) .replace("{totalDays}", formData?.noOfDays) .replace("{traveler}", formData?.traveler) - .replace("{budget}", formData?.budget) - .replace("{totalDays}", formData?.noOfDays); + .replace("{budget}", formData?.budget); try { const result = await chatSession.sendMessage(FINAL_PROMPT); - SaveAiTrip(result?.response?.text()); + await SaveAiTrip(result?.response?.text()); } catch (e) { setLoading(false); toast.error("Failed to generate plan. Please try again."); @@ -162,9 +122,9 @@ export default function CreateTrip() { id: docId, }); navigate("/view-trip/" + docId); - window.location.reload(); } catch (e) { console.error("Error saving trip", e); + toast.error("Failed to save trip. Please try again."); } finally { setLoading(false); } @@ -364,7 +324,6 @@ export default function CreateTrip() { : "bg-white/5 border-white/5 hover:border-purple-500/30 hover:bg-white/10" }`} > - {/* Selection Checkmark */} {formData?.budget === item.title && ( - {/* Selection Checkmark */} {formData?.traveler === item.people && ( - - - - + ); } diff --git a/src/hooks/useGoogleAuth.js b/src/hooks/useGoogleAuth.js new file mode 100644 index 0000000..bf0df47 --- /dev/null +++ b/src/hooks/useGoogleAuth.js @@ -0,0 +1,39 @@ +import { useGoogleLogin } from "@react-oauth/google"; +import axios from "axios"; +import { useState } from "react"; +import { toast } from "sonner"; + +export function useGoogleAuth({ onSuccess } = {}) { + const [loading, setLoading] = useState(false); + + const fetchUserProfile = (tokenInfo) => { + setLoading(true); + axios + .get( + `https://www.googleapis.com/oauth2/v1/userinfo?access_token=${tokenInfo?.access_token}`, + { + headers: { + Authorization: `Bearer ${tokenInfo?.access_token}`, + Accept: "Application/json", + }, + } + ) + .then((res) => { + localStorage.setItem("user", JSON.stringify(res.data)); + setLoading(false); + onSuccess?.(res.data); + }) + .catch((error) => { + console.error("Error fetching user profile:", error); + setLoading(false); + toast.error("Failed to sign in. Please try again."); + }); + }; + + const login = useGoogleLogin({ + onSuccess: (codeResp) => fetchUserProfile(codeResp), + onError: (error) => console.error(error), + }); + + return { loading, login }; +} diff --git a/src/main.jsx b/src/main.jsx index b4c45f8..99ee15e 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -2,7 +2,7 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import "./index.css"; import App from "./App.jsx"; -import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { createBrowserRouter, Outlet, RouterProvider } from "react-router-dom"; import CreateTrip from "./create-trip"; import Header from "./components/custom/Header"; import { Toaster } from "./components/ui/sonner"; @@ -11,38 +11,49 @@ import ViewTrip from "./view-trip/[tripId]"; import MyTrips from "./my-trips"; import ProtectedRoute from "./components/custom/ProtectedRoute"; +const Layout = () => ( + <> +
+ + + +); + const router = createBrowserRouter([ { - path: "/", - element: , - }, - { - path: "/create-trip", - element: , - }, - { - path: "/view-trip/:tripId", - element: ( - - - - ), - }, - { - path: "/my-trips", - element: ( - - - - ), + element: , + children: [ + { + path: "/", + element: , + }, + { + path: "/create-trip", + element: , + }, + { + path: "/view-trip/:tripId", + element: ( + + + + ), + }, + { + path: "/my-trips", + element: ( + + + + ), + }, + ], }, ]); createRoot(document.getElementById("root")).render( -
- diff --git a/src/my-trips/components/UserTripCard.jsx b/src/my-trips/components/UserTripCard.jsx index 212abe6..69e0cfb 100644 --- a/src/my-trips/components/UserTripCard.jsx +++ b/src/my-trips/components/UserTripCard.jsx @@ -1,12 +1,8 @@ -import { GetPlaceDetails } from "@/service/GlobalApi"; +import { GetPlaceDetails, PHOTO_REF_URL } from "@/service/GlobalApi"; import { motion } from "motion/react"; import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -const PHOTO_REF_URL = - "https://places.googleapis.com/v1/{NAME}/media?maxHeightPx=600&maxWidthPx=600&key=" + - import.meta.env.VITE_GOOGLE_MAP_API_KEY; - export default function UserTripCard({ trip }) { const [photoUrl, setPhotoUrl] = useState(); const [isLoading, setIsLoading] = useState(true); @@ -26,7 +22,7 @@ export default function UserTripCard({ trip }) { const res = await GetPlaceDetails(data); const PhotoUrl = PHOTO_REF_URL.replace( "{NAME}", - res.data.places[0].photos[1].name + res.data.places[0]?.photos?.[0]?.name ); setPhotoUrl(PhotoUrl); } catch (error) { diff --git a/src/my-trips/index.jsx b/src/my-trips/index.jsx index dbb29d6..3ffb2cd 100644 --- a/src/my-trips/index.jsx +++ b/src/my-trips/index.jsx @@ -56,7 +56,7 @@ export default function MyTrips() { }); // Sort trips by newest first if possible, or just reverse to show latest added // Assuming no timestamp, just reversing order of addition usually flows better, or use sorting if field exists - setUserTrips(trips.reverse()); + setUserTrips([...trips].reverse()); } catch (error) { console.error("Error fetching trips:", error); } finally { diff --git a/src/service/GlobalApi.jsx b/src/service/GlobalApi.jsx index c41e74b..6f0b868 100644 --- a/src/service/GlobalApi.jsx +++ b/src/service/GlobalApi.jsx @@ -1,17 +1,26 @@ -import axios from "axios" +import axios from "axios"; -const BASE_URL = "https://places.googleapis.com/v1/places:searchText" +const BASE_URL = "https://places.googleapis.com/v1/places:searchText"; + +export const PHOTO_REF_URL = + "https://places.googleapis.com/v1/{NAME}/media?maxHeightPx=600&maxWidthPx=600&key=" + + import.meta.env.VITE_GOOGLE_MAP_API_KEY; const config = { - headers:{ - 'Content-Type' : 'application/json', - 'X-Goog-Api-Key':import.meta.env.VITE_GOOGLE_MAP_API_KEY, - 'X-Goog-FieldMask':[ - 'places.photos', - 'places.displayName', - 'places.id' - ] - } -} + headers: { + "Content-Type": "application/json", + "X-Goog-Api-Key": import.meta.env.VITE_GOOGLE_MAP_API_KEY, + "X-Goog-FieldMask": ["places.photos", "places.displayName", "places.id"], + }, +}; + +const photoCache = new Map(); -export const GetPlaceDetails = (data)=>axios.post(BASE_URL,data,config) \ No newline at end of file +export const GetPlaceDetails = (data) => { + const key = data.textQuery; + if (photoCache.has(key)) return Promise.resolve(photoCache.get(key)); + return axios.post(BASE_URL, data, config).then((res) => { + photoCache.set(key, res); + return res; + }); +}; diff --git a/src/view-trip/[tripId]/index.jsx b/src/view-trip/[tripId]/index.jsx index d0cedcd..3f07373 100644 --- a/src/view-trip/[tripId]/index.jsx +++ b/src/view-trip/[tripId]/index.jsx @@ -27,17 +27,20 @@ export default function ViewTrip() { const GetTripData = async () => { setLoading(true); - const docRef = doc(db, "AITrips", tripId); - const docSnap = await getDoc(docRef); - - if (docSnap.exists()) { - // console.log("Document: ", docSnap.data()); - setTrip(docSnap.data()); - } else { - // console.log("No such document"); - toast("No trip found"); + try { + const docRef = doc(db, "AITrips", tripId); + const docSnap = await getDoc(docRef); + if (docSnap.exists()) { + setTrip(docSnap.data()); + } else { + toast("No trip found"); + } + } catch (e) { + console.error("Error fetching trip", e); + toast.error("Failed to load trip. Please try again."); + } finally { + setLoading(false); } - setLoading(false); }; if (loading) { diff --git a/src/view-trip/components/HotelCardItem.jsx b/src/view-trip/components/HotelCardItem.jsx index 1eab84e..27831a6 100644 --- a/src/view-trip/components/HotelCardItem.jsx +++ b/src/view-trip/components/HotelCardItem.jsx @@ -1,12 +1,8 @@ -import { GetPlaceDetails } from "@/service/GlobalApi"; +import { GetPlaceDetails, PHOTO_REF_URL } from "@/service/GlobalApi"; import { motion } from "motion/react"; import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -const PHOTO_REF_URL = - "https://places.googleapis.com/v1/{NAME}/media?maxHeightPx=600&maxWidthPx=600&key=" + - import.meta.env.VITE_GOOGLE_MAP_API_KEY; - export default function HotelCardItem({ hotel }) { const [photoUrl, setPhotoUrl] = useState(); @@ -24,7 +20,7 @@ export default function HotelCardItem({ hotel }) { const res = await GetPlaceDetails(data); const PhotoUrl = PHOTO_REF_URL.replace( "{NAME}", - res.data.places[0].photos[1].name + res.data.places[0]?.photos?.[0]?.name ); setPhotoUrl(PhotoUrl); } catch (error) { diff --git a/src/view-trip/components/PlaceCardItem.jsx b/src/view-trip/components/PlaceCardItem.jsx index d9e7319..5fbe5ea 100644 --- a/src/view-trip/components/PlaceCardItem.jsx +++ b/src/view-trip/components/PlaceCardItem.jsx @@ -1,10 +1,7 @@ -import { GetPlaceDetails } from "@/service/GlobalApi"; +import { GetPlaceDetails, PHOTO_REF_URL } from "@/service/GlobalApi"; import { motion } from "motion/react"; import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; -const PHOTO_REF_URL = - "https://places.googleapis.com/v1/{NAME}/media?maxHeightPx=600&maxWidthPx=600&key=" + - import.meta.env.VITE_GOOGLE_MAP_API_KEY; export default function PlaceCardItem({ place }) { const [photoUrl, setPhotoUrl] = useState(); @@ -23,7 +20,7 @@ export default function PlaceCardItem({ place }) { const res = await GetPlaceDetails(data); const PhotoUrl = PHOTO_REF_URL.replace( "{NAME}", - res.data.places[0].photos[1].name + res.data.places[0]?.photos?.[0]?.name ); setPhotoUrl(PhotoUrl); } catch (error) {