(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
|
||||
tailwindcss_4
|
||||
tailwindcss-language-server
|
||||
tailwindcss-language-server:
|
||||
watchman
|
||||
docker-language-server
|
||||
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 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() {
|
||||
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 (
|
||||
<>
|
||||
{/* Intro Section */}
|
||||
<section className="w-full h-fit mb-16">
|
||||
<div className="relative">
|
||||
<video autoPlay loop muted playsInline>
|
||||
@ -19,5 +124,100 @@ export default function Home() {
|
||||
browse our trending dishes for fresh ideas.
|
||||
</p>
|
||||
</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;
|
||||
Profile: string;
|
||||
ShoppingList: string;
|
||||
Login: string;
|
||||
History: string;
|
||||
} = {
|
||||
Home: `${VERSION_FLAG}/web/home`,
|
||||
Favorites: `${VERSION_FLAG}/web/favorites`,
|
||||
Create: `${VERSION_FLAG}/web/create`,
|
||||
Profile: `${VERSION_FLAG}/web/profile`,
|
||||
ShoppingList: `${VERSION_FLAG}/web/list`,
|
||||
|
||||
Login: `${VERSION_FLAG}/web/login`,
|
||||
History: `${VERSION_FLAG}/web/history`,
|
||||
};
|
||||
|
||||
export default ROUTE_CONSTANTS;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user