(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 {
|
interface MadeButtonProps {
|
||||||
id: number | undefined;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MadeButton({ id }: MadeButtonProps) {
|
export default function MadeButton({ id }: MadeButtonProps) {
|
||||||
const [clicked, setClicked] = useState<boolean>(false);
|
const [clicked, setClicked] = useState<boolean>(false);
|
||||||
|
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
if (!id || clicked) return;
|
if (clicked) return;
|
||||||
|
|
||||||
// TODO: Implement actions
|
// TODO: Implement actions
|
||||||
|
|
||||||
|
|||||||
@ -2,14 +2,14 @@ import { useState } from "react";
|
|||||||
|
|
||||||
|
|
||||||
interface ShareButtonProps {
|
interface ShareButtonProps {
|
||||||
id: number | undefined;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ShareButton({ id }: ShareButtonProps) {
|
export default function ShareButton({ id }: ShareButtonProps) {
|
||||||
const [clicked, setClicked] = useState<boolean>(false);
|
const [clicked, setClicked] = useState<boolean>(false);
|
||||||
|
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
if (!id || clicked) return;
|
if (clicked) return;
|
||||||
|
|
||||||
// TODO: Implement action here
|
// 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 { isApiError, type ApiError } from "../types/api/error";
|
||||||
import { GetRecipe } from "../services/RecipeService";
|
import { GetRecipe } from "../services/RecipeService";
|
||||||
import type { Recipe } from "../types/recipe";
|
import type { Recipe } from "../types/recipe";
|
||||||
import { AuthContext } from "../context/AuthContext";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
import RecipePlaceholder from "../assets/images/recipe_placeholder_wide.jpg"
|
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 RecipeMetaData from "../components/display/RecipeMetaData";
|
||||||
import MadeButton from "../components/buttons/MadeButton";
|
import MadeButton from "../components/buttons/MadeButton";
|
||||||
import ShareButton from "../components/buttons/ShareButton";
|
import ShareButton from "../components/buttons/ShareButton";
|
||||||
import FavoriteButton from "../components/buttons/FavoriteButton";
|
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() {
|
export default function RecipePage() {
|
||||||
// Context
|
|
||||||
const { isLoggedIn } = use(AuthContext);
|
|
||||||
|
|
||||||
// Url params
|
// Url params
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
@ -42,28 +40,32 @@ export default function RecipePage() {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
return (
|
return recipe ? (
|
||||||
<>
|
<>
|
||||||
<img className="bg-gray-100 w-full h-96 mx-auto mb-8" src={RecipePlaceholder} />
|
<img className="bg-gray-100 w-full h-96 mx-auto mb-8" src={RecipePlaceholder} />
|
||||||
<div className="px-4 py-8 md:px-8">
|
<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 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>
|
</div>
|
||||||
<RecipeMetaData recipe={recipe} />
|
<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">
|
<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} />
|
<FavoriteButton favorite={recipe.Favorite} id={recipe.Id} />
|
||||||
<MadeButton id={recipe?.Id} />
|
<MadeButton id={recipe.Id} />
|
||||||
<ShareButton id={recipe?.Id} />
|
<ShareButton id={recipe.Id} />
|
||||||
</section>
|
</section>
|
||||||
<div className="px-4 py-8 md:px-8">
|
<div className="px-4 py-8 md:px-8">
|
||||||
<h3 className="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
<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>
|
</div>
|
||||||
@ingredientList(recipe.Ingredients)
|
<IngredientList ingredients={recipe.Ingredients} />
|
||||||
@instructionList(recipe.Instructions)
|
<InstructionList instructions={recipe.Instructions} />
|
||||||
@tagList(recipe.Tags, recipe.Created, recipe.Modified)
|
<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