Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Recipe Card and Feed Page #196

Merged
merged 1 commit into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"react-router-dom": "^6.23.0",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
"vite-plugin-svgr": "^4.2.0",
"zod": "^3.23.7",
"zustand": "^4.5.2"
},
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/components/Recipe.tsx
Original file line number Diff line number Diff line change
@@ -1,0 +1,78 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { useMemo } from "react";

Check warning on line 4 in frontend/src/components/Recipe.tsx

View workflow job for this annotation

GitHub Actions / build

'useMemo' is defined but never used

Check warning on line 4 in frontend/src/components/Recipe.tsx

View workflow job for this annotation

GitHub Actions / build

'useMemo' is defined but never used

Check warning on line 4 in frontend/src/components/Recipe.tsx

View workflow job for this annotation

GitHub Actions / build

'useMemo' is defined but never used

Check warning on line 4 in frontend/src/components/Recipe.tsx

View workflow job for this annotation

GitHub Actions / build

'useMemo' is defined but never used
import LinkIcon from "@/assets/Icon/General/Link.svg";
import BookmarkIcon from "@/assets/Icon/General/Bookmark.svg";
import TimeIcon from "@/assets/Icon/General/Clock.svg";
import StarIcon from "@/assets/Icon/General/Star.svg";
import FoodIcon from "@/assets/Icon/General/Food.svg";
import { DishSummary, UserSummary } from "@/services/api/semanticBrowseSchemas";

interface Recipe {
name: string;
images: string[];
avgRating?: number;
ratingsCount: number;
cookTime: number;
dish: DishSummary;
author: UserSummary;
}

export const Recipe = ({
recipe: { name, images, avgRating, ratingsCount, cookTime, dish, author },
}: {
recipe: Recipe;
}) => {
return (
<div className="flex flex-col self-stretch justify-self-stretch">
<div className="-mb-16 w-[70%] self-center">
<AspectRatio ratio={16 / 9}>
<img src={images[0]} className="h-full w-full rounded-2xl object-cover" alt={name} />
</AspectRatio>
</div>

<Card className="flex flex-1 flex-col bg-gray-100 pt-16">
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle>{name}</CardTitle>
<div className="flex gap-2">
<Button className="bg-transparent" size="icon" variant="secondary">
<img src={LinkIcon} alt="Link icon" />
</Button>
<Button className="bg-transparent" size="icon" variant="secondary">
<img src={BookmarkIcon} alt="Bookmark icon" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="flex flex-1 flex-col justify-between gap-2">
<div className="flex items-center">
<img src={StarIcon} alt="avgRating icon" className="w-4 h-4 mr-2" />
<p className="text-sm text-gray-500">{avgRating} ({ratingsCount} Reviews)</p>
</div>
<div className="flex items-center">
<img src={TimeIcon} alt="Time icon" className="w-4 h-4 mr-2" />
<p className="text-sm text-gray-500">{cookTime}</p>
</div>
{dish && (
<div className="flex items-center">
<img src={FoodIcon} alt="Food icon" className="w-4 h-4 mr-2" />
<p className="text-sm text-gray-500">{dish.name}</p>
</div>
)}
{author && author.profilePicture && (
<div className="flex items-center">
<img src={author.profilePicture} alt="Author" className="w-4 h-4 mr-2" />
</div>
)}
<div className="self-end">
<a className="cursor-not-allowed text-sm font-medium text-blue-500 hover:underline">
Go to recipe →
</a>
</div>
</CardContent>
</Card>
</div>
);
};
80 changes: 80 additions & 0 deletions frontend/src/routes/feed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useState } from "react";
import { Recipe } from "../components/Recipe";
import { FullscreenLoading } from "../components/FullscreenLoading";
import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert";
import { AlertCircle, Filter } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useGetRecipesForEntity, GetRecipesForEntityQueryParams } from "../services/api/semanticBrowseComponents";
import { renderError } from "../services/api/semanticBrowseFetcher";

Check warning on line 9 in frontend/src/routes/feed.tsx

View workflow job for this annotation

GitHub Actions / build

'GetRecipesForEntityQueryParams' is defined but never used

Check warning on line 9 in frontend/src/routes/feed.tsx

View workflow job for this annotation

GitHub Actions / build

'GetRecipesForEntityQueryParams' is defined but never used

Check warning on line 9 in frontend/src/routes/feed.tsx

View workflow job for this annotation

GitHub Actions / build

'GetRecipesForEntityQueryParams' is defined but never used

Check warning on line 9 in frontend/src/routes/feed.tsx

View workflow job for this annotation

GitHub Actions / build

'GetRecipesForEntityQueryParams' is defined but never used
export const Feed = () => {
const navigate = useNavigate();
const [selectedButton, setSelectedButton] = useState<"Following" | "Explore">("Following");
const { data: feedData, isLoading, error } = useGetRecipesForEntity({
queryParams: { sort: selectedButton === "Following" ? "topRated" : "recent" },
});

if (isLoading) {
return <FullscreenLoading overlay />;
}

if (error) {
return (
<div className="container flex flex-col gap-2 py-8">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{renderError(error)}</AlertDescription>
</Alert>
</div>
);
}

return (
<div className="container flex flex-col gap-2 py-8">
<h1 className="text-2xl font-bold">
{feedData?.data?.length
? `Found ${feedData.data.length} results`
: "No recipes found"}
</h1>
<div className="mt-4 flex justify-between items-center">
<div className="flex space-x-4">
<button
className={`px-4 py-2 rounded-md ${
selectedButton === "Following"
? "bg-red-500 text-white"
: "bg-transparent text-red-400"
}`}
onClick={() => setSelectedButton("Following")}
>
Following
</button>
<button
className={`px-4 py-2 rounded-md ${
selectedButton === "Explore"
? "bg-red-500 text-white"
: "bg-transparent text-red-400"
}`}
onClick={() => setSelectedButton("Explore")}
>
Explore
</button>
</div>
<div
className="cursor-pointer"
onClick={() => navigate("/filter")}
>
<Filter className="h-6 w-6 text-gray-700" />
</div>
</div>
<div className="mt-4 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
{feedData?.data?.map((recipe) => (
<Recipe
key={recipe.id}
recipe={recipe}
/>
))}
</div>
</div>
);
};
1 change: 1 addition & 0 deletions frontend/src/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SearchBar } from "../components/SearchBar";
import { Recipe } from "../components/Recipe";

Check warning on line 2 in frontend/src/routes/home.tsx

View workflow job for this annotation

GitHub Actions / build

'Recipe' is defined but never used

Check warning on line 2 in frontend/src/routes/home.tsx

View workflow job for this annotation

GitHub Actions / build

'Recipe' is defined but never used

Check warning on line 2 in frontend/src/routes/home.tsx

View workflow job for this annotation

GitHub Actions / build

'Recipe' is defined but never used

Check warning on line 2 in frontend/src/routes/home.tsx

View workflow job for this annotation

GitHub Actions / build

'Recipe' is defined but never used

export function IndexRoute() {
return (
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Signup from "./signup";
import { IndexRoute } from "./home";
import { signout } from "../services/auth";
import { Search } from "./search";
import { Feed } from "./feed";
import { NavbarLayout } from "../components/NavbarLayout";

export const routes: RouteObject[] = [
Expand All @@ -19,6 +20,10 @@ export const routes: RouteObject[] = [
path: "/search",
Component: Search,
},
{
path: "/feed",
Component: Feed,
},
{
index: true,
Component: IndexRoute,
Expand Down
76 changes: 63 additions & 13 deletions frontend/src/services/api/semanticBrowseComponents.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Generated by @openapi-codegen
*
* @version 1.0.2
* @version 1.0.3
*/
import * as reactQuery from "@tanstack/react-query";
import {
Expand Down Expand Up @@ -418,7 +418,7 @@ export const fetchGetUserById = (
GetUserByIdPathParams
>({ url: "/users/{userId}", method: "get", ...variables, signal });

export const useGetUserById = <TData = GetUserByIdResponse>(
export const useGetUserById = <TData = GetUserByIdResponse,>(
variables: GetUserByIdVariables,
options?: Omit<
reactQuery.UseQueryOptions<GetUserByIdResponse, GetUserByIdError, TData>,
Expand Down Expand Up @@ -506,6 +506,51 @@ export const useUpdateUserById = (
});
};

export type GetMeError = Fetcher.ErrorWrapper<undefined>;

export type GetMeResponse = {
/**
* Internal status code of the response. An HTTP 200 response with an internal 500 status code is an error response. Prioritize the inner status over the HTTP status.
*
* @example 200
* @example 201
*/
status: 200 | 201;
data: Schemas.UserProfile;
};

export type GetMeVariables = SemanticBrowseContext["fetcherOptions"];

export const fetchGetMe = (variables: GetMeVariables, signal?: AbortSignal) =>
semanticBrowseFetch<GetMeResponse, GetMeError, undefined, {}, {}, {}>({
url: "/users/me",
method: "get",
...variables,
signal,
});

export const useGetMe = <TData = GetMeResponse,>(
variables: GetMeVariables,
options?: Omit<
reactQuery.UseQueryOptions<GetMeResponse, GetMeError, TData>,
"queryKey" | "queryFn" | "initialData"
>,
) => {
const { fetcherOptions, queryOptions, queryKeyFn } =
useSemanticBrowseContext(options);
return reactQuery.useQuery<GetMeResponse, GetMeError, TData>({
queryKey: queryKeyFn({
path: "/users/me",
operationId: "getMe",
variables,
}),
queryFn: ({ signal }) =>
fetchGetMe({ ...fetcherOptions, ...variables }, signal),
...options,
...queryOptions,
});
};

export type GetUserFollowingPathParams = {
userId: number;
};
Expand Down Expand Up @@ -543,7 +588,7 @@ export const fetchGetUserFollowing = (
GetUserFollowingPathParams
>({ url: "/users/{userId}/following", method: "get", ...variables, signal });

export const useGetUserFollowing = <TData = GetUserFollowingResponse>(
export const useGetUserFollowing = <TData = GetUserFollowingResponse,>(
variables: GetUserFollowingVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Expand Down Expand Up @@ -610,7 +655,7 @@ export const fetchGetUserFollowers = (
GetUserFollowersPathParams
>({ url: "/users/{userId}/followers", method: "get", ...variables, signal });

export const useGetUserFollowers = <TData = GetUserFollowersResponse>(
export const useGetUserFollowers = <TData = GetUserFollowersResponse,>(
variables: GetUserFollowersVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Expand Down Expand Up @@ -807,7 +852,7 @@ export const fetchSearchUsers = (
{}
>({ url: "/search/users", method: "get", ...variables, signal });

export const useSearchUsers = <TData = SearchUsersResponse>(
export const useSearchUsers = <TData = SearchUsersResponse,>(
variables: SearchUsersVariables,
options?: Omit<
reactQuery.UseQueryOptions<SearchUsersResponse, SearchUsersError, TData>,
Expand Down Expand Up @@ -868,7 +913,7 @@ export const fetchSearchDishes = (
{}
>({ url: "/search/dishes", method: "get", ...variables, signal });

export const useSearchDishes = <TData = SearchDishesResponse>(
export const useSearchDishes = <TData = SearchDishesResponse,>(
variables: SearchDishesVariables,
options?: Omit<
reactQuery.UseQueryOptions<SearchDishesResponse, SearchDishesError, TData>,
Expand Down Expand Up @@ -927,7 +972,7 @@ export const fetchGetDishById = (
GetDishByIdPathParams
>({ url: "/dishes/{dishId}", method: "get", ...variables, signal });

export const useGetDishById = <TData = GetDishByIdResponse>(
export const useGetDishById = <TData = GetDishByIdResponse,>(
variables: GetDishByIdVariables,
options?: Omit<
reactQuery.UseQueryOptions<GetDishByIdResponse, GetDishByIdError, TData>,
Expand Down Expand Up @@ -991,7 +1036,7 @@ export const fetchGetCuisineById = (
GetCuisineByIdPathParams
>({ url: "/cuisines/{cuisineId}", method: "get", ...variables, signal });

export const useGetCuisineById = <TData = GetCuisineByIdResponse>(
export const useGetCuisineById = <TData = GetCuisineByIdResponse,>(
variables: GetCuisineByIdVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Expand Down Expand Up @@ -1130,7 +1175,7 @@ export const fetchGetRecipesForEntity = (
{}
>({ url: "/recipes", method: "get", ...variables, signal });

export const useGetRecipesForEntity = <TData = GetRecipesForEntityResponse>(
export const useGetRecipesForEntity = <TData = GetRecipesForEntityResponse,>(
variables: GetRecipesForEntityVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Expand Down Expand Up @@ -1252,7 +1297,7 @@ export const fetchGetRecipeById = (
GetRecipeByIdPathParams
>({ url: "/recipes/{recipeId}", method: "get", ...variables, signal });

export const useGetRecipeById = <TData = GetRecipeByIdResponse>(
export const useGetRecipeById = <TData = GetRecipeByIdResponse,>(
variables: GetRecipeByIdVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Expand Down Expand Up @@ -1436,7 +1481,7 @@ export const fetchGetBookmarkers = (
signal,
});

export const useGetBookmarkers = <TData = GetBookmarkersResponse>(
export const useGetBookmarkers = <TData = GetBookmarkersResponse,>(
variables: GetBookmarkersVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Expand Down Expand Up @@ -1614,7 +1659,7 @@ export const fetchGetCommentsForRecipe = (
signal,
});

export const useGetCommentsForRecipe = <TData = GetCommentsForRecipeResponse>(
export const useGetCommentsForRecipe = <TData = GetCommentsForRecipeResponse,>(
variables: GetCommentsForRecipeVariables,
options?: Omit<
reactQuery.UseQueryOptions<
Expand Down Expand Up @@ -1880,7 +1925,7 @@ export const fetchGetFeed = (
{}
>({ url: "/feed", method: "get", ...variables, signal });

export const useGetFeed = <TData = GetFeedResponse>(
export const useGetFeed = <TData = GetFeedResponse,>(
variables: GetFeedVariables,
options?: Omit<
reactQuery.UseQueryOptions<GetFeedResponse, GetFeedError, TData>,
Expand All @@ -1904,6 +1949,11 @@ export type QueryOperation =
operationId: "getUserById";
variables: GetUserByIdVariables;
}
| {
path: "/users/me";
operationId: "getMe";
variables: GetMeVariables;
}
| {
path: "/users/{userId}/following";
operationId: "getUserFollowing";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/services/api/semanticBrowseResponses.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Generated by @openapi-codegen
*
* @version 1.0.2
* @version 1.0.3
*/
import type * as Schemas from "./semanticBrowseSchemas";

Expand Down
Loading
Loading