diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c023163 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index bf61aa7..dcbba63 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /kubernetes/backend-config.yaml /kubernetes/tls-secret.yaml /secrets/*.pem -/secrets/*.sh \ No newline at end of file +/secrets/*.sh +.DS_Store \ No newline at end of file diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..c34aef0 --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": false, + "printWidth": 80, + "tabWidth": 2, + "endOfLine": "auto", + "trailingComma": "es5" +} diff --git a/backend/controller/authController.js b/backend/controller/authController.js index 3387476..1e905b3 100644 --- a/backend/controller/authController.js +++ b/backend/controller/authController.js @@ -267,6 +267,20 @@ exports.login = catchAsync(async (req, res, next) => { createSendToken(user, res); }); +// check if the user is logged in +exports.isLoggedIn = catchAsync(async (req, res, next) => { + if (req.user) { + res.status(200).json({ + status: "success", + data: { + message: "User is logged in", + }, + }); + } else { + return next(new AppError("Please log in to access this page", 401)); + } +}); + // middleware to protect routes exports.protect = catchAsync(async (req, res, next) => { let token; @@ -283,7 +297,12 @@ exports.protect = catchAsync(async (req, res, next) => { ); } - const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET); + let decoded; + try { + decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET); + } catch (error) { + return next(new AppError("Invalid token. Please log in again.", 401)); + } const currentUser = await User.findByPk(decoded.id); // check if the user is banned diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index 551f804..ec635e8 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -12,6 +12,8 @@ router.post("/signup-verify", authController.signUpVerify); router.post("/resend-code", authController.resendCode); // login route router.post("/login", authController.login); +// isLoggedin route +router.get("/is-loggedin", authController.protect, authController.isLoggedIn); //update notification settings route router diff --git a/frontend/public/bid-page.png b/frontend/public/bid-page.png new file mode 100644 index 0000000..dc0f586 Binary files /dev/null and b/frontend/public/bid-page.png differ diff --git a/frontend/public/main-page.png b/frontend/public/main-page.png new file mode 100644 index 0000000..f32ba54 Binary files /dev/null and b/frontend/public/main-page.png differ diff --git a/frontend/public/market-info.png b/frontend/public/market-info.png new file mode 100644 index 0000000..cbae01d Binary files /dev/null and b/frontend/public/market-info.png differ diff --git a/frontend/public/match-page.png b/frontend/public/match-page.png new file mode 100644 index 0000000..335efa0 Binary files /dev/null and b/frontend/public/match-page.png differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7bea46f..92a51e4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -13,6 +13,7 @@ import AdminPage from "./pages/AdminPage/AdminPage"; import Profile from "./pages/ProfilePage/Profile"; import PrivateRoute from "./PrivateRoute"; import { useTheme, ToggleThemeProvider } from "./context/ThemeContext"; +import LandingPage from "./pages/LandingPage/LandingPage"; function AppContent() { const { darkMode } = useTheme(); @@ -76,6 +77,7 @@ function AppContent() { /> } /> + } /> diff --git a/frontend/src/PrivateRoute.jsx b/frontend/src/PrivateRoute.jsx index 85a927e..aeb3af6 100644 --- a/frontend/src/PrivateRoute.jsx +++ b/frontend/src/PrivateRoute.jsx @@ -4,23 +4,57 @@ import { useAuth } from "./context/AuthContext"; import { useConfig } from "./context/ConfigContext"; import { useDispatch } from "react-redux"; import { fetchInitialData } from "./features/bidSlice"; +import axios from "axios"; const PrivateRoute = ({ children, adminPage = false }) => { - const { config, loading } = useConfig(); - const { isLoggedIn, isLoading, authToken, role } = useAuth(); + const { config, loading: configLoading } = useConfig(); + const { + isLoggedIn, + isLoading: authLoading, + authToken, + role, + logout, + } = useAuth(); const location = useLocation(); const dispatch = useDispatch(); useEffect(() => { - if (isLoggedIn && !isLoading && !loading) { - dispatch(fetchInitialData({ authToken, config })); - } - }, [isLoggedIn, isLoading, dispatch, authToken, config, loading]); - - if (!isLoggedIn && !isLoading && !loading) { - // Redirect to auth, but store original location for later - return ; - } else if (isLoggedIn && adminPage && role !== "admin" && !loading) { + const checkToken = async () => { + if (isLoggedIn && !authLoading && !configLoading) { + try { + await axios.get(`${config.REACT_APP_API_URL}/v1/users/is-loggedin`, { + headers: { Authorization: `Bearer ${authToken}` }, + }); + // Token is valid, fetch initial data + dispatch(fetchInitialData({ authToken, config })); + } catch (error) { + // Token is invalid, call logout + logout(); + } + } + }; + + checkToken(); + }, [ + isLoggedIn, + authLoading, + configLoading, + config, + authToken, + dispatch, + logout, + ]); + + if (authLoading || configLoading) { + return null; // Render nothing while loading + } + + if (!isLoggedIn) { + // Redirect to landing page, but store original location for later + return ; + } + + if (adminPage && role !== "admin") { return ; } diff --git a/frontend/src/pages/LandingPage/LandingPage.jsx b/frontend/src/pages/LandingPage/LandingPage.jsx new file mode 100644 index 0000000..eb70c31 --- /dev/null +++ b/frontend/src/pages/LandingPage/LandingPage.jsx @@ -0,0 +1,268 @@ +import React, { useState, useEffect } from "react"; +import { NavLink } from "react-router-dom"; +import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import { + CursorArrowRaysIcon, + ShieldCheckIcon, + ChartBarIcon, + CheckIcon, +} from "@heroicons/react/24/outline"; + +const contentSections = [ + { + title: "Real-Time Market Insights", + description: + "Stay informed with up-to-the-minute market data. Our intuitive dashboard provides a comprehensive overview of current meal point prices, recent transactions, and market trends.", + features: [ + "Live price updates", + "Historical price charts", + "Transaction volume indicators", + "Market recommendations", + ], + image: "/market-info.png", + }, + { + title: "Effortless Bidding Process", + description: + "Place your bid with ease and confidence. Our streamlined bidding page allows you to set your desired price and quantity, while providing helpful market guidance to ensure you make informed decisions.", + features: [ + "One-click bid placement", + "Market-based price suggestions", + "Flexible quantity options", + "Instant bid confirmation", + ], + image: "/bid-page.png", + }, + { + title: "Seamless Match Notifications", + description: + "Experience the joy of a perfect match! When our system finds an ideal buyer or seller for your bid, you'll receive instant email with all the details you need to complete your transaction.", + features: [ + "Real-time match alerts", + "Detailed transaction summaries", + "Easy transaction finalization", + ], + image: "/match-page.png", + }, +]; + +const LandingPage = () => { + const [scrolled, setScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + const isScrolled = window.scrollY > 50; + if (isScrolled !== scrolled) { + setScrolled(isScrolled); + } + }; + + window.addEventListener("scroll", handleScroll); + + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, [scrolled]); + + return ( +
+ + + {/* Hero Section */} +
+
+
+
+

+ Safe, Fair, and Easy + + Meal Point Exchange + + for WashU Students +

+

+ We've revolutionized meal point exchange for WashU students. Our + secure marketplace eliminates haggling and prevents scams, + making transactions effortless. +

+
+ + Get Started + +
+
+
+
+ The Bear Bazaar mockup +
+
+
+
+
+ + {/* Features Section */} +
+
+
+ {[ + { + title: "Automated Matching", + desc: "Say goodbye to endless haggling! Our algorithm instantly connects you with the best deal, saving you time and ensuring fair prices for everyone.", + icon: CursorArrowRaysIcon, + }, + { + title: "Secure Authentication", + desc: "Trade with confidence knowing you're part of a verified WashU community. Our email verification keeps your transactions safe and scam-free.", + icon: ShieldCheckIcon, + }, + { + title: "Transparent Transactions", + desc: "Make informed decisions with our real-time market insights. View anonymized transaction data to understand pricing trends and get the best value for your meal points.", + icon: ChartBarIcon, + }, + ].map((feature, index) => ( +
+
+
+
+

+ {feature.title} +

+
+

{feature.desc}

+
+ ))} +
+
+
+ + {/* Content Sections with Mockups */} + {contentSections.map((section, index) => ( +
+
+
+ {index % 2 === 1 ? ( + <> +
+
+ {`${section.title} +
+
+
+

+ {section.title} +

+

+ {section.description} +

+
    + {section.features.map((item, featureIndex) => ( +
  • + + +

    {item}

    +
  • + ))} +
+
+ + ) : ( + <> +
+

+ {section.title} +

+

+ {section.description} +

+
    + {section.features.map((item, featureIndex) => ( +
  • + + +

    {item}

    +
  • + ))} +
+
+
+
+ {`${section.title} +
+
+ + )} +
+
+
+ ))} + + {/* Footer */} + +
+ ); +}; + +export default LandingPage; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index d6dff43..dbd86f7 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -3,7 +3,11 @@ module.exports = { darkMode: false, content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { - extend: {}, + extend: { + colors: { + mainLight: "#a51417", + }, + }, }, plugins: [require("daisyui")], daisyui: {