From 8f8d0e6df60cccb4fdc9ee24f99b60f21e27eaae Mon Sep 17 00:00:00 2001 From: Anya Zhang Date: Sun, 26 May 2024 22:08:16 -0700 Subject: [PATCH 01/36] tried to save user token but it resets after new page opens --- app/locations/[location]/page.tsx | 7 +++++-- app/profile/page.tsx | 13 +++++-------- app/token_manager.ts | 18 ++++++++++++++++++ components/login.tsx | 6 ++++-- db.sqlite3 | Bin 143360 -> 143360 bytes 5 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 app/token_manager.ts diff --git a/app/locations/[location]/page.tsx b/app/locations/[location]/page.tsx index a7c87b9..582dcfb 100644 --- a/app/locations/[location]/page.tsx +++ b/app/locations/[location]/page.tsx @@ -2,7 +2,7 @@ import { fetchLocations, fetchFoodReviewsBulk } from "@/app/db"; import { Location } from "@/interfaces/Location"; import { FrontEndReviews } from "@/interfaces/Review"; - +import { getToken } from "@/app/token_manager"; import LocationCategories from "@/components/location/categories"; import Link from "next/link"; @@ -24,9 +24,12 @@ export default async function Page({ ) ); + const token = getToken(); + const userToken = token ? token : "anonymous"; // Use "anonymous" if token is null + const foodReviews: FrontEndReviews = await fetchFoodReviewsBulk({ food_names: food_names, - user_id: "anonymous", + user_id: userToken, }); return ( diff --git a/app/profile/page.tsx b/app/profile/page.tsx index 18b8ef4..0198698 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -2,6 +2,7 @@ import React from "react"; import { useEffect, useState } from "react"; import { googleLogout } from "@react-oauth/google"; +import { removeToken } from "@/app/token_manager"; import axios from "axios"; interface User { name: string; @@ -43,12 +44,13 @@ const Page = () => { const handleLogout = () => { googleLogout(); axios - .post("http://localhost:8000/api/logout/") + .post("http://localhost:8000/myapi/logout/") .then((res) => console.log("Backend logout successful", res)) .catch((err) => console.error("Backend logout failed", err)); - // Remove the token from local storage + // Remove the stored user login token sessionStorage.removeItem("token"); + removeToken(); // Redirect the user to the main page after logging out window.location.href = "/"; console.log("Logged out successfully"); @@ -59,12 +61,7 @@ const Page = () => {

Profile

{user && (
- User profile + User profile

Welcome, {user.name} - {user.email}

diff --git a/app/token_manager.ts b/app/token_manager.ts new file mode 100644 index 0000000..367e88d --- /dev/null +++ b/app/token_manager.ts @@ -0,0 +1,18 @@ +// Manage the user's login token +let token: string | null = null; + +export const setToken = (newToken: string) => { + token = newToken; +}; + +export const getToken = (): string | null => { + return token; +}; + +export const isTokenNull = (): boolean => { + return token === null; +}; + +export const removeToken = () => { + token = null; +}; diff --git a/components/login.tsx b/components/login.tsx index e750098..20cc59f 100644 --- a/components/login.tsx +++ b/components/login.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { GoogleOAuthProvider, useGoogleLogin } from "@react-oauth/google"; import axios from "axios"; - +import { setToken, isTokenNull } from "@/app/token_manager"; import { GOOGLE_CLIENT_ID } from "@/private/secrets"; interface User { @@ -30,8 +30,10 @@ const LoginComponent = () => { onSuccess: (tokenResponse) => { console.log(tokenResponse); - // Store authentication token in the browser's storage for navigation bar use + // Store authentication token in 2 places sessionStorage.setItem("token", tokenResponse.access_token); + setToken(tokenResponse.access_token); + // Redirect the user to main page window.location.href = "/"; //handleLoginSuccess diff --git a/db.sqlite3 b/db.sqlite3 index d77d92414e1dd260b52de06cb56f5efd89097e05..9f992f5f98c023f3fc0a0f2152282720527f28cf 100644 GIT binary patch delta 1635 zcmai!O=#O@9L8gZ#FD>4VRR$OO36map)s$dSJEqGFl@*9uw%!y6UT8OL5gjC*s>(c zvMoVK#SUXI_Q9YzHtbXwj0FZc7s?JBI~4|l9C{rLHU>M4jxjqMC8@LJ{C{5lKc4q_ z{tupG51wP6%=mUbolQ>cjK#LoC)MLerNw|d7XC8&W8{a>>r+}p3O~cY4Y>II=*OXV zuqb{bdOPxAR1NM%|B58A&qG3ZI`9+D1b@RQ{Flg^kr$yau%EH=)N1&H;P+E!(XT_l zV+Vn!pXH8kO$HtXUz>XC#QSsd@a^%ekC6=BF`Q0aCs|t)5d}MDtz{Ds*4wh`T8l%G zODj#9b9HmS(PM$e8IIbr-Bu6mH)~Z@68WLXkQB|;J6exb5R|o=-9mb^4K&5k+g;sm zazl}1HoM)fO3DP(4<)kS>bH!T`kl5GlX2MtxIsjF1g)j5Dvmg&igsUf%OkTkZf{jk={FWO7|#kR4I71PjrEVHj3d zb0>72Ht6>b9-kgO_PxI{zKES21%CA@QT!p^!^=2_Ph&5zvlDL@JG?a>_mO~v3xHW5 zz-<6|3i1RKV-ZEtXmNB*BA#MnGzAHQ;IAALTnv#6K|=0|G2|f;V+ll(2o1)&uPi4A z%VCT}3=IG~`kY{RYWSRlgAJo&!14qULl99cNe{9aq4#GTnNc4utre)Owo zB%+3&gl9u$@SETa-o?Jd?woj&*t|ElIFa>=@#~Uvy5Gbw| zZCYZlGepFMekJ8Voo9@TQN7RYCbIyLL7{c zyo`=FkIwD5SgWgAH9|79!xs6(ve4MSCwKBqH(%SXvXzvz1lx&1fy-7p7gLPMEQ=DAK&9F$ nUCV9em-1c8c7W1$vz1Clpo>K}EjIenWs-~4K7Df6|L*wT?pxvw delta 174 zcmV;f08#&d;0S==2#^~AQjr`(1yTSmc8alNpr1-K2z>wyt`6A^)(aO8Z4GA((+SE6 zTMmy4JP8pFAPtNTY6@Ns{tZG1q6=XR8VNQE@(J$^9S!>ns|f55NDPSz%@5oTvqVOaK4? From 42eca1180fb684839142292c6a2f020ede8fac4a Mon Sep 17 00:00:00 2001 From: Noahkmm Date: Mon, 27 May 2024 17:02:36 -0700 Subject: [PATCH 02/36] made a so so ratings page for all ratings --- app/foods/[food]/page.tsx | 11 +++++++---- components/food/ratings.tsx | 29 ++++++++++++++++++++++------- components/location/food.tsx | 1 + db.sqlite3 | Bin 143360 -> 143360 bytes interfaces/Food.ts | 11 ++++++++--- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/app/foods/[food]/page.tsx b/app/foods/[food]/page.tsx index 6113808..9876ff8 100644 --- a/app/foods/[food]/page.tsx +++ b/app/foods/[food]/page.tsx @@ -28,9 +28,9 @@ export default function Page({ params }: { params: { food: string } }) { >, i, ) => [ - category as string, - () as JSX.Element, - ], + category as string, + () as JSX.Element, + ], ); useEffect(() => { @@ -53,7 +53,10 @@ export default function Page({ params }: { params: { food: string } }) { return (
-
    +

    + {food.name} +

    +
      {tabs.map(([category, _]: Array, i) => (
    • + + +
+ +
Image 1 + {food.images.map((image, index) => ( + Uh oh - 404 + + ))} +
); } From 8d14a7bab8d8a1060c56e0d3781a0bfead50cca8 Mon Sep 17 00:00:00 2001 From: Anya Zhang Date: Fri, 31 May 2024 11:58:43 -0700 Subject: [PATCH 05/36] merge w main --- README.md | 2 +- backend/api/model_logic/locations/tests.py | 125 +++++++++++++++++++++ backend/api/model_logic/tasks/tests.py | 88 +++++++++++++++ backend/api/model_logic/users/tests.py | 77 +++++++++++++ db.sqlite3 | Bin 143360 -> 143360 bytes 5 files changed, 291 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5711345..0699dc3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next ## Running Unit Tests -`python backend/manage.py test backend/` +`python backend/manage.py test backend/api --shuffle` ## Formatting Code diff --git a/backend/api/model_logic/locations/tests.py b/backend/api/model_logic/locations/tests.py index e69de29..c024bce 100644 --- a/backend/api/model_logic/locations/tests.py +++ b/backend/api/model_logic/locations/tests.py @@ -0,0 +1,125 @@ +from django.test import TestCase +from .actions import * + + +class LocationsTestCase(TestCase): + def test_set_get_location(self): + location = { + "name": "test_set_get_location", + "categories": [ + { + "name": "category1", + "sub_categories": [ + { + "name": "sub_category1", + "foods": [ + { + "name": "food1", + "restrictions": ["restriction1"], + }, + ], + } + ], + } + ], + } + + # delete location from db if exists + delete_location(location["name"]) + + # set location + set_location(location) + + # check location + location = get_location(location["name"]) + if location is None: + self.fail("Failed to set or get location") + + # delete the location from db + delete_location(location["name"]) + + def test_update_location(self): + location = { + "name": "test_update_location", + "categories": [ + { + "name": "category1", + "sub_categories": [ + { + "name": "sub_category1", + "foods": [ + { + "name": "food1", + "restrictions": ["restriction1"], + }, + ], + } + ], + } + ], + } + + # delete location from db if exists + delete_location(location["name"]) + + # set location + set_location(location) + + # check location + location = get_location(location["name"]) + if location is None: + self.fail("Failed to set or get location") + + # update location + location["categories"][0]["sub_categories"][0]["foods"][0][ + "restrictions" + ].append("restriction2") + update_location(location["name"], location) + + # check location + location = get_location(location["name"]) + if location is None: + self.fail("Failed to update location") + + self.assertEquals( + location["categories"][0]["sub_categories"][0]["foods"][0]["restrictions"], + ["restriction1", "restriction2"], + ) + + # delete the location from db + delete_location(location["name"]) + + def test_delete_location(self): + location = { + "name": "test_delete_location", + "categories": [ + { + "name": "category1", + "sub_categories": [ + { + "name": "sub_category1", + "foods": [ + { + "name": "food1", + "restrictions": ["restriction1"], + }, + ], + } + ], + } + ], + } + + # delete location from db if exists + delete_location(location["name"]) + + # set location + set_location(location) + + # delete location + delete_location(location["name"]) + + # check location + location = get_location(location["name"]) + + self.assertIsNone(location) diff --git a/backend/api/model_logic/tasks/tests.py b/backend/api/model_logic/tasks/tests.py index e69de29..78d27dd 100644 --- a/backend/api/model_logic/tasks/tests.py +++ b/backend/api/model_logic/tasks/tests.py @@ -0,0 +1,88 @@ +from django.test import TestCase +from .actions import * + +from utils import str_to_datetime + + +class TasksTestCase(TestCase): + def test_set_get_task(self): + task_name = "test_set_get_task" + + # delete task from db if exists + delete_task(task_name) + + # set task + task = set_task(task_name) + + # check task + if task is None: + self.fail("Failed to set task") + + # get task + task = get_task(task_name) + + # check task + if task is None: + self.fail("Failed to get task") + + # delete the task from db + delete_task(task_name) + + def test_update_task(self): + task_name = "test_update_task" + + # delete task from db if exists + delete_task(task_name) + + # set task + task = set_task(task_name) + + time_before = task["last_update"] + + # check task + if task is None: + self.fail("Failed to set task") + + # update task + update_task(task_name) + + # get task + task = get_task(task_name) + + # check task + if task is None: + self.fail("Failed to update task") + + time_after = task["last_update"] + + # compare times + time_before = str_to_datetime(time_before) + time_after = str_to_datetime(time_after) + + # check if the time has been updated + self.assertTrue(time_after > time_before) + + # delete the task from db + delete_task(task_name) + + def test_delete_task(self): + task_name = "test_delete_task" + + # delete task from db if exists + delete_task(task_name) + + # set task + task = set_task(task_name) + + # check task + if task is None: + self.fail("Failed to set task") + + # delete task + delete_task(task_name) + + # get task + task = get_task(task_name) + + # check task + self.assertIsNone(task) diff --git a/backend/api/model_logic/users/tests.py b/backend/api/model_logic/users/tests.py index e69de29..a9a4306 100644 --- a/backend/api/model_logic/users/tests.py +++ b/backend/api/model_logic/users/tests.py @@ -0,0 +1,77 @@ +from os import name +from django.test import TestCase +from .actions import * + + +class UsersModelTestCase(TestCase): + def test_set_get_user(self): + user_id = "12345" + name = "test_set_get_user" + # delete user from db if exists + delete_user(user_id) + + # set user + set_user(name, user_id) + + # get user + user = get_user(user_id) + + # check user + if user is None: + self.fail("Failed to set or get user") + + self.assertEquals(user["name"], name) + + # delete the user from db + delete_user(user_id) + + def test_update_user(self): + user_id = "12345" + name = "test_update_user" + # delete user from db if exists + delete_user(user_id) + + # set user + set_user(name, user_id) + + # check user + user = get_user(user_id) + if user is None: + self.fail("Failed to set user") + + # update user + new_name = "new_name" + update_user(user_id, new_name) + + # get user + user = get_user(user_id) + + # check user + if user is None: + self.fail("Failed to update user") + + self.assertEquals(user["name"], new_name) + + # delete the user from db + delete_user(user_id) + + def test_delete_user(self): + user_id = "12345" + name = "test_delete_user" + # delete user from db if exists + delete_user(user_id) + + # set user + set_user(name, user_id) + + # check user + user = get_user(user_id) + if user is None: + self.fail("Failed to set user") + + # delete user + delete_user(user_id) + + # check user + user = get_user(user_id) + self.assertIsNone(user) diff --git a/db.sqlite3 b/db.sqlite3 index 01261e4d080b25efc7531636a911611cfbf16ea1..ed4596d50c3572cb9ab0a4520bdc96bac4a28347 100644 GIT binary patch delta 755 zcmZp8z|ru4V}dke%tRSy#+Z!>OXf4%v2#ymUm(xK9y(cVfr1n}H@^o@C)Zj2JA7NX zzp@|UsO4MRV8k?On?mt2~(PIO?af ze_$+UnXaLc zf{~$>v4NGbsh)+Yk+GS%Ev7V3g^`J#nSqIsxrq%(diq8UCaLYeelT*WEAg|bGVn+9 zZQ$eO&Eh%8BgS3Gb&N}zvy9_3hZ=h|+XXh&jg9|V)f+YJSbZDg9SH>1^ow##R`t$d znTb}GmZfGcl@XbxA(@$_Dc-@AhQ|51{w2nh#mNQkxkTPHl=$umS?(XOon4ILD z=j-EBmR;ym<(inAm{yQ);Zxx2>I!!RMvyjY*nk~DD0(p+;pH5dm29Q!VxCcCQEq5Z x8R={p?jG-7mY!%*WLD&uS(<2C9B=667hGat5tdV)8H(x%wNq@M#j2YQnSpdXVcpdc0xNDPSz%@5oTv Date: Fri, 31 May 2024 13:01:56 -0700 Subject: [PATCH 06/36] username update --- app/locations/[location]/page.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/locations/[location]/page.tsx b/app/locations/[location]/page.tsx index bea3c13..7a8ee3d 100644 --- a/app/locations/[location]/page.tsx +++ b/app/locations/[location]/page.tsx @@ -7,14 +7,15 @@ import { useState, useEffect } from "react"; import LocationCategories from "@/components/location/categories"; import Link from "next/link"; -import { getCookies } from 'next-client-cookies/server'; +import { fetchUserInfo } from "@/app/user_info"; export default function Page({ params }: { params: { location: number } }) { const [location, setLocation] = useState(null); const [foodReviews, setFoodReviews] = useState(null); + const [userId, setUserId] = useState("anonymous"); useEffect(() => { - fetchLocations().then((locations: Location[]) => { + fetchLocations().then(async (locations: Location[]) => { if (params.location < 0 || params.location >= locations.length) { return

Location not found

; } @@ -26,9 +27,14 @@ export default function Page({ params }: { params: { location: number } }) { ) ); + //get username and set it + const userInfo = await fetchUserInfo(); + const username = userInfo?.email || "anonymous"; + setUserId(username); + fetchFoodReviewsBulk({ food_names: food_names, - user_id: "anonymous", + user_id: userId, }).then((reviews: FrontEndReviews) => { setLocation(location); setFoodReviews(reviews); From 468a927a9eb92f9e6fe28dec3943e19eb21aadef Mon Sep 17 00:00:00 2001 From: Anya Zhang Date: Fri, 31 May 2024 17:17:59 -0700 Subject: [PATCH 07/36] ratings user_id works now for locations page --- app/db.ts | 2 +- app/locations/[location]/page.tsx | 16 ++++++++++------ backend/api/model_logic/foods/views.py | 2 +- components/location/categories.tsx | 23 +++++++++++++++++++++-- components/location/food.tsx | 4 ++-- db.sqlite3 | Bin 143360 -> 143360 bytes 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/app/db.ts b/app/db.ts index 841ea75..4495f2f 100644 --- a/app/db.ts +++ b/app/db.ts @@ -21,7 +21,7 @@ export async function fetchLocations(): Promise { export async function fetchFoodReviewsBulk(data: { food_names: string[]; - user_id: string | null; + user_id: string; }): Promise { const res = await api .post(`/get_ratings_bulk/`, data) diff --git a/app/locations/[location]/page.tsx b/app/locations/[location]/page.tsx index 5c7bd2e..eaf919a 100644 --- a/app/locations/[location]/page.tsx +++ b/app/locations/[location]/page.tsx @@ -12,7 +12,6 @@ import { fetchUserInfo } from "@/app/user_info"; export default function Page({ params }: { params: { location: number } }) { const [location, setLocation] = useState(null); const [foodReviews, setFoodReviews] = useState(null); - const [userId, setUserId] = useState("anonymous"); useEffect(() => { fetchLocations().then(async (locations: Location[]) => { @@ -28,13 +27,18 @@ export default function Page({ params }: { params: { location: number } }) { ); //get username and set it - const userInfo = await fetchUserInfo(); - const username = userInfo?.email || "anonymous"; - setUserId(username); - + const username = "anonymous"; + try { + const userInfo = await fetchUserInfo(); + const username = userInfo.email ? userInfo.email : "anonymous"; + //console.log("username is: ", username); + } catch (error) { + console.error("Failed to fetch user info:", error); + } + fetchFoodReviewsBulk({ food_names: food_names, - user_id: userId, + user_id: username, }).then((reviews: FrontEndReviews) => { setLocation(location); setFoodReviews(reviews); diff --git a/backend/api/model_logic/foods/views.py b/backend/api/model_logic/foods/views.py index 71f98ff..784f2de 100644 --- a/backend/api/model_logic/foods/views.py +++ b/backend/api/model_logic/foods/views.py @@ -65,7 +65,7 @@ def get_food(request, name: str): @api_view(["POST"]) def return_ratings_bulk(request): food_names: list[str] = request.data.get("food_names") - user_id: str | None = request.data.get("user_id") + user_id: str = request.data.get("user_id") if food_names == None: print("food_names is None") diff --git a/components/location/categories.tsx b/components/location/categories.tsx index cd20ee4..049a1b7 100644 --- a/components/location/categories.tsx +++ b/components/location/categories.tsx @@ -1,10 +1,11 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import LocationFood from "@/components/location/food"; import { Location } from "@/interfaces/Location"; import { FrontEndReviews } from "@/interfaces/Review"; +import { fetchUserInfo } from "@/app/user_info"; export default function LocationCategories({ location, @@ -31,6 +32,23 @@ export default function LocationCategories({ }) ); + const [userId, setUserId] = useState("anonymous"); + + useEffect(() => { + const getUserInfo = async () => { + try { + const userInfo = await fetchUserInfo(); + const username = userInfo.email ? userInfo.email : "anonymous"; + setUserId(username); + } catch (error) { + console.error("Failed to fetch user info:", error); + setUserId("anonymous"); + } + }; + getUserInfo(); + //console.log("userId =", userId); + }, [userId]); + const menuArrow = (rotate180: boolean) => ( {subCategory.foods.map((food, k) => ( + //this is where user_id is actually set for the reviews smh ))} diff --git a/components/location/food.tsx b/components/location/food.tsx index 5a21c55..d825c9e 100644 --- a/components/location/food.tsx +++ b/components/location/food.tsx @@ -16,7 +16,7 @@ export default function LocationFood({ food_average: number | null; user_rating: number | null; restrictions: string[]; - user_id: string | null; + user_id: string; }) { const ratings = [1, 2, 3, 4, 5]; const [average, setAverage] = useState(food_average); @@ -54,7 +54,7 @@ export default function LocationFood({ onChange={(e) => updateReview({ food_name: food_name, - user_id: user_id || "anonymous", + user_id: user_id, food_rating: parseInt(e.target.value), }).then((data) => { const newAverage = data.average; diff --git a/db.sqlite3 b/db.sqlite3 index 9f992f5f98c023f3fc0a0f2152282720527f28cf..2979d2d7b1c5205f9a41935507b362452bedf25c 100644 GIT binary patch delta 511 zcmZp8z|ru4V}dke^h6nF#^{X+OXf4%vKvfhU!cy!9y(cXfgKaO!RDL=2iUw+n0*<` z&CT-)3{uOCQ_a$hO>;~P3aau9Q(FfcSWv;j#^->AVP zwf)x*MlN*)KDG-C{Ly?H_;`7n%*eQWLfXx>|bc5TbZ2{k>l+io}Qm>o>5vJ5mxD+<>i;^Y7mhT6>MmcmSy5w zV3`w^YLX9kErw?rHEh845(=l(O0TSx2(QYha8rYb>3z~nI$kbL?tWHICCNFKe(oL~ xE|JD%d1;ve{+X$|X+e$|=~4bs2JTVGhF(T)d9GDS7Ugh9U;O~snyLT* delta 120 zcmV-;0Ehp8;0S==2#^~AS&wNq@M#j2YQnSpeK_spgyy3puh%^kWP~@rv!t3 zr?-Bm0ftBeI|$sD2k-%Rv4LI)m!9|m4VSto0ui_R_yGzh91I9101sOZunr9kY7D^) a91C#@#tIt=b_mM|Cb5AA2)8CE0@??q$0=?A From c30a515391da2556543b7628ccdaa47bf85b47f9 Mon Sep 17 00:00:00 2001 From: anyazhang17 <112535657+anyazhang17@users.noreply.github.com> Date: Fri, 31 May 2024 17:35:32 -0700 Subject: [PATCH 08/36] Delete app/locations/[location]/client.tsx --- app/locations/[location]/client.tsx | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 app/locations/[location]/client.tsx diff --git a/app/locations/[location]/client.tsx b/app/locations/[location]/client.tsx deleted file mode 100644 index ea73202..0000000 --- a/app/locations/[location]/client.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; -import { useEffect, useState } from "react"; -import { useCookies } from 'next-client-cookies'; - -export default function ClientComponent() { - console.log("running client"); - // State to store the user token - const [userToken, setUserToken] = useState(""); - - // Effect to retrieve the token from sessionStorage - useEffect(() => { - // Retrieve the token from sessionStorage - const token = sessionStorage.getItem("email"); - // Update the state with the retrieved token or set it to "anonymous" if not found - setUserToken(token || "anonymous"); - }, []); - - const cookies = useCookies(); - cookies.set("username", userToken); - -}; From bd8f4611689a8c59e5053d1efec2b2313e1fc7cd Mon Sep 17 00:00:00 2001 From: Anya Zhang Date: Fri, 31 May 2024 17:50:48 -0700 Subject: [PATCH 09/36] revert random changes from before --- app/locations/[location]/page.tsx | 4 ++-- app/profile/page.tsx | 2 +- components/login.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/locations/[location]/page.tsx b/app/locations/[location]/page.tsx index eaf919a..b0113f6 100644 --- a/app/locations/[location]/page.tsx +++ b/app/locations/[location]/page.tsx @@ -27,10 +27,10 @@ export default function Page({ params }: { params: { location: number } }) { ); //get username and set it - const username = "anonymous"; + let username = ""; try { const userInfo = await fetchUserInfo(); - const username = userInfo.email ? userInfo.email : "anonymous"; + username = userInfo.email ? userInfo.email : "anonymous"; //console.log("username is: ", username); } catch (error) { console.error("Failed to fetch user info:", error); diff --git a/app/profile/page.tsx b/app/profile/page.tsx index cd92256..fc1da0d 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -65,4 +65,4 @@ const Page = () => { ); }; -export default Page; +export default Page; \ No newline at end of file diff --git a/components/login.tsx b/components/login.tsx index d55b250..49861e5 100644 --- a/components/login.tsx +++ b/components/login.tsx @@ -51,4 +51,4 @@ const LoginComponent = () => { return ; }; -export default LoginPage; +export default LoginPage; \ No newline at end of file From 3f3b8184a7155435bd43b88aaaae6bd33093914f Mon Sep 17 00:00:00 2001 From: Anya Zhang Date: Fri, 31 May 2024 17:57:10 -0700 Subject: [PATCH 10/36] jk reverted fr now --- app/locations/[location]/page.tsx | 2 +- app/profile/page.tsx | 13 ++++++++----- components/login.tsx | 6 +----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/app/locations/[location]/page.tsx b/app/locations/[location]/page.tsx index b0113f6..c5df968 100644 --- a/app/locations/[location]/page.tsx +++ b/app/locations/[location]/page.tsx @@ -14,7 +14,7 @@ export default function Page({ params }: { params: { location: number } }) { const [foodReviews, setFoodReviews] = useState(null); useEffect(() => { - fetchLocations().then(async (locations: Location[]) => { + fetchLocations().then((locations: Location[]) => { if (params.location < 0 || params.location >= locations.length) { return

Location not found

; } diff --git a/app/profile/page.tsx b/app/profile/page.tsx index fc1da0d..ee48da8 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -11,7 +11,6 @@ interface User { picture: string; } import Image from "next/image"; -import { useCookies } from 'next-client-cookies'; const imageWidth = 100; const imageHeight = 100; @@ -32,13 +31,12 @@ const Page = () => { const handleLogout = () => { googleLogout(); axios - .post("http://localhost:8000/myapi/logout/") + .post("http://localhost:8000/api/logout/") .then((res) => console.log("Backend logout successful", res)) .catch((err) => console.error("Backend logout failed", err)); - // Remove the stored user login token + // Remove the token from sessionStorage sessionStorage.removeItem("token"); - sessionStorage.removeItem("email"); // Redirect the user to the main page after logging out window.location.href = "/"; console.log("Logged out successfully"); @@ -49,7 +47,12 @@ const Page = () => {

Profile

{user && (
- User profile + User profile

Welcome, {user.name} - {user.email}

diff --git a/components/login.tsx b/components/login.tsx index 49861e5..117319e 100644 --- a/components/login.tsx +++ b/components/login.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useState } from "react"; import { GoogleOAuthProvider, useGoogleLogin } from "@react-oauth/google"; import axios from "axios"; import { GOOGLE_CLIENT_ID } from "@/private/secrets"; -import Cookies from 'js-cookie'; interface User { name: string; @@ -19,8 +18,6 @@ const LoginPage = () => { }; const LoginComponent = () => { - const [user, setUser] = useState(null); - useEffect(() => { console.log("LoginPage component mounted"); }, []); @@ -30,9 +27,8 @@ const LoginComponent = () => { onSuccess: (tokenResponse) => { console.log(tokenResponse); - // Store authentication token + // Store authentication token in the browser's storage for navigation bar use sessionStorage.setItem("token", tokenResponse.access_token); - // Redirect the user to main page window.location.href = "/"; //handleLoginSuccess From 1b8c6d5a9d7632cdb1afda2e04ab5586bbbfa5b0 Mon Sep 17 00:00:00 2001 From: Anya Zhang Date: Fri, 31 May 2024 18:04:07 -0700 Subject: [PATCH 11/36] oops revert edit that caused error async --- app/locations/[location]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locations/[location]/page.tsx b/app/locations/[location]/page.tsx index c5df968..b0113f6 100644 --- a/app/locations/[location]/page.tsx +++ b/app/locations/[location]/page.tsx @@ -14,7 +14,7 @@ export default function Page({ params }: { params: { location: number } }) { const [foodReviews, setFoodReviews] = useState(null); useEffect(() => { - fetchLocations().then((locations: Location[]) => { + fetchLocations().then(async (locations: Location[]) => { if (params.location < 0 || params.location >= locations.length) { return

Location not found

; } From 382af3009c479add80df622285ce2309cfd4b1a8 Mon Sep 17 00:00:00 2001 From: Akshat Tiwari Date: Fri, 31 May 2024 18:15:05 -0700 Subject: [PATCH 12/36] Added some setup functions --- backend/api/urls.py | 2 + backend/api/views.py | 14 +++++ components/food/images.tsx | 61 ++++++++++++++++++++- components/images_section/ImageDisplay.tsx | 25 +++++++++ components/images_section/feed.tsx | 35 ++++++++++++ db.sqlite3 | Bin 143360 -> 143360 bytes 6 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 components/images_section/ImageDisplay.tsx create mode 100644 components/images_section/feed.tsx diff --git a/backend/api/urls.py b/backend/api/urls.py index dee25a6..5bad6df 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -10,4 +10,6 @@ path("comments/", foods_views.add_comment, name="comments"), path("rating_update/", foods_views.user_rating_update, name="rating_update"), path("get_ratings_bulk/", foods_views.return_ratings_bulk, name="get_ratings_bulk"), + # Add the new endpoint for image uploads + path("upload_image/", views.upload_image, name="upload_image"), ] diff --git a/backend/api/views.py b/backend/api/views.py index 5f344ef..18a212a 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -1,5 +1,7 @@ import json from .model_logic.foods.actions import get_food, set_food, update_food +from django.http import JsonResponse + from rest_framework.response import Response from rest_framework.decorators import api_view @@ -185,6 +187,18 @@ def current_logout(request): return JsonResponse({"message": "User has been logged out"}) +@api_view(["POST"]) +def upload_image(request): + if request.method == "POST" and request.FILES.get("image"): + # Handle image upload logic here + uploaded_image = request.FILES["image"] + # Process the uploaded image (e.g., save it to a storage location) + # Return a JSON response indicating success + return JsonResponse({"success": True, "message": "Image uploaded successfully"}) + else: + # Return a JSON response with an error message if no image is provided or method is not POST + return JsonResponse({"success": False, "message": "Image upload failed"}) + # @api_view(["GET"]) # def get_user_rating(request): # user = get_rating(request.user_ids) diff --git a/components/food/images.tsx b/components/food/images.tsx index 3937936..28c9e9c 100644 --- a/components/food/images.tsx +++ b/components/food/images.tsx @@ -1,11 +1,66 @@ +import React, { useState } from "react"; +import axios from "axios"; import { Food } from "@/interfaces/Food"; +import ImageDisplay from "@/components/images_section/ImageDisplay"; // Import ImageDisplay component + +interface ImagesProps { + food: Food; +} + +const Images: React.FC = ({ food }) => { + const [image, setImage] = useState(null); + const [uploadedImageDetails, setUploadedImageDetails] = useState<{ + imageName: string; + userId: string; + date: string; + imageUrl: string; + } | null>(null); + + const handleImageUpload = async () => { + if (!image) return; // No image selected + + try { + const formData = new FormData(); + formData.append("image", image); + + // Send image data to backend + const response = await axios.post("http://localhost:8000/api/upload_image/", formData); + + // Handle success and set uploaded image details + const { imageName, userId, date, imageUrl } = response.data; + setUploadedImageDetails({ imageName, userId, date, imageUrl }); + } catch (error) { + // Handle error + console.error("Failed to upload image:", error); + } + }; + + const handleImageChange = (e: React.ChangeEvent) => { + const selectedImage = e.target.files && e.target.files[0]; + setImage(selectedImage); + }; -export default function Images({ food }: { food: Food }) { return (

{food && food.name}

Images

-
Image 1
+
+ {/* Input field for selecting an image */} + + {/* Button to trigger image upload */} + +
+ {/* Display uploaded image details */} + {uploadedImageDetails && ( + + )}
); -} +}; + +export default Images; diff --git a/components/images_section/ImageDisplay.tsx b/components/images_section/ImageDisplay.tsx new file mode 100644 index 0000000..5499767 --- /dev/null +++ b/components/images_section/ImageDisplay.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +interface ImageDisplayProps { + userId: string; + date: string; + imageUrl: string; +} + +const ImageDisplay: React.FC = ({ userId, date, imageUrl }) => { + return ( +
+

Uploaded Image Details

+
+ {/* Display uploaded image */} + Uploaded +
+
+

User ID: {userId}

+

Date: {date}

+
+
+ ); +}; + +export default ImageDisplay; diff --git a/components/images_section/feed.tsx b/components/images_section/feed.tsx new file mode 100644 index 0000000..f551a01 --- /dev/null +++ b/components/images_section/feed.tsx @@ -0,0 +1,35 @@ +import { useState, useEffect } from "react"; +import axios from "axios"; + +function Feed() { + const [feedData, setFeedData] = useState([]); + + useEffect(() => { + // Fetch feed data from backend when component mounts + const fetchFeedData = async () => { + try { + const response = await axios.get("http://localhost:8000/api/feed"); + setFeedData(response.data); + } catch (error) { + console.error("Failed to fetch feed data:", error); + } + }; + + fetchFeedData(); + }, []); + + return ( +
+ {feedData.map((item) => ( +
+

{item.user}

+

{item.caption}

+ Uploaded + {/* Additional metadata or actions */} +
+ ))} +
+ ); +} + +export default Feed; diff --git a/db.sqlite3 b/db.sqlite3 index 21e0d337e211d63525cff2e9a0aed27c162b74d9..4a04c56f9bfff3c0954efa58c8161b324287620d 100644 GIT binary patch delta 310 zcmZp8z|ru4V}dke^h6nF#^{X+OXf4%vKvfhUr@}!ZosvION3u}W8*qDO%-Ne#>yh| zl!9F2Jj9y)i3eA&N7bE9BS;PY)3XWE@0DW)U##vZOnEg`}><&AGjT7ROh48W|Xw=o*;m O8e%wfd#O58HwOT~4_^HM delta 118 zcmV-+0Ez#A;0S==2#^~AT9F(>0a~$Oq@M#k2UU{?pmVc)ptJ^&kSCKcrv#UMCjuaY zd8fB|rvZyd13U=5mk00xd9i^#2$!Dt0S&jx_yIa69t{U|01sOZunr9kY7D^)77K9- Y#tIt=b_mM|CI@w~fl~*!bteLg2pnuEasU7T From c684eb478e9da002aa5291799c80024ad9534b8f Mon Sep 17 00:00:00 2001 From: Noahkmm Date: Fri, 31 May 2024 22:54:32 -0700 Subject: [PATCH 13/36] Rating ui improved --- app/db.ts | 31 +- app/favicon.ico | Bin 25931 -> 0 bytes app/global_search/Search.module.css | 87 +++- app/global_search/page.tsx | 386 +++++++-------- app/layout.tsx | 7 +- app/locations/[location]/DH_Search/page.tsx | 499 +++++--------------- app/page.tsx | 31 +- components/LocationsClient.tsx | 68 +++ components/location/categories.tsx | 26 +- components/location/food.tsx | 26 +- 10 files changed, 520 insertions(+), 641 deletions(-) delete mode 100644 app/favicon.ico create mode 100644 components/LocationsClient.tsx diff --git a/app/db.ts b/app/db.ts index 10a6525..cf5d1b3 100644 --- a/app/db.ts +++ b/app/db.ts @@ -3,10 +3,19 @@ import axios from "axios"; import { Location } from "@/interfaces/Location"; import { FrontEndReviews } from "@/interfaces/Review"; -const backend = "http://localhost:8000"; +const api = axios.create({ + baseURL: "http://localhost:8000/api", +}); export async function fetchLocations(): Promise { - const res = await axios.get(`${backend}/api/locations`); + const res = await api.get(`/locations/`).catch((err) => { + console.error(err); + }); + + if (!res) { + return []; + } + return res.data.locations; } @@ -14,7 +23,14 @@ export async function fetchFoodReviewsBulk(data: { food_names: string[]; user_id: string | null; }): Promise { - const res = await axios.post(`${backend}/api/get_ratings_bulk/`, data); + const res = await api.post(`/get_ratings_bulk/`, data).catch((err) => { + console.error(err); + }); + + if (!res) { + return {}; + } + return res.data; } @@ -23,6 +39,13 @@ export async function updateReview(data: { user_id: string; food_rating: number; }): Promise<{ average: number | null }> { - const res = await axios.post(`${backend}/api/rating_update/`, data); + const res = await api.post(`/rating_update/`, data).catch((err) => { + console.error(err); + }); + + if (!res) { + return { average: null }; + } + return res.data; } diff --git a/app/favicon.ico b/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/app/global_search/Search.module.css b/app/global_search/Search.module.css index edf7cf6..a8cfc9a 100644 --- a/app/global_search/Search.module.css +++ b/app/global_search/Search.module.css @@ -1,17 +1,74 @@ -.filterText { - font-size: 3rem; /* Adjust the font size as needed */ - color: #003c6c; /* Set the text color */ - font-weight: 600; /* Set the font weight */ - padding-top: 1.25rem; /* Match the py-5 padding top */ - display: center; /* Match the flex display */ - align-items: center; /* Match the items-center alignment */ - justify-content: center; /* Match the justify-center alignment */ -} - -.filterTopLeft { - position: absolute; +.container { + display: flex; + flex-direction: column; align-items: center; - top: 3rem; /* Adjust the top position as needed */ - left: 80 rem; /* Adjust the left position as needed */ - margin: 0; /* Remove the margin */ +} + +.title { + font-size: 60px; + margin-bottom: 20px; + color: #003c6c; + font-weight: 600; + text-align: center; +} + +.searchBar { + display: flex; + align-items: center; + margin-top: 20px; +} + +.searchInput { + padding: 10px; + font-size: 16px; + border: 1px solid #ccc; + border-radius: 5px; + margin-right: 10px; +} + +.searchButton { + padding: 10px 20px; + background-color: #4caf50; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.filterButton { + margin-left: 10px; + padding: 10px 20px; + background-color: #4caf50; + color: white; + cursor: pointer; + border-radius: 5px; +} + +.results { + margin-top: 20px; +} + +.resultList { + list-style-type: none; + padding: 0; +} + +.resultItem { + margin-bottom: 10px; + font-size: 18px; +} + +.restrictionIcons { + display: flex; + flex-wrap: nowrap; +} + +.restrictionIcon { + margin: 5px; +} + +.noResults { + margin-top: 20px; + font-size: 18px; + color: red; } diff --git a/app/global_search/page.tsx b/app/global_search/page.tsx index 9737a0c..b72468b 100644 --- a/app/global_search/page.tsx +++ b/app/global_search/page.tsx @@ -1,216 +1,230 @@ "use client"; import React, { useState, useEffect } from "react"; import axios from "axios"; -import styles from "./Search.module.css"; import Image from "next/image"; +import Link from "next/link"; +import styles from "./Search.module.css"; +import { fetchLocations } from "@/app/db"; +import { Location, Food } from "@/interfaces/Location"; -interface Food { - name: string; - restrictions: string[]; // Change to string array -} - -interface subCategory { - name: string; - foods: Array; -} - -interface Category { - name: string; - sub_categories: Array; -} - -interface DiningHall { - name: string; - categories: Array; +interface FoodWithCategory { + food: Food; + category: string; + diningHall: string; // Added dining hall name } -interface RestrictionImageMap { - [key: string]: string; -} - -const restrictionImageMap = { - eggs: "/Images/egg.jpg", - vegan: "/Images/vegan.jpg", - fish: "/Images/fish.jpg", - veggie: "/Images/veggie.jpg", - gluten: "/Images/gluten.jpg", - pork: "/Images/pork.jpg", - milk: "/Images/milk.jpg", - beef: "/Images/beef.jpg", - nuts: "/Images/nuts.jpg", - halal: "/Images/halal.jpg", - soy: "/Images/soy.jpg", - shellfish: "/Images/shellfish.jpg", - treenut: "/Images/treenut.jpg", - sesame: "/Images/sesame.jpg", - alcohol: "/Images/alcohol.jpg", -}; -const BarebonesComponent = () => { - const [dhs, setDhs] = useState([]); +const restrictions: string[] = [ + "eggs", + "vegan", + "fish", + "veggie", + "gluten", + "pork", + "milk", + "beef", + "nuts", + "halal", + "soy", + "shellfish", + "treenut", + "sesame", + "alcohol", +]; + +const GlobalSearch = () => { + const [locations, setLocations] = useState([]); + const [foods, setFoods] = useState([]); const [searchInput, setSearchInput] = useState(""); - const [filteredFoods, setFilteredFoods] = useState< - { food: Food; dhName: string; categoryName: string }[] - >([]); - const [showSearchResults, setShowSearchResults] = useState(false); - const [noFoodsFound, setNoFoodsFound] = useState(false); - - // Retrieve hide and show allergies from local storage - const [selectedHideAllergies, setSelectedHideAllergies] = useState( + const [foundFoods, setFoundFoods] = useState([]); + const [isFilterPopupOpen, setIsFilterPopupOpen] = useState(false); + const [selectedRestrictions, setSelectedRestrictions] = useState( () => { - const storedHideAllergies = localStorage.getItem("hideAllergies"); - return storedHideAllergies ? JSON.parse(storedHideAllergies) : []; - }, - ); - - const [selectedShowAllergies, setSelectedShowAllergies] = useState( - () => { - const storedShowAllergies = localStorage.getItem("showAllergies"); - return storedShowAllergies ? JSON.parse(storedShowAllergies) : []; - }, + const storedRestrictions = localStorage.getItem("selectedRestrictions"); + return storedRestrictions ? JSON.parse(storedRestrictions) : []; + } ); + const [filterApplied, setFilterApplied] = useState(false); useEffect(() => { - axios - .get("http://localhost:8000/api/locations/") - .then((response) => { - const dhsData: DiningHall[] = response.data.locations; - setDhs(dhsData); - }) - .catch((error) => { - console.log(error); - }); + fetchLocations().then((locations: Location[]) => { + setLocations(locations); + + const allFoods = locations.flatMap(location => + location.categories.flatMap(category => + category.sub_categories.flatMap(subCategory => + subCategory.foods.map(food => ({ + food: food, + category: category.name, + diningHall: location.name // Added dining hall name + })) + ) + ) + ); + setFoods(allFoods); + }); }, []); - const handleSearchInputChange = ( - event: React.ChangeEvent, - ) => { - setSearchInput(event.target.value); - }; + const searchForFood = (food_name: string) => { + const foundFoods = foods.filter(foodWithCategory => + foodWithCategory.food.name + .toLowerCase() + .includes(food_name.toLowerCase()) + ); - const handleFilter = () => { - window.location.href = "Filter-Window"; + const filteredFoods = foundFoods.filter(({ food }) => + selectedRestrictions.every(restriction => + food.restrictions.includes(restriction) + ) + ); + + setFoundFoods(filteredFoods); }; - const handleSearch = () => { - const allFoods: { food: Food; dhName: string; categoryName: string }[] = []; - dhs.forEach((dh) => { - dh.categories.forEach((category) => { - category.sub_categories.forEach((subCategory) => { - subCategory.foods.forEach((food) => { - allFoods.push({ - food, - dhName: dh.name, - categoryName: category.name, - }); - }); - }); - }); - }); + const toggleFilterPopup = () => { + setIsFilterPopupOpen(!isFilterPopupOpen); + }; - const filtered = allFoods.filter(({ food }) => - food.name.toLowerCase().includes(searchInput.toLowerCase()), + const handleRestrictionChange = (restriction: string, checked: boolean) => { + const newRestrictions = checked + ? [...selectedRestrictions, restriction] + : selectedRestrictions.filter(r => r !== restriction); + setSelectedRestrictions(newRestrictions); + localStorage.setItem( + "selectedRestrictions", + JSON.stringify(newRestrictions) ); + }; - // Check if all boxes are unchecked - const allBoxesUnchecked = - selectedShowAllergies.length === 0 && selectedHideAllergies.length === 0; - - let finalFilteredFoods = filtered; - if (!allBoxesUnchecked) { - // Filter foods based on selectedShowAllergies and selectedHideAllergies - finalFilteredFoods = filtered.filter(({ food }) => { - const hasShowAllergy = - selectedShowAllergies.length === 0 || - selectedShowAllergies.every((allergy) => - food.name.toLowerCase().includes(allergy.toLowerCase()), - ); - const hasHideAllergy = selectedHideAllergies.some( - (allergy) => food.restrictions.includes(allergy.toLowerCase()), // Check if food's restrictions include the hide allergy - ); - return hasShowAllergy && !hasHideAllergy; - }); - } - - setNoFoodsFound(finalFilteredFoods.length === 0); - setFilteredFoods(finalFilteredFoods); - setShowSearchResults(true); + const applyFilter = () => { + setFilterApplied(true); + searchForFood(searchInput); + toggleFilterPopup(); }; return ( -
- {/* Title and Search bar */} -
-

- Global Search -

-
- - - {/* Filter button */} -
+
+
+

+ Global Search +

+
+ +
+
+ setSearchInput(e.target.value)} + /> + +
+
+ +
+
+
-
- {/* Display search results if button clicked */} - {showSearchResults && ( -
-

Search Results:

-
    - {filteredFoods.map(({ food, dhName, categoryName }, index) => ( -
  • - {food.name} - {categoryName} ({dhName}) -
    - {food.restrictions.map((restriction, index) => ( - {restriction} +
    +

    + Filter by Restrictions +

    +
    + {restrictions.map(restriction => ( +
    + + handleRestrictionChange(restriction, e.target.checked) + } /> - ))} -
    -
  • - ))} -
-
- )} + +
+ ))} +
+ + +
+
+ )} - {noFoodsFound && (
-

No foods found at this dining hall.

-
- )} - - ); -}; - -export default BarebonesComponent; + {filterApplied && foundFoods.length === 0 ? ( +

No foods found with the specified allergy constraints

+ ) : ( + foundFoods.map((foodWithCategory, index) => ( +
+ +

{foodWithCategory.food.name}

+
+ {foodWithCategory.category} @ {foodWithCategory.diningHall} {/* Added dining hall name */} +
+ +
    + {foodWithCategory.food.restrictions.map( + (restriction, index) => ( +
  • + {restriction} +
  • + ) + )} +
+
+ )) + )} + + + + ); + }; + + export default GlobalSearch; + diff --git a/app/layout.tsx b/app/layout.tsx index 3b8fd54..c369fcf 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,8 +7,11 @@ import Navbar from "@/components/navbar"; const monst = Montserrat({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "Hungry Slugs", + description: "Review foods at UCSC food locations", + icons: { + icon: "images/logos/UCSC-dining-logo.png", + }, }; // This is the height of the navbar which will also be used to set the padding-top of the main content diff --git a/app/locations/[location]/DH_Search/page.tsx b/app/locations/[location]/DH_Search/page.tsx index afe7418..9350845 100644 --- a/app/locations/[location]/DH_Search/page.tsx +++ b/app/locations/[location]/DH_Search/page.tsx @@ -1,366 +1,34 @@ "use client"; import { useEffect, useState } from "react"; import Image from "next/image"; +import Link from "next/link"; import { fetchLocations } from "@/app/db"; - import { Location, Food } from "@/interfaces/Location"; -import Link from "next/link"; interface FoodWithCategory { food: Food; category: string; } -// Map of restriction images -const restrictionImageMap: { [key: string]: string } = { - eggs: "/Images/egg.jpg", - vegan: "/Images/vegan.jpg", - fish: "/Images/fish.jpg", - veggie: "/Images/veggie.jpg", - gluten: "/Images/gluten.jpg", - pork: "/Images/pork.jpg", - milk: "/Images/milk.jpg", - beef: "/Images/beef.jpg", - nuts: "/Images/nuts.jpg", - halal: "/Images/halal.jpg", - soy: "/Images/soy.jpg", - shellfish: "/Images/shellfish.jpg", - treenut: "/Images/treenut.jpg", - sesame: "/Images/sesame.jpg", - alcohol: "/Images/alcohol.jpg", -}; - -// const HelloWorld: React.FC = () => { -// const [filteredFoods, setFilteredFoods] = useState< -// { food: Food; dhName: string; categoryName: string }[] -// >([]); -// const [showSearchResults, setShowSearchResults] = useState(false); -// const [noFoodsFound, setNoFoodsFound] = useState(false); -// const [diningHall, setDiningHall] = useState(null); -// const [searchInput, setSearchInput] = useState(""); -// const [searchResults, setSearchResults] = useState([]); - -// // Retrieve the dining hall name from localStorage when the component mounts -// useEffect(() => { -// const storedDiningHall = localStorage.getItem("diningHall"); -// setDiningHall(storedDiningHall); -// }, []); - -// // Handle search logic -// const handleSearch = () => { -// const currentDiningHallName = localStorage.getItem("diningHall"); - -// // Filter the dining halls to find the current one -// const currentDiningHall = dhs.find( -// (dh) => dh.name === currentDiningHallName -// ); - -// if (currentDiningHall) { -// const allFoods: { food: Food; dhName: string; categoryName: string }[] = -// []; - -// // Collect all foods from the current dining hall only -// currentDiningHall.categories.forEach((category) => { -// category.sub_categories.forEach((subCategory) => { -// subCategory.foods.forEach((food) => { -// allFoods.push({ -// food, -// dhName: currentDiningHall.name, -// categoryName: category.name, -// }); -// }); -// }); -// }); - -// // Filter the collected foods based on the search input -// const filtered = allFoods.filter(({ food }) => -// food.name.toLowerCase().includes(searchInput.toLowerCase()) -// ); - -// // Check if all boxes are unchecked -// const allBoxesUnchecked = -// selectedShowAllergies.length === 0 && -// selectedHideAllergies.length === 0; - -// let finalFilteredFoods = filtered; -// if (!allBoxesUnchecked) { -// // Filter foods based on selectedShowAllergies and selectedHideAllergies -// finalFilteredFoods = filtered.filter(({ food }) => { -// const hasShowAllergy = -// selectedShowAllergies.length === 0 || -// selectedShowAllergies.every((allergy) => -// food.name.toLowerCase().includes(allergy.toLowerCase()) -// ); -// const hasHideAllergy = selectedHideAllergies.some( -// (allergy) => food.restrictions.includes(allergy.toLowerCase()) // Check if food's restrictions include the hide allergy -// ); -// return hasShowAllergy && !hasHideAllergy; -// }); -// } - -// // Update the state based on the search results -// setNoFoodsFound(finalFilteredFoods.length === 0); -// setFilteredFoods(finalFilteredFoods); -// setShowSearchResults(true); -// } else { -// // Handle case where the current dining hall is not found -// setFilteredFoods([]); -// setShowSearchResults(false); -// setNoFoodsFound(true); -// } -// }; - -// return ( -//
-//

-// {diningHall ? diningHall : "Dining Hall not found"} -//

-//
-// setSearchInput(e.target.value)} -// className="border border-gray-400 p-2 rounded" -// /> -// -//
-// {searchResults.length > 0 && ( -//
-//

{searchResults[0].categoryName}:

-//
    -// {searchResults.map((food: any, index: number) => ( -//
  • -//
    -// -// {food.name} - {food.categoryName} ({diningHall}) -// -//
    -// {food.restrictions.map( -// (restriction: string, index: number) => ( -// {restriction} -// ) -// )} -//
    -//
    -//
  • -// ))} -//
-//
-// )} -//
-// ); -// }; - -// const BarebonesComponent: React.FC = () => { -// const [dhs, setDhs] = useState([]); -// const [searchInput, setSearchInput] = useState(""); -// const [filteredFoods, setFilteredFoods] = useState< -// { food: Food; dhName: string; categoryName: string }[] -// >([]); -// const [showSearchResults, setShowSearchResults] = useState(false); -// const [noFoodsFound, setNoFoodsFound] = useState(false); - -// // Retrieve hide and show allergies from local storage -// const [selectedHideAllergies, setSelectedHideAllergies] = useState( -// () => { -// const storedHideAllergies = localStorage.getItem("hideAllergies"); -// return storedHideAllergies ? JSON.parse(storedHideAllergies) : []; -// } -// ); - -// const [selectedShowAllergies, setSelectedShowAllergies] = useState( -// () => { -// const storedShowAllergies = localStorage.getItem("showAllergies"); -// return storedShowAllergies ? JSON.parse(storedShowAllergies) : []; -// } -// ); - -// // Fetch dining hall data from the API when the component mounts -// useInsertionEffect(() => { -// fetchLocations().then((locations: Location[]) => { -// setDhs(locations); -// }); -// }, []); - -// // Handle search input change -// const handleSearchInputChange = ( -// event: React.ChangeEvent -// ) => { -// setSearchInput(event.target.value); -// }; - -// // Handle filter button click -// const handleFilter = () => { -// window.location.href = "Filter-Window"; -// }; - -// // Handle search logic -// const handleSearch = () => { -// const currentDiningHallName = localStorage.getItem("diningHall"); - -// // Filter the dining halls to find the current one -// const currentDiningHall = dhs.find( -// (dh) => dh.name === currentDiningHallName -// ); - -// if (currentDiningHall) { -// const allFoods: { food: Food; dhName: string; categoryName: string }[] = -// []; - -// // Collect all foods from the current dining hall only -// currentDiningHall.categories.forEach((category) => { -// category.sub_categories.forEach((subCategory) => { -// subCategory.foods.forEach((food) => { -// allFoods.push({ -// food, -// dhName: currentDiningHall.name, -// categoryName: category.name, -// }); -// }); -// }); -// }); - -// // Filter the collected foods based on the search input -// const filtered = allFoods.filter(({ food }) => -// food.name.toLowerCase().includes(searchInput.toLowerCase()) -// ); - -// // Check if all boxes are unchecked -// const allBoxesUnchecked = -// selectedShowAllergies.length === 0 && -// selectedHideAllergies.length === 0; - -// let finalFilteredFoods = filtered; -// if (!allBoxesUnchecked) { -// // Filter foods based on selectedShowAllergies and selectedHideAllergies -// finalFilteredFoods = filtered.filter(({ food }) => { -// const hasShowAllergy = -// selectedShowAllergies.length === 0 || -// selectedShowAllergies.every((allergy) => -// food.name.toLowerCase().includes(allergy.toLowerCase()) -// ); -// const hasHideAllergy = selectedHideAllergies.some( -// (allergy) => food.restrictions.includes(allergy.toLowerCase()) // Check if food's restrictions include the hide allergy -// ); -// return hasShowAllergy && !hasHideAllergy; -// }); -// } - -// // Update the state based on the search results -// setNoFoodsFound(finalFilteredFoods.length === 0); -// setFilteredFoods(finalFilteredFoods); -// setShowSearchResults(true); -// } else { -// // Handle case where the current dining hall is not found -// setFilteredFoods([]); -// setShowSearchResults(false); -// setNoFoodsFound(true); -// } -// }; - -// return ( -//
-// {/* Title and Search bar */} -//
-//

-// {localStorage.getItem("diningHall")} -//

- -//
-// -// -// {/* Filter button */} -//
-// Filter -//
-//
-//
- -// {/* Display search results if button clicked */} -// {showSearchResults && ( -//
-//

Search Results:

-//
    -// {filteredFoods.map(({ food, dhName, categoryName }, index) => ( -//
  • -// {food.name} - {categoryName} ({dhName}) -//
    -// {food.restrictions.map((restriction, index) => ( -// {restriction} -// ))} -//
    -//
  • -// ))} -//
-//
-// )} - -// {noFoodsFound && ( -//
-//

No foods found at this dining hall.

-//
-// )} -//
-// ); -// }; - -// export default BarebonesComponent; - -// import Link from "next/link"; +// list of restrictions +const restrictions: string[] = [ + "eggs", + "vegan", + "fish", + "veggie", + "gluten", + "pork", + "milk", + "beef", + "nuts", + "halal", + "soy", + "shellfish", + "treenut", + "sesame", + "alcohol", +]; export default function Page({ params }: { params: { location: number } }) { const [location, setLocation] = useState(null); @@ -368,6 +36,15 @@ export default function Page({ params }: { params: { location: number } }) { const [searchInput, setSearchInput] = useState(""); const [foundFoods, setFoundFoods] = useState([]); + const [selectedRestrictions, setSelectedRestrictions] = useState( + () => { + const storedRestrictions = localStorage.getItem("selectedRestrictions"); + return storedRestrictions ? JSON.parse(storedRestrictions) : []; + }, + ); + const [isFilterPopupOpen, setIsFilterPopupOpen] = useState(false); + const [filterApplied, setFilterApplied] = useState(false); + useEffect(() => { fetchLocations().then((locations: Location[]) => { if (params.location < 0 || params.location >= locations.length) { @@ -392,47 +69,128 @@ export default function Page({ params }: { params: { location: number } }) { return

Loading...

; } - // const filterFoods; - const searchForFood = (food_name: string) => { - // search for food const foundFoods = foods.filter((foodWithCategory) => foodWithCategory.food.name .toLowerCase() .includes(food_name.toLowerCase()), ); - // apply filters + const filteredFoods = foundFoods.filter(({ food }) => + selectedRestrictions.every((restriction) => + food.restrictions.includes(restriction), + ), + ); + + setFoundFoods(filteredFoods); + }; + + const toggleFilterPopup = () => { + setIsFilterPopupOpen(!isFilterPopupOpen); + }; + + const handleRestrictionChange = (restriction: string, checked: boolean) => { + const newRestrictions = checked + ? [...selectedRestrictions, restriction] + : selectedRestrictions.filter((r) => r !== restriction); + setSelectedRestrictions(newRestrictions); + localStorage.setItem( + "selectedRestrictions", + JSON.stringify(newRestrictions), + ); + }; - setFoundFoods(foundFoods); + const applyFilter = () => { + setFilterApplied(true); + searchForFood(searchInput); + toggleFilterPopup(); }; return (
-
+

{location.name}

-
- setSearchInput(e.target.value)} - /> +
+
+ setSearchInput(e.target.value)} + /> + +
+ {isFilterPopupOpen && ( +
+
+

+ Filter by Restrictions +

+
+ {restrictions.map((restriction) => ( +
+ + handleRestrictionChange(restriction, e.target.checked) + } + /> + +
+ ))} +
+ + +
+
+ )} +
- {foundFoods.length > 0 && + {filterApplied && foundFoods.length === 0 ? ( +

No foods found with the specified allergy constraints

+ ) : ( foundFoods.map((foodWithCategory, index) => (
(
  • {restriction}
  • - ))} + )) + )}
    diff --git a/app/page.tsx b/app/page.tsx index 594bee0..e083167 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,9 @@ "use server"; import { fetchLocations } from "@/app/db"; +import LocationsClient from "@/components/LocationsClient"; import { Location } from "@/interfaces/Location"; -import DhBar from "@/components/dh_bar_main"; - export default async function Home() { const locations: Location[] = await fetchLocations(); @@ -14,33 +13,7 @@ export default async function Home() {

    Locations

    - -
      - {[ - ["Dining Halls", "#"], - ["Markets", "#"], - ["Cafes & Other", "#"], - ].map(([category, href]: Array, i) => ( -
    • - - {category} - -
    • - ))} -
    - -

    -
      - {locations.map((location, i) => ( -
    • - -
    • - ))} -
    -

    + ); diff --git a/components/LocationsClient.tsx b/components/LocationsClient.tsx new file mode 100644 index 0000000..da23cfc --- /dev/null +++ b/components/LocationsClient.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useState } from "react"; +import { Location } from "@/interfaces/Location"; +import DhBar from "@/components/dh_bar_main"; + +interface LocationsClientProps { + locations: Location[]; +} + +export default function LocationsClient({ locations }: LocationsClientProps) { + const [selectedCategory, setSelectedCategory] = useState("All"); + + const handleCategoryClick = (category: string) => { + setSelectedCategory(category); + }; + + const filteredLocations = locations.filter((location) => { + if (selectedCategory === "All") { + return true; + } else if (selectedCategory === "Dining Halls") { + return location.name.toLowerCase().includes("dining hall"); + } else if (selectedCategory === "Markets") { + return ( + location.name.toLowerCase().includes("market") || + location.name.toLowerCase().includes("stop") + ); + } else if (selectedCategory === "Cafes & Other") { + return ( + location.name.toLowerCase().includes("cafe") || + location.name.toLowerCase().includes("other") + ); + } + return true; + }); + + return ( +
    +
      + {[ + ["All", "#"], + ["Dining Halls", "#"], + ["Markets", "#"], + ["Cafes & Other", "#"], + ].map(([category, href], i) => ( +
    • + +
    • + ))} +
    + +

    +
      + {filteredLocations.map((location, i) => ( +
    • + +
    • + ))} +
    +

    +
    + ); +} diff --git a/components/location/categories.tsx b/components/location/categories.tsx index 55628a6..df85634 100644 --- a/components/location/categories.tsx +++ b/components/location/categories.tsx @@ -31,28 +31,6 @@ export default function LocationCategories({ }), ); - interface RestrictionImageMap { - [key: string]: string; - } - - const restrictionImageMap: RestrictionImageMap = { - eggs: "/Images/egg.jpg", - vegan: "/Images/vegan.jpg", - fish: "/Images/fish.jpg", - veggie: "/Images/veggie.jpg", - gluten: "/Images/gluten.jpg", - pork: "/Images/pork.jpg", - milk: "/Images/milk.jpg", - beef: "/Images/beef.jpg", - nuts: "/Images/nuts.jpg", - halal: "/Images/halal.jpg", - soy: "/Images/soy.jpg", - shellfish: "/Images/shellfish.jpg", - treenut: "/Images/treenut.jpg", - sesame: "/Images/sesame.jpg", - alcohol: "/Images/alcohol.jpg", - }; - const menuArrow = (rotate180: boolean) => ( restrictionImageMap[restriction], - )} + restrictions={food.restrictions} user_id={null} /> ))} diff --git a/components/location/food.tsx b/components/location/food.tsx index 21c93fc..1be2a95 100644 --- a/components/location/food.tsx +++ b/components/location/food.tsx @@ -9,13 +9,13 @@ export default function LocationFood({ food_name, food_average, user_rating, - restriction_images, + restrictions, user_id, }: { food_name: string; food_average: number | null; user_rating: number | null; - restriction_images: string[]; + restrictions: string[]; user_id: string | null; }) { const ratings = [1, 2, 3, 4, 5]; @@ -29,21 +29,27 @@ export default function LocationFood({
      - {restriction_images.map((image, index) => ( + {restrictions.map((image_name, index) => (
    • - {image}{" "} + {image_name} {/* Display the image */}
    • ))}
    -
    -

    {average ? average : "?"}

    +
    + +

    Score: {average ? average : "?"}

    -

    -
    - @@ -59,7 +65,7 @@ export default function LocationFood({ } > {ratings.map((rating, index) => ( {ratings.map((rating, index) => (

    - {restrictions.map(restriction => ( + {restrictions.map((restriction) => (
    { id={restriction} className="mr-2" checked={selectedRestrictions.includes(restriction)} - onChange={e => + onChange={(e) => handleRestrictionChange(restriction, e.target.checked) } /> @@ -200,7 +200,8 @@ const GlobalSearch = () => { >

    {foodWithCategory.food.name}

    - {foodWithCategory.category} @ {foodWithCategory.diningHall} {/* Added dining hall name */} + {foodWithCategory.category} @ {foodWithCategory.diningHall}{" "} + {/* Added dining hall name */}
      @@ -214,17 +215,16 @@ const GlobalSearch = () => { height={25} /> - ) + ), )} -
    -
    - )) - )} -
    -
    - - ); - }; - - export default GlobalSearch; + +
    + )) + )} +
    + + + ); +}; +export default GlobalSearch; diff --git a/app/locations/[location]/page.tsx b/app/locations/[location]/page.tsx index 8d92cb9..b6baa96 100644 --- a/app/locations/[location]/page.tsx +++ b/app/locations/[location]/page.tsx @@ -25,8 +25,8 @@ export default function Page({ params }: { params: { location: number } }) { const food_names = location.categories.flatMap((category) => category.sub_categories.flatMap((sub_category) => - sub_category.foods.map((food) => food.name) - ) + sub_category.foods.map((food) => food.name), + ), ); //get username and set it diff --git a/app/requests.ts b/app/requests.ts index 0ad39d9..97363f4 100644 --- a/app/requests.ts +++ b/app/requests.ts @@ -62,7 +62,7 @@ export async function fetchUserInfo() { "https://www.googleapis.com/oauth2/v3/userinfo", { headers: { Authorization: `Bearer ${access_token}` }, - } + }, ); const userInfo = response.data; diff --git a/app/user_info.ts b/app/user_info.ts index c1aaeaf..1c12067 100644 --- a/app/user_info.ts +++ b/app/user_info.ts @@ -1,3 +1,2 @@ "use client"; import axios from "axios"; - diff --git a/components/food/Images.css b/components/food/Images.css index 8489f42..beff499 100644 --- a/components/food/Images.css +++ b/components/food/Images.css @@ -1,60 +1,59 @@ .images-container { - padding: 20px; - max-width: 600px; - margin: 0 auto; - font-family: Arial, sans-serif; - } - - .food-name { - text-align: center; - font-size: 2em; - margin-bottom: 10px; - } - - .section-title { - text-align: center; - font-size: 1.5em; - margin-bottom: 20px; - } - - .upload-section { - display: flex; - flex-direction: column; - align-items: center; - } - - .file-input { - margin-bottom: 10px; - } - - .upload-button { - background-color: #4CAF50; - color: white; - padding: 10px 20px; - border: none; - border-radius: 5px; - cursor: pointer; - } - - .upload-button:hover { - background-color: #45a049; - } - - .uploaded-image-details { - margin-top: 20px; - text-align: center; - } - - .image-details { - margin-bottom: 10px; - } - - .uploaded-image { - max-width: 100%; - height: auto; - border: 1px solid #ddd; - border-radius: 4px; - padding: 5px; - margin-top: 10px; - } - \ No newline at end of file + padding: 20px; + max-width: 600px; + margin: 0 auto; + font-family: Arial, sans-serif; +} + +.food-name { + text-align: center; + font-size: 2em; + margin-bottom: 10px; +} + +.section-title { + text-align: center; + font-size: 1.5em; + margin-bottom: 20px; +} + +.upload-section { + display: flex; + flex-direction: column; + align-items: center; +} + +.file-input { + margin-bottom: 10px; +} + +.upload-button { + background-color: #4caf50; + color: white; + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; +} + +.upload-button:hover { + background-color: #45a049; +} + +.uploaded-image-details { + margin-top: 20px; + text-align: center; +} + +.image-details { + margin-bottom: 10px; +} + +.uploaded-image { + max-width: 100%; + height: auto; + border: 1px solid #ddd; + border-radius: 4px; + padding: 5px; + margin-top: 10px; +} diff --git a/components/food/comments.tsx b/components/food/comments.tsx index 65eafca..f5f719b 100644 --- a/components/food/comments.tsx +++ b/components/food/comments.tsx @@ -14,7 +14,7 @@ function pythonDatetimeToJsDatetime(pythonDatetime: string): Date { parseInt(day), parseInt(hour), parseInt(minute), - parseInt(second) + parseInt(second), ); } @@ -86,7 +86,6 @@ export default function Comments({ food }: { food: Food }) { return (
    -
    {comments.map((comment, i) => (
    Post diff --git a/components/food/images.tsx b/components/food/images.tsx index 2984a27..14b2e77 100644 --- a/components/food/images.tsx +++ b/components/food/images.tsx @@ -45,14 +45,22 @@ const Images: React.FC = ({ food }) => { formData.append("user_id", userId); // Use the state userId // Send image data to backend - const response = await axios.post("http://localhost:8000/api/upload_image/", formData); + const response = await axios.post( + "http://localhost:8000/api/upload_image/", + formData, + ); // Log the response to debug console.log("API response:", response.data); // Handle success and set uploaded image details const { imageName, user_id, date, imageUrl } = response.data; - setUploadedImageDetails({ imageName, uploadedBy: user_id, date, imageUrl }); + setUploadedImageDetails({ + imageName, + uploadedBy: user_id, + date, + imageUrl, + }); } catch (error) { // Handle error console.error("Failed to upload image:", error); @@ -87,11 +95,21 @@ const Images: React.FC = ({ food }) => { {uploadedImageDetails && (
    -

    Name: {uploadedImageDetails.imageName}

    -

    Uploaded by: {uploadedImageDetails.uploadedBy}

    -

    Date: {formatDateTime(uploadedImageDetails.date)}

    +

    + Name: {uploadedImageDetails.imageName} +

    +

    + Uploaded by: {uploadedImageDetails.uploadedBy} +

    +

    + Date: {formatDateTime(uploadedImageDetails.date)} +

    - {uploadedImageDetails.imageName} + {uploadedImageDetails.imageName}
    )}
    diff --git a/components/food/ratings.tsx b/components/food/ratings.tsx index 6c68539..b8ccfe7 100644 --- a/components/food/ratings.tsx +++ b/components/food/ratings.tsx @@ -1,29 +1,33 @@ import { Food } from "@/interfaces/Food"; import { Doppio_One } from "next/font/google"; - export default function Ratings({ food }: { food: Food }) { console.log(food); return ( -
    {/* Yes I know this is horribly written and inefficent -Noah*/} +
    + {" "} + {/* Yes I know this is horribly written and inefficent -Noah*/}

    -
    - User -
    -
    - Rating -
    +
    User
    +
    Rating

    -
    - {/* convert ratings obj to array and map */ + { + /* convert ratings obj to array and map */ Object.entries(food.ratings).map(([user_id, rating]) => ( -

    -

    {user_id}
    -
    {rating.rating}
    +

    +

    + {user_id} +
    +
    + {rating.rating} +

    - - ))} + )) + }
    ); diff --git a/components/images_section/ImageDisplay.tsx b/components/images_section/ImageDisplay.tsx index 97348cd..a4f58d4 100644 --- a/components/images_section/ImageDisplay.tsx +++ b/components/images_section/ImageDisplay.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; interface ImageDisplayProps { imageName: string; @@ -7,13 +7,24 @@ interface ImageDisplayProps { imageUrl: string; } -const ImageDisplay: React.FC = ({ imageName, userId, date, imageUrl }) => { +const ImageDisplay: React.FC = ({ + imageName, + userId, + date, + imageUrl, +}) => { return (
    -

    Name: {imageName}

    -

    Uploaded by: {userId}

    -

    Date: {date}

    +

    + Name: {imageName} +

    +

    + Uploaded by: {userId} +

    +

    + Date: {date} +

    {imageName}
    diff --git a/components/location/food.tsx b/components/location/food.tsx index b0e2d7d..1a48b03 100644 --- a/components/location/food.tsx +++ b/components/location/food.tsx @@ -43,13 +43,15 @@ export default function LocationFood({
    - -

    Score: {average ? average : "?"}

    +

    + Score: {average ? average : "?"}{" "} +

    -