From 7e5a07713bc98c74600fb172d8a8144133312d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Tue, 14 May 2024 14:30:13 +0300 Subject: [PATCH 1/3] WIP --- frontend/src/components/NavbarLayout.tsx | 32 +++++++++++++++++++++--- frontend/src/routes/index.tsx | 5 ++++ frontend/src/routes/profile.tsx | 7 ++++++ 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 frontend/src/routes/profile.tsx diff --git a/frontend/src/components/NavbarLayout.tsx b/frontend/src/components/NavbarLayout.tsx index 25310da3..84f04480 100644 --- a/frontend/src/components/NavbarLayout.tsx +++ b/frontend/src/components/NavbarLayout.tsx @@ -1,4 +1,11 @@ -import { CircleUser, Menu, Package2, UtensilsCrossed } from "lucide-react"; +import { + CircleUser, + LogOut, + Menu, + Package2, + User, + UtensilsCrossed, +} from "lucide-react"; import { Link, NavLink, @@ -10,6 +17,9 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { Button } from "./ui/button"; @@ -107,11 +117,25 @@ export const NavbarLayout = () => { + Account + - - - + + Profile + + + + + + ) : ( diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 2bea1361..50e8fa29 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -5,6 +5,7 @@ import { IndexRoute } from "./home"; import { signout } from "../services/auth"; import { Search } from "./search"; import { NavbarLayout } from "../components/NavbarLayout"; +import Profile from "./profile"; export const routes: RouteObject[] = [ { @@ -23,6 +24,10 @@ export const routes: RouteObject[] = [ index: true, Component: IndexRoute, }, + { + path: "/profile", + Component: Profile, + }, { path: "/logout", async action() { diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx new file mode 100644 index 00000000..f129085f --- /dev/null +++ b/frontend/src/routes/profile.tsx @@ -0,0 +1,7 @@ +export default function Profile() { + return ( +
+

Profile

+
+ ); +} From a6117ec782c17186ad3e0c93ccfbb4ee7d193f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Wed, 15 May 2024 13:43:06 +0300 Subject: [PATCH 2/3] feat(frontend): fix some styles --- frontend/package.json | 1 + frontend/src/assets/Icon/General/Plus.svg | 2 +- frontend/src/components/Comment.tsx | 1 - frontend/src/components/NavbarLayout.tsx | 9 +- frontend/src/components/Recipe.tsx | 48 +++++----- frontend/src/components/SearchBar.tsx | 6 +- .../src/components/SearchFilterPopover.tsx | 93 +++++++++++-------- frontend/src/components/ui/avatar.tsx | 48 ++++++++++ frontend/src/components/ui/tabs.tsx | 4 +- frontend/src/index.css | 2 +- frontend/src/routes/feed.tsx | 86 ++++++++--------- frontend/src/routes/profile.tsx | 90 +++++++++++++++++- frontend/src/routes/recipe.tsx | 1 - frontend/src/routes/search.tsx | 6 +- .../src/services/api/semanticBrowseSchemas.ts | 3 +- frontend/yarn.lock | 24 +++++ swagger/openapi.yml | 3 + 17 files changed, 298 insertions(+), 129 deletions(-) create mode 100644 frontend/src/components/ui/avatar.tsx diff --git a/frontend/package.json b/frontend/package.json index c6715f2a..896d0f2f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "@hookform/resolvers": "^3.3.4", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.0.3", + "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", diff --git a/frontend/src/assets/Icon/General/Plus.svg b/frontend/src/assets/Icon/General/Plus.svg index 5b8f2627..c0bc9fb9 100644 --- a/frontend/src/assets/Icon/General/Plus.svg +++ b/frontend/src/assets/Icon/General/Plus.svg @@ -1,3 +1,3 @@ - + diff --git a/frontend/src/components/Comment.tsx b/frontend/src/components/Comment.tsx index 08f59b03..210077b9 100644 --- a/frontend/src/components/Comment.tsx +++ b/frontend/src/components/Comment.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { UserSummary } from "@/services/api/semanticBrowseSchemas"; diff --git a/frontend/src/components/NavbarLayout.tsx b/frontend/src/components/NavbarLayout.tsx index 84f04480..ac723cb9 100644 --- a/frontend/src/components/NavbarLayout.tsx +++ b/frontend/src/components/NavbarLayout.tsx @@ -19,7 +19,6 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, - DropdownMenuShortcut, DropdownMenuTrigger, } from "./ui/dropdown-menu"; import { Button } from "./ui/button"; @@ -119,9 +118,11 @@ export const NavbarLayout = () => { Account - - - Profile + + + + Profile + { return (
{name} @@ -58,20 +49,20 @@ export const Recipe = ({
-
- avgRating icon -

- {avgRating} ({ratingsCount} Reviews) -

+
+ avgRating icon + + {avgRating} ({ratingsCount || 0} Reviews) +
-
- Time icon -

{cookTime}

+
+ Time icon + {cookTime}
{dish && ( -
- Food icon -

{dish.name}

+
+ Food icon + {dish.name}
)} {author && author.profilePicture && ( @@ -84,9 +75,12 @@ export const Recipe = ({
)} diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index 9ba71988..16c3558e 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -19,8 +19,12 @@ export const SearchBar = () => {
{ e.preventDefault(); + const params = new URLSearchParams(); + params.append("q", search); + if (cuisine) params.append("cuisine", cuisine); + if (foodType) params.append("foodType", foodType); - navigate("/search?q=" + encodeURIComponent(search)); + navigate("/search?" + params.toString()); }} className="flex gap-4" > diff --git a/frontend/src/components/SearchFilterPopover.tsx b/frontend/src/components/SearchFilterPopover.tsx index bcef43eb..6b4ee13c 100644 --- a/frontend/src/components/SearchFilterPopover.tsx +++ b/frontend/src/components/SearchFilterPopover.tsx @@ -16,17 +16,24 @@ const predefinedCuisines = [ ]; const predefinedTypeOfFood = ["Meat", "Baked", "Dairy", "Eggs"]; -export default function SearchFilterPopover({ - cuisine, - setCuisine, - foodType, - setFoodType, -}: { +type CuisineFilter = { cuisine: string; setCuisine: (cuisine: string) => void; +}; +type FoodTypeFilter = { foodType: string; setFoodType: (foodType: string) => void; -}) { +}; +export default function SearchFilterPopover( + props: (object | CuisineFilter) & (object | FoodTypeFilter), +) { + const { + cuisine = "", + foodType = "", + setCuisine = null, + setFoodType = null, + } = props as CuisineFilter & FoodTypeFilter; + const [tempCuisine, setTempCuisine] = useState(cuisine); const [tempFoodType, setTempFoodType] = useState(foodType); @@ -63,40 +70,50 @@ export default function SearchFilterPopover({ -

Filter

-
-
Cuisine
-
- {predefinedCuisines.map((cuisine) => ( - - setTempCuisine(e.target.checked ? cuisine : "") - } - /> - ))} -
-
-
-
Type of Food
-
- {predefinedTypeOfFood.map((type) => ( - setTempFoodType(e.target.checked ? type : "")} - /> - ))} -
-
+ {setCuisine && ( + <> +

Filter

+
+
Cuisine
+
+ {predefinedCuisines.map((cuisine) => ( + + setTempCuisine(e.target.checked ? cuisine : "") + } + /> + ))} +
+
+ + )} + {setFoodType && ( + <> +
+
Type of Food
+
+ {predefinedTypeOfFood.map((type) => ( + + setTempFoodType(e.target.checked ? type : "") + } + /> + ))} +
+
+ + )} - -
-
navigate("/filter")}> - -
-
-
- {feedData?.data?.map((recipe) => ( - - ))} +
+

+ {feedData?.data?.length + ? `Found ${feedData.data.length} results` + : "No recipes found"} +

+
+ + + Following + Explore + + +
+ {feedData?.data?.map((recipe) => ( + + ))} +
+
+ +
+ {feedData?.data?.map((recipe) => ( + + ))} +
+
+
); }; diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx index f129085f..0ec7ed14 100644 --- a/frontend/src/routes/profile.tsx +++ b/frontend/src/routes/profile.tsx @@ -1,7 +1,93 @@ +import { useGetUserById } from "@/services/api/semanticBrowseComponents"; +import { useParams } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { AvatarImage, Avatar } from "@/components/ui/avatar"; +import Plus from "@/assets/Icon/General/Plus.svg?react"; +import { FullscreenLoading } from "@/components/FullscreenLoading"; +import ErrorAlert from "@/components/ErrorAlert"; +import { cn } from "@/lib/utils"; +import { Recipe } from "@/components/Recipe"; + export default function Profile() { + const { userId = "" } = useParams<{ userId: string }>(); + const me = userId === "me"; + + const { isLoading, data, error } = useGetUserById({ + pathParams: { userId: me ? ("me" as unknown as number) : parseInt(userId) }, + queryParams: { + enabled: !me && !isNaN(Number(userId)), + }, + }); + + if (!me && isNaN(Number(userId))) { + return

Invalid user id

; + } + if (isLoading) { + return ; + } + if (error) { + return ; + } + + const profile = data!.data; return ( -
-

Profile

+
+
+
+

My profile

+
+
+ + + +
+
+
{profile.recipeCount}
+
Recipes
+
+
+
{profile.followersCount}
+
Followers
+
+
+
{profile.followingCount}
+
Following
+
+
+
+
+
+

{profile.name}

+

+ {profile.bio ?? "Empty bio."} +

+
+ {me && } +
+
+
+
+

Recipes

+ {me && ( + + )} +
+
+ {profile.recipes?.map((recipe) => ( + + ))} +
+
); } diff --git a/frontend/src/routes/recipe.tsx b/frontend/src/routes/recipe.tsx index 4a3a8449..7e76014a 100644 --- a/frontend/src/routes/recipe.tsx +++ b/frontend/src/routes/recipe.tsx @@ -43,7 +43,6 @@ export default function RecipePage() { }, }); - console.log(data, isLoading, error); if (isLoading) { return ; } diff --git a/frontend/src/routes/search.tsx b/frontend/src/routes/search.tsx index 36c531ec..c03e73ce 100644 --- a/frontend/src/routes/search.tsx +++ b/frontend/src/routes/search.tsx @@ -14,7 +14,11 @@ export const Search = () => { isLoading, error, } = useSearchDishes({ - queryParams: { q: params.get("q") ?? "" }, + queryParams: { + q: params.get("q") ?? "", + ...(params.get("cuisine") ? { cuisine: params.get("cuisine")! } : {}), + ...(params.get("foodType") ? { foodType: params.get("foodType")! } : {}), + }, }); if (isLoading) { diff --git a/frontend/src/services/api/semanticBrowseSchemas.ts b/frontend/src/services/api/semanticBrowseSchemas.ts index 25128184..23113e02 100644 --- a/frontend/src/services/api/semanticBrowseSchemas.ts +++ b/frontend/src/services/api/semanticBrowseSchemas.ts @@ -23,7 +23,7 @@ export type AuthToken = { }; /** - * @example {"id":1,"username":"takoyaki_lover","name":"Takoyaki Lover","bio":"I love takoyaki!","followersCount":100,"gender":"unknown","profilePicture":"https://images.unsplash.com/photo-1633790450512-98e68a55ef15?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=brunno-tozzo-GAIC2WHxm5A-unsplash.jpg&w=640","diets":["keto"],"selfFollowing":true,"recipeCount":10,"bookmarks":[{"id":1,"name":"My Takoyaki Recipe","description":"A delicious takoyaki recipe that I learned from my grandmother.","cookTime":30,"images":["http://commons.wikimedia.org/wiki/Special:FilePath/Takoyaki%20by%20yomi955.jpg"],"rating":4.5,"dish":{"id":"http://www.wikidata.org/entity/Q905527","name":"takoyaki"}}],"recipes":[{"id":1,"name":"My Takoyaki Recipe","description":"A delicious takoyaki recipe that I learned from my grandmother.","cookTime":30,"images":["http://commons.wikimedia.org/wiki/Special:FilePath/Takoyaki%20by%20yomi955.jpg"],"rating":4.5,"dish":{"id":"http://www.wikidata.org/entity/Q905527","name":"takoyaki"}}]} + * @example {"id":1,"username":"takoyaki_lover","name":"Takoyaki Lover","bio":"I love takoyaki!","followersCount":100,"followingCount":100,"gender":"unknown","profilePicture":"https://images.unsplash.com/photo-1633790450512-98e68a55ef15?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=brunno-tozzo-GAIC2WHxm5A-unsplash.jpg&w=640","diets":["keto"],"selfFollowing":true,"recipeCount":10,"bookmarks":[{"id":1,"name":"My Takoyaki Recipe","description":"A delicious takoyaki recipe that I learned from my grandmother.","cookTime":30,"images":["http://commons.wikimedia.org/wiki/Special:FilePath/Takoyaki%20by%20yomi955.jpg"],"rating":4.5,"dish":{"id":"http://www.wikidata.org/entity/Q905527","name":"takoyaki"}}],"recipes":[{"id":1,"name":"My Takoyaki Recipe","description":"A delicious takoyaki recipe that I learned from my grandmother.","cookTime":30,"images":["http://commons.wikimedia.org/wiki/Special:FilePath/Takoyaki%20by%20yomi955.jpg"],"rating":4.5,"dish":{"id":"http://www.wikidata.org/entity/Q905527","name":"takoyaki"}}]} */ export type UserProfile = { id?: number; @@ -31,6 +31,7 @@ export type UserProfile = { name?: string; bio?: string; followersCount?: number; + followingCount?: number; gender?: "male" | "female" | "unknown"; /** * @format uri diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 2c0d9f1b..acdf80ca 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -896,6 +896,29 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-avatar@npm:^1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-avatar@npm:1.0.4" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-context": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + "@radix-ui/react-use-layout-effect": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/608494c53968085bfcf9b987d80c3ec6720bdb65f78591d53e8bba3b360e86366d48a7dee11405dd443f5a3565432184b95bb9d4954bca1922cc9385a942caaf + languageName: node + linkType: hard + "@radix-ui/react-collapsible@npm:1.0.3": version: 1.0.3 resolution: "@radix-ui/react-collapsible@npm:1.0.3" @@ -4298,6 +4321,7 @@ __metadata: "@openapi-codegen/typescript": "npm:^8.0.2" "@radix-ui/react-accordion": "npm:^1.1.2" "@radix-ui/react-aspect-ratio": "npm:^1.0.3" + "@radix-ui/react-avatar": "npm:^1.0.4" "@radix-ui/react-dialog": "npm:^1.0.5" "@radix-ui/react-dropdown-menu": "npm:^2.0.6" "@radix-ui/react-label": "npm:^2.0.2" diff --git a/swagger/openapi.yml b/swagger/openapi.yml index 320b899b..7a662e5c 100644 --- a/swagger/openapi.yml +++ b/swagger/openapi.yml @@ -997,6 +997,8 @@ components: type: string followersCount: type: integer + followingCount: + type: integer gender: type: string enum: [male, female, unknown] @@ -1026,6 +1028,7 @@ components: name: "Takoyaki Lover" bio: "I love takoyaki!" followersCount: 100 + followingCount: 100 gender: unknown profilePicture: "https://images.unsplash.com/photo-1633790450512-98e68a55ef15?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=brunno-tozzo-GAIC2WHxm5A-unsplash.jpg&w=640" diets: ["keto"] From 2f7157573be8d0ec6a34d8ab8e6cb54f55f4b551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Wed, 15 May 2024 14:12:11 +0300 Subject: [PATCH 3/3] fix(frontend): some styles were fixed --- frontend/src/routes/feed.tsx | 20 ++++++++++++++------ frontend/src/routes/profile.tsx | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/frontend/src/routes/feed.tsx b/frontend/src/routes/feed.tsx index 0dd129fd..6e40be24 100644 --- a/frontend/src/routes/feed.tsx +++ b/frontend/src/routes/feed.tsx @@ -2,7 +2,7 @@ import { Recipe } from "../components/Recipe"; import { FullscreenLoading } from "../components/FullscreenLoading"; import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert"; import { AlertCircle } from "lucide-react"; -import { useGetRecipesForEntity } from "../services/api/semanticBrowseComponents"; +import { useGetFeed } from "../services/api/semanticBrowseComponents"; import { renderError } from "../services/api/semanticBrowseFetcher"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import SearchFilterPopover from "@/components/SearchFilterPopover"; @@ -10,14 +10,19 @@ import { useSearchParams } from "react-router-dom"; import { useState } from "react"; export const Feed = () => { - const { data, isLoading, error } = useGetRecipesForEntity({ + const [params, setParams] = useSearchParams(); + const { + data: feedData, + isLoading, + error, + } = useGetFeed({ queryParams: { - sort: "topRated", + type: ["explore", "following"].includes(params.get("type") ?? "") + ? (params.get("type") as "explore" | "following") + : "explore", }, }); - const [params] = useSearchParams(); const [foodType, setFoodType] = useState(params.get("foodType") || ""); - const feedData = { data }; if (isLoading) { return ; @@ -45,7 +50,10 @@ export const Feed = () => {
- + setParams((prev) => ({ ...prev, type: val }))} + > Following Explore diff --git a/frontend/src/routes/profile.tsx b/frontend/src/routes/profile.tsx index 0ec7ed14..f741d32a 100644 --- a/frontend/src/routes/profile.tsx +++ b/frontend/src/routes/profile.tsx @@ -31,7 +31,7 @@ export default function Profile() { const profile = data!.data; return ( -
+

My profile