(FEAT): UI is complete, just need the actions to be implemented.
This commit is contained in:
parent
cfaace1bfd
commit
2f5dd0dbc4
12
web/src/components/Spinner.tsx
Normal file
12
web/src/components/Spinner.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
interface SpinnerProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export default function Spinner({ content }: SpinnerProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-8 h-8 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin" />
|
||||
<h2 className="text-xl text-gray-700"> { content }</h2>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -2,14 +2,14 @@ import { useState } from "react";
|
||||
|
||||
|
||||
interface MadeButtonProps {
|
||||
id: number | undefined;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default function MadeButton({ id }: MadeButtonProps) {
|
||||
const [clicked, setClicked] = useState<boolean>(false);
|
||||
|
||||
const clickHandler = () => {
|
||||
if (!id || clicked) return;
|
||||
if (clicked) return;
|
||||
|
||||
// TODO: Implement actions
|
||||
|
||||
|
||||
@ -2,14 +2,14 @@ import { useState } from "react";
|
||||
|
||||
|
||||
interface ShareButtonProps {
|
||||
id: number | undefined;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default function ShareButton({ id }: ShareButtonProps) {
|
||||
const [clicked, setClicked] = useState<boolean>(false);
|
||||
|
||||
const clickHandler = () => {
|
||||
if (!id || clicked) return;
|
||||
if (clicked) return;
|
||||
|
||||
// TODO: Implement action here
|
||||
|
||||
|
||||
34
web/src/components/items/IngredientList.tsx
Normal file
34
web/src/components/items/IngredientList.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import type { RecipeIngredient } from "../../types/recipe";
|
||||
|
||||
interface IngredientListProps {
|
||||
ingredients: RecipeIngredient[];
|
||||
}
|
||||
|
||||
export default function IngredientList({ ingredients }: IngredientListProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="px-4 py-8 md:px-8">
|
||||
<h2 className="text-2xl text-gray-800 font-semibold mb-2">Ingredients</h2>
|
||||
<hr className="text-gray-300" />
|
||||
<ul className="text-lg my-4 text-gray-700">
|
||||
{ingredients?.map(ingredient => (
|
||||
<li key={ingredient.Name} className="p-2 hover:bg-gray-100 transition-all duration-300 rounded-sm flex items-center justify-start odd:bg-[#f8f8f8]">
|
||||
<span className="mr-4">
|
||||
<svg className="h-4 text-gray-400" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="font-semibold mr-2">{ingredient.Quantity}: </span> {ingredient.Name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
24
web/src/components/items/InstructionList.tsx
Normal file
24
web/src/components/items/InstructionList.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
interface InstructionListProps {
|
||||
instructions: string[];
|
||||
}
|
||||
export default function InstructionList({ instructions }: InstructionListProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="px-4 py-8 md:px-8">
|
||||
<h2 className="text-2xl text-gray-800 font-semibold mb-2">Instructions</h2>
|
||||
<hr className="text-gray-300"/>
|
||||
<ul className="text-lg my-4 text-gray-700">
|
||||
{instructions?.map((instruction, i) => (
|
||||
<li key={instruction} className="p-4 flex items-start gap-x-4 odd:bg-[#f8f8f8]">
|
||||
<div className="size-8 md:size-10 bg-blue-50 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<h3 className="text-base md:text-xl text-blue-600 font-semibold">{ i + 1}</h3>
|
||||
</div>
|
||||
<p className="text-base">{ instruction }</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
43
web/src/components/items/TagList.tsx
Normal file
43
web/src/components/items/TagList.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import type { Tag } from "../../types/recipe";
|
||||
|
||||
interface TagListProps {
|
||||
tags: Tag[]
|
||||
created: Date
|
||||
modified: Date | null;
|
||||
}
|
||||
|
||||
function FormatDate(date: Date): string {
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit"
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export default function TagList({ tags, created, modified }: TagListProps) {
|
||||
|
||||
|
||||
return (
|
||||
<div className="px-4 py-4 md:px-8">
|
||||
{tags && (
|
||||
<>
|
||||
<h2 className="text-2xl text-gray-800 font-semibold mb-2">Tags</h2>
|
||||
<hr className="text-gray-300" />
|
||||
<ul id="tag-list" className="my-4 flex gap-1 flex-wrap">
|
||||
{tags.map(tag => (
|
||||
<li key={tag.Id} className="text-sm items-center bg-blue-100 text-blue-700 w-fit px-3 py-1.5 rounded-full">
|
||||
{tag.Name}
|
||||
</li>
|
||||
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
<hr className="text-gray-300" />
|
||||
<p className="my-4 mb-1.5 text-sm text-gray-700">Created: {FormatDate(new Date(created))}</p>
|
||||
{modified && (
|
||||
<p className="mb-4 text-sm text-gray-700">Last Modified: {FormatDate(new Date(modified))}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,22 +1,20 @@
|
||||
import { use, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { isApiError, type ApiError } from "../types/api/error";
|
||||
import { GetRecipe } from "../services/RecipeService";
|
||||
import type { Recipe } from "../types/recipe";
|
||||
import { AuthContext } from "../context/AuthContext";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import RecipePlaceholder from "../assets/images/recipe_placeholder_wide.jpg"
|
||||
import TimeIcon from "../components/icons/TimeIcon";
|
||||
import StarIcon from "../components/icons/StarIcon";
|
||||
import RecipeMetaData from "../components/display/RecipeMetaData";
|
||||
import MadeButton from "../components/buttons/MadeButton";
|
||||
import ShareButton from "../components/buttons/ShareButton";
|
||||
import FavoriteButton from "../components/buttons/FavoriteButton";
|
||||
import TagList from "../components/items/TagList";
|
||||
import IngredientList from "../components/items/IngredientList";
|
||||
import InstructionList from "../components/items/InstructionList";
|
||||
import Spinner from "../components/Spinner";
|
||||
|
||||
export default function RecipePage() {
|
||||
// Context
|
||||
const { isLoggedIn } = use(AuthContext);
|
||||
|
||||
// Url params
|
||||
const { id } = useParams();
|
||||
|
||||
@ -42,28 +40,32 @@ export default function RecipePage() {
|
||||
console.error(error);
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
return recipe ? (
|
||||
<>
|
||||
<img className="bg-gray-100 w-full h-96 mx-auto mb-8" src={RecipePlaceholder} />
|
||||
<div className="px-4 py-8 md:px-8">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-800">{recipe?.Title ?? "Loading..."}</h1>
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-gray-800">{recipe.Title}</h1>
|
||||
<p className="text-sm mt-2 mb-1 text-gray-700">Author: loading...</p>
|
||||
<p className="text-sm mb-2 text-gray-700">Category: {recipe?.Category ?? "loading..."}</p>
|
||||
<p className="text-sm mb-2 text-gray-700">Category: {recipe.Category}</p>
|
||||
</div>
|
||||
<RecipeMetaData recipe={recipe} />
|
||||
<section className="w-full flex flex-col md:flex-row gap-x-4 gap-y-2 py-8 px-4 md:px-8">
|
||||
<FavoriteButton favorite={recipe?.Favorite} id={recipe?.Id} />
|
||||
<MadeButton id={recipe?.Id} />
|
||||
<ShareButton id={recipe?.Id} />
|
||||
<FavoriteButton favorite={recipe.Favorite} id={recipe.Id} />
|
||||
<MadeButton id={recipe.Id} />
|
||||
<ShareButton id={recipe.Id} />
|
||||
</section>
|
||||
<div className="px-4 py-8 md:px-8">
|
||||
<h3 className="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
||||
<p className="text-gray-700">{recipe?.Description ?? "loading..."}</p>
|
||||
<p className="text-gray-700">{recipe.Description}</p>
|
||||
</div>
|
||||
@ingredientList(recipe.Ingredients)
|
||||
@instructionList(recipe.Instructions)
|
||||
@tagList(recipe.Tags, recipe.Created, recipe.Modified)
|
||||
|
||||
<IngredientList ingredients={recipe.Ingredients} />
|
||||
<InstructionList instructions={recipe.Instructions} />
|
||||
<TagList tags={recipe.Tags} created={recipe.Created} modified={recipe.Modified} />
|
||||
</>
|
||||
)
|
||||
) : (
|
||||
/* TODO: Implement a loading page! */
|
||||
<div className="min-h-[90vh] flex items-center justify-center gap-x-4">
|
||||
<Spinner content={"Recipe is loading..."} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user