(FEAT): Continued implementation of the home page.

Making lots of progress
This commit is contained in:
Hayden Hargreaves 2025-10-30 13:10:50 -07:00
parent b8ed93fd9e
commit 7c67225f10
11 changed files with 382 additions and 18 deletions

View File

@ -24,7 +24,6 @@
templ templ
tailwindcss_4 tailwindcss_4
tailwindcss-language-server tailwindcss-language-server
tailwindcss-language-server:
watchman watchman
docker-language-server docker-language-server
dockerfile-language-server-nodejs dockerfile-language-server-nodejs

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -0,0 +1,12 @@
interface BannerProps {
content: string;
};
export default function Banner({ content }: BannerProps) {
return (
<h2 className="text-xl md:text-2xl bg-gradient-to-r from-blue-400 to-blue-600 w-full h-fit py-6 text-center text-white">
{content}
</h2>
);
}

View File

@ -0,0 +1,10 @@
export default function LikeButton() {
return (
<svg className="h-6 text-red-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M2 9.1371C2 14 6.01943 16.5914 8.96173 18.9109C10 19.7294 11 20.5 12 20.5C13 20.5 14 19.7294 15.0383 18.9109C17.9806 16.5914 22 14 22 9.1371C22 4.27416 16.4998 0.825464 12 5.50063C7.50016 0.825464 2 4.27416 2 9.1371Z"
fill="currentColor"
></path>
</svg>
);
}

View File

@ -0,0 +1,17 @@
interface ContentCardSmallProps {
content: string;
target: string;
};
export default function ContentCardSmall({ content, target }: ContentCardSmallProps) {
return (
<div className="flex flex-col items-center justify-center rounded-lg border-gray-300 border shadow-md p-4 flex-shrink-0">
<div className="mt-8 w-52 md:w-48 text-center">
<a className="underline" href={target}>
<p className="text-sm">{content}</p>
</a>
</div>
</div>
);
}

View File

@ -0,0 +1,47 @@
import type { Recipe } from "../../types/recipe";
import RecipePlaceholder from "../../assets/images/recipe_placeholder.png"
import LikeButton from "../buttons/LikeButton";
interface RecipeCardLargeProps {
recipe: Recipe | null;
}
export default function RecipeCardLarge({ recipe }: RecipeCardLargeProps) {
// HANDLERS
const makeButtonHandler = () => console.log("makeButtonHandler()");
if (recipe == null) {
return <h2 className="text-2xl md:text-3xl text-gray-400">Coming soon!</h2>
}
return (
<div className="flex flex-col items-center justify-between rounded-lg border-gray-300 border shadow-md p-4 flex-shrink-0">
<img className="size-80 rounded-sm" src={RecipePlaceholder} />
<div className="w-full mt-8">
<h2 className="font-semibold overflow-hidden whitespace-nowrap text-ellipsis">
{recipe.Title}
</h2>
<p className="text-xs overflow-hidden whitespace-nowrap text-ellipsis">
Serves {recipe.Serves}
</p>
<p className="text-sm text-wrap w-80">
{recipe.Description}
</p>
<div className="flex items-end justify-between">
<p className="text-xs mt-4 bg-gray-200 rounded-lg w-fit px-2 py-1">
{recipe.Category} - {recipe.Duration.Total} mins
</p>
{recipe.Favorite && <LikeButton />}
</div>
<button
onClick={makeButtonHandler}
className="w-full rounded-lg py-2 bg-gradient-to-r from-blue-400 to-blue-600 text-white mt-2 hover:ring-blue-700 hover:shadow shadow-blue-300 duration-200 cursor-pointer"
>
Make Now!
</button>
</div>
</div>
);
}

View File

@ -0,0 +1,39 @@
import type { Recipe } from "../../types/recipe";
import RecipePlaceholder from "../../assets/images/recipe_placeholder.png"
import LikeButton from "../buttons/LikeButton";
interface RecipeCardSmallProps {
recipe: Recipe;
}
export default function RecipeCardSmall({ recipe }: RecipeCardSmallProps) {
// HANDLERS
const makeButtonHandler = () => console.log("makeButtonHandler()");
return (
<div className="flex flex-col items-center justify-between rounded-lg border-gray-300 border shadow-md p-4 flex-shrink-0">
<img className="size-52 md:size-48 rounded-sm" src={RecipePlaceholder} />
<div className="w-52 md:w-48 mt-8">
<h2 className="font-semibold overflow-hidden whitespace-nowrap text-ellipsis">
{recipe.Title}
</h2>
<p className="text-xs overflow-hidden whitespace-nowrap text-ellipsis">
Serves {recipe.Serves}
</p>
<div className="flex items-end justify-between">
<p className="text-xs mt-4 bg-gray-200 rounded-lg w-fit px-2 py-1">
{recipe.Category} - {recipe.Duration.Total} mins
</p>
{recipe.Favorite && <LikeButton />}
</div>
<button
onClick={makeButtonHandler}
className="w-full rounded-lg py-2 bg-gradient-to-r from-blue-400 to-blue-600 text-white mt-2 hover:ring-blue-700 hover:shadow shadow-blue-300 duration-200 cursor-pointer"
>
Make Now!
</button>
</div>
</div>
);
}

View File

@ -1,7 +1,112 @@
import { Fragment, useEffect, useState } from "react";
import SalmonVideo from "../assets/videos/salmon_video.mp4"; import SalmonVideo from "../assets/videos/salmon_video.mp4";
import Banner from "../components/Banner";
import ROUTE_CONSTANTS from "../types/routes";
import RecipeLarge from "../components/cards/RecipeCardLarge";
import type { Recipe } from "../types/recipe";
import RecipeCardSmall from "../components/cards/RecipeCardSmall";
import ContentCardSmall from "../components/cards/ContentCardSmall";
export default function Home() { export default function Home() {
const [loggedIn, isLoggedIn] = useState<boolean>(false);
const [recipeOfTheWeek, setRecipeOfTheWeek] = useState<Recipe | null>(null);
const [madeRecipes, setMadeRecipes] = useState<Recipe[]>([]);
const [viewedRecipes, setViewedRecipes] = useState<Recipe[]>([]);
// BUG: Remove these
useEffect(() => {
const recipe: Recipe = {
Id: 1,
Title: "Classic Pancakes",
Description: "Fluffy and delicious pancakes perfect for breakfast.",
Instructions: [
"In a bowl, mix all the dry ingredients.",
"In another bowl, whisk the wet ingredients.",
"Combine both mixes until smooth.",
"Heat a non-stick skillet and pour batter.",
"Cook until bubbles form, flip and cook the other side.",
"Serve warm with syrup."
],
Serves: 4,
Difficulty: 2, // scale 1-5 (example)
Duration: {
Total: 20,
Prep: 5,
Cook: 15
},
Category: "breakfast",
Ingredients: [
{ Name: "Flour", Quantity: "2 cups" },
{ Name: "Milk", Quantity: "1.5 cups" },
{ Name: "Egg", Quantity: "1 large" },
{ Name: "Baking Powder", Quantity: "2 teaspoons" },
{ Name: "Salt", Quantity: "0.5 teaspoon" },
{ Name: "Sugar", Quantity: "1 tablespoon" }
],
UserId: 101,
Modified: new Date("2025-10-30T09:00:00"),
Created: new Date("2025-10-01T08:30:00"),
Tags: [
{ Id: 1, Name: "easy", Created: new Date("2025-01-01T12:00:00") },
{ Id: 2, Name: "quick", Created: new Date("2025-01-02T12:00:00") },
{ Id: 3, Name: "breakfast", Created: new Date("2025-01-03T12:00:00") }
],
Favorite: true
};
const recipe2: Recipe = {
Id: 2,
Title: "Classic Pancakes",
Description: "Fluffy and delicious pancakes perfect for breakfast.",
Instructions: [
"In a bowl, mix all the dry ingredients.",
"In another bowl, whisk the wet ingredients.",
"Combine both mixes until smooth.",
"Heat a non-stick skillet and pour batter.",
"Cook until bubbles form, flip and cook the other side.",
"Serve warm with syrup."
],
Serves: 4,
Difficulty: 2, // scale 1-5 (example)
Duration: {
Total: 20,
Prep: 5,
Cook: 15
},
Category: "breakfast",
Ingredients: [
{ Name: "Flour", Quantity: "2 cups" },
{ Name: "Milk", Quantity: "1.5 cups" },
{ Name: "Egg", Quantity: "1 large" },
{ Name: "Baking Powder", Quantity: "2 teaspoons" },
{ Name: "Salt", Quantity: "0.5 teaspoon" },
{ Name: "Sugar", Quantity: "1 tablespoon" }
],
UserId: 101,
Modified: new Date("2025-10-30T09:00:00"),
Created: new Date("2025-10-01T08:30:00"),
Tags: [
{ Id: 1, Name: "easy", Created: new Date("2025-01-01T12:00:00") },
{ Id: 2, Name: "quick", Created: new Date("2025-01-02T12:00:00") },
{ Id: 3, Name: "breakfast", Created: new Date("2025-01-03T12:00:00") }
],
Favorite: true
};
isLoggedIn(true);
setRecipeOfTheWeek(recipe);
const recipes: Recipe[] = [recipe, recipe2];
setMadeRecipes(recipes);
setViewedRecipes(recipes);
}, []);
return ( return (
<>
{/* Intro Section */}
<section className="w-full h-fit mb-16"> <section className="w-full h-fit mb-16">
<div className="relative"> <div className="relative">
<video autoPlay loop muted playsInline> <video autoPlay loop muted playsInline>
@ -19,5 +124,100 @@ export default function Home() {
browse our trending dishes for fresh ideas. browse our trending dishes for fresh ideas.
</p> </p>
</section> </section>
{/* Search Section */}
<section className="w-full flex flex-col items-center justify-center my-8 py-4">
<Banner content="Craving Something Specific?" />
<div className="w-full md:w-3/4">
{/* TODO: Create this */}
{/* @components.SearchBar(filters, true, false, false) */}
</div>
<div className="hidden" id="result-list"></div>
</section>
{/* Highlight Section */}
<section className="w-full flex flex-col items-center justify-center my-8 py-4">
<Banner content="Recipe of the Week!" />
<p className="leading-relaxed p-4 my-8">
Our 'Recipe of the Week' is the cream of the crop! We handpick it by looking at what recipes
our community loves most. This isn't just about how many people view a recipe; it's also about
how many times it's been made, liked, reviewed, and its average rating, all combined to find
the true fan favorite of the week. It's our way of highlighting the best recipes that truly
resonate with our users!
</p>
<div className="flex items-center justify-center w-full">
<RecipeLarge recipe={recipeOfTheWeek} />
</div>
</section>
{/* Lists Section */}
<section className="w-full flex flex-col items-center justify-center my-8 py-4">
<Banner content="Take Another Look." />
<div className="w-full">
<h3 className="text-lg mt-8 mx-4">Recently viewed</h3>
{loggedIn ?
<div className="flex overflow-x-auto gap-x-4 mx-4 my-4">
{viewedRecipes && viewedRecipes.length > 0 ? (
<>
{viewedRecipes.map((recipe: Recipe) => (
<RecipeCardSmall key={recipe.Id} recipe={recipe} />
))}
<ContentCardSmall content="View full history..." target={ROUTE_CONSTANTS.History} />
</>
) : (
<p className="text-sm">No recently viewed recipes</p>
)}
</div>
:
<div className="my-2 mx-4 text-gray-800">
<a className="underline" href={ROUTE_CONSTANTS.Login}>
<p className="text-sm">Log in to view metrics.</p>
</a>
</div>
}
<h3 className="text-lg mt-8 mx-4">Make again</h3>
{loggedIn ?
<div className="flex overflow-x-auto gap-x-4 mx-4 my-4">
{madeRecipes && madeRecipes.length > 0 ? (
<>
{madeRecipes.map((recipe: Recipe) => (
<RecipeCardSmall key={recipe.Id} recipe={recipe} />
))}
<ContentCardSmall content="View full history..." target={ROUTE_CONSTANTS.History} />
</>
) : (
<p className="text-sm">No recently made recipes</p>
)}
{/* } */}
</div>
:
<div className="my-2 mx-4 text-gray-800">
<a className="underline" href={ROUTE_CONSTANTS.Login}>
<p className="text-sm">Log in to view metrics.</p>
</a>
</div>
}
</div>
</section >
{/* Call-to-Action Section */}
< section
className="w-full flex flex-col items-center justify-center mt-16 py-8 md:py-12 bg-gradient-to-br from-blue-100 to-purple-100 text-center" >
<h2 className="text-2xl md:text-3xl font-extrabold text-gray-800 mb-6 px-4">
Unleash Your Inner Chef!
</h2>
<p className="text-md md:text-lg text-gray-700 max-w-2xl mb-10 px-4 leading-relaxed">
Have a unique recipe idea? Want to share your culinary masterpiece with the world?
It's time to bring your creations to life!
</p>
<a href={ROUTE_CONSTANTS.Create} className="flex items-center justify-center
bg-gradient-to-r from-blue-400 to-blue-600 text-white
px-12 py-5 rounded-full shadow-sm hover:shadow-md
transition-all duration-300 ease-in-out shadow-blue-700
text-lg md:text-2xl font-bold uppercase tracking-wide">
Create Your Recipe!
</a>
</section >
</>
); );
} }

37
web/src/types/recipe.ts Normal file
View File

@ -0,0 +1,37 @@
export interface RecipeDuration {
Total: number;
Prep: number;
Cook: number;
}
// BUG: This might need to be integers? Not sure yet.
export type RecipeMeal = "breakfast" | "lunch" | "dinner" | "dessert" | "snack" | "side" | "other";
export interface RecipeIngredient {
Name: string;
Quantity: string;
}
export interface Tag {
Id: number;
Name: string;
Created: Date;
}
export interface Recipe {
Id: number;
Title: string;
Description: string;
Instructions: string[];
Serves: number;
Difficulty: number;
Duration: RecipeDuration;
Category: RecipeMeal;
Ingredients: RecipeIngredient[];
UserId: number;
Modified: Date;
Created: Date;
Tags: Tag[];
Favorite: boolean;
}

View File

@ -6,13 +6,16 @@ const ROUTE_CONSTANTS: {
Create: string; Create: string;
Profile: string; Profile: string;
ShoppingList: string; ShoppingList: string;
Login: string;
History: string;
} = { } = {
Home: `${VERSION_FLAG}/web/home`, Home: `${VERSION_FLAG}/web/home`,
Favorites: `${VERSION_FLAG}/web/favorites`, Favorites: `${VERSION_FLAG}/web/favorites`,
Create: `${VERSION_FLAG}/web/create`, Create: `${VERSION_FLAG}/web/create`,
Profile: `${VERSION_FLAG}/web/profile`, Profile: `${VERSION_FLAG}/web/profile`,
ShoppingList: `${VERSION_FLAG}/web/list`, ShoppingList: `${VERSION_FLAG}/web/list`,
Login: `${VERSION_FLAG}/web/login`,
History: `${VERSION_FLAG}/web/history`,
}; };
export default ROUTE_CONSTANTS; export default ROUTE_CONSTANTS;