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() {
-
+
);
}
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 (
+
+ );
+}
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 && (
- {/* Login Dialog */}
-
+
);
}
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) {