(FEAT): Continued implementation of the home page.
Making lots of progress
This commit is contained in:
parent
b8ed93fd9e
commit
7c67225f10
@ -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
|
||||||
|
|||||||
BIN
web/src/assets/images/recipe_placeholder.png
Normal file
BIN
web/src/assets/images/recipe_placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
web/src/assets/images/recipe_placeholder_wide.jpg
Normal file
BIN
web/src/assets/images/recipe_placeholder_wide.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
12
web/src/components/Banner.tsx
Normal file
12
web/src/components/Banner.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
web/src/components/buttons/LikeButton.tsx
Normal file
10
web/src/components/buttons/LikeButton.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
web/src/components/cards/ContentCardSmall.tsx
Normal file
17
web/src/components/cards/ContentCardSmall.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
web/src/components/cards/RecipeCardLarge.tsx
Normal file
47
web/src/components/cards/RecipeCardLarge.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
web/src/components/cards/RecipeCardSmall.tsx
Normal file
39
web/src/components/cards/RecipeCardSmall.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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
37
web/src/types/recipe.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user