(FEAT): UI is complete, just need the actions to be implemented.

This commit is contained in:
Hayden Hargreaves 2025-11-19 12:30:56 -07:00
parent cfaace1bfd
commit 2f5dd0dbc4
7 changed files with 138 additions and 23 deletions

View 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>
</>
);
}

View File

@ -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

View File

@ -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

View 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>
</>
);
}

View 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>
</>
);
}

View 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>
);
}

View File

@ -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>
);
}