Compare commits

...

3 Commits

Author SHA1 Message Date
Hayden Hargreaves
5db803d033 (FEAT): Create page translate, but still not functionality.
This one will be very difficult to translate.
2025-11-13 20:13:46 -07:00
Hayden Hargreaves
c9be9876e3 (FEAT): Profile page is complete, minus functionality 2025-11-13 19:57:10 -07:00
Hayden Hargreaves
45a0d0e54c (FEAT): Favorites list completed, roughly.
No functionality still.
2025-11-13 14:22:10 -07:00
11 changed files with 840 additions and 3 deletions

View File

@ -0,0 +1,16 @@
export default function ServingSizeIconSmall() {
return <>
<svg className="h-5 text-blue-600" fill="currentColor" version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32" xmlSpace="preserve">
<g>
<circle cx="12" cy="16" r="5"></circle>
<path d="M12,6C6.5,6,2,10.5,2,16s4.5,10,10,10s10-4.5,10-10S17.5,6,12,6z M12,23c-3.9,0-7-3.1-7-7s3.1-7,7-7s7,3.1,7,7
S15.9,23,12,23z"></path>
<path d="M30,10.5V5c0-0.6-0.4-1-1-1s-1,0.4-1,1v5.5c0,0.2,0,0.4,0,0.5h-1V5c0-0.6-0.4-1-1-1s-1,0.4-1,1v6h-1c0-0.2,0-0.4,0-0.5V5
c0-0.6-0.4-1-1-1s-1,0.4-1,1v5.5c0,1.9,0.5,3.4,1.4,4.3c0.7,0.8,1,1.8,0.9,2.7l-1,7.3c-0.1,0.8,0.1,1.6,0.6,2.2S25.2,28,26,28
s1.5-0.3,2.1-0.9s0.8-1.4,0.6-2.2l-1-7.3c-0.1-1,0.2-2,0.9-2.8C29.5,13.8,30,12.3,30,10.5z"></path>
</g>
</svg>
</>
}

View File

@ -0,0 +1,25 @@
interface StarIconSmallProps {
filled: boolean;
};
export default function StarIconSmall({ filled }: StarIconSmallProps) {
return <>
{filled ? (
<svg className="h-4 text-blue-600" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M23.632 9.201a.628.628 0 0 1-.22.678l-5.726 4.96 1.727 7.394a.606.606 0 0 1-.935.676l-6.503-3.953-6.503 3.953a.713.713 0 0 1-.374.112.57.57 0 0 1-.34-.109.629.629 0 0 1-.222-.679l1.729-7.393L.539 9.879A.607.607 0 0 1 .897 8.78l7.536-.635 2.965-7.083a.62.62 0 0 1 1.155.001l2.965 7.082 7.536.635a.63.63 0 0 1 .578.42z">
</path>
<path fill="none" d="M0 0h24v24H0z"></path>
</svg>
) : (
<svg className="h-4 text-gray-500" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M23.054 8.781l-7.536-.635-2.965-7.082a.619.619 0 0 0-1.155 0L8.433 8.145.896 8.78a.607.607 0 0 0-.357 1.1l5.726 4.96-1.729 7.395a.63.63 0 0 0 .223.679.573.573 0 0 0 .339.108.717.717 0 0 0 .374-.111l6.503-3.954 6.503 3.953a.606.606 0 0 0 .935-.677l-1.727-7.392 5.725-4.96a.607.607 0 0 0-.357-1.099zm-6.48 5.698l1.662 7.113-6.261-3.806-6.262 3.807 1.663-7.114-5.513-4.776 7.257-.611 2.855-6.817 2.855 6.817 7.257.611z">
</path>
<path fill="none" d="M0 0h24v24H0z"></path>
</svg>
)}
</>
}

View File

@ -0,0 +1,9 @@
export default function TimeIconSmall() {
return <>
<svg className="h-5 text-blue-600" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 7V12L14.5 13.5M21 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" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</>;
}

View File

@ -0,0 +1,19 @@
import type { Engagement } from "../../types/engagement";
interface ActivityListItemProps {
engagement: Engagement;
}
export default function ActivityListItem({ engagement }: ActivityListItemProps) {
return <>
<li className="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150 flex justify-between items-center">
<p className="text-sm md:text-base text-gray-800">
{engagement.Message}
</p>
<p className="text-xs md:text-sm text-gray-600 w-fit shrink-0">
{engagement.Created.toLocaleDateString()}
</p>
</li>
</>;
}

View File

@ -0,0 +1,56 @@
import type { Recipe } from "../../types/recipe";
import ServingSizeIconSmall from "../icons/ServingSizeIconSmall";
import StarIconSmall from "../icons/StarIconSmall";
import TimeIconSmall from "../icons/TimeIconSmall";
interface FavoriteResultProps {
recipe: Recipe;
};
export default function FavoriteResult({ recipe }: FavoriteResultProps) {
return <>
<div className="w-full p-2 border-b border-gray-200 hover:bg-gray-100 duration-200 flex items-center flex-col md:flex-row even:bg-[#f8f8f8] cursor-pointer">
<img className="bg-gray-50 size-56 md:size-40 rounded-md border-0" src="/v1/web/static/img/recipe_placeholder.png" />
<div className="text-gray-700 p-4 flex flex-col items-center md:items-start w-full">
<div className="flex flex-col md:flex-row items-center md:items-start justify-between w-full">
<div className="flex flex-col items-center md:items-start">
<h3 className="text-xl font-semibold text-black pb-1">
{recipe.Title} <span className="text-sm font-normal hidden md:inline">{recipe.Category}</span>
</h3>
<div className="text-sm flex gap-x-3 gap-y-1 items-center flex-wrap">
<span className="flex gap-x-1 align-center">
<TimeIconSmall />
{recipe.Duration.Total} min
</span>
<span className="flex gap-x-1 align-center">
{Array.from({ length: recipe.Difficulty }).map((_, i) => (
<StarIconSmall key={`${recipe.Id}-filled-${i}`} filled={true} />
))}
{Array.from({ length: 5 - recipe.Difficulty }).map((_, i) => (
<StarIconSmall key={`${recipe.Id}-unfilled-${i}`} filled={false} />
))}
</span>
<span className="flex gap-x-1 align-center">
<ServingSizeIconSmall />
Serves {recipe.Serves}
</span>
</div>
</div>
<div className="mb-2 mt-4 md:my-0 hidden md:block">
<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>
</div>
</div>
<p className="text-sm my-2 text-center md:text-left overflow-hidden text-ellipsis"
style={{ display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical" }}>
{recipe.Description}
</p>
</div>
</div>
</>;
}

View File

@ -0,0 +1,56 @@
import type { Recipe, Tag } from "../../types/recipe"
interface RecipeListItemProps {
recipe: Recipe;
};
function displayDifficulty(diff: number): string {
switch (diff) {
case 1:
return "Beginner"
case 2:
return "Easy"
case 3:
return "Intermediate"
case 4:
return "Challenging"
case 5:
return "Extreme"
default:
return ""
}
}
function displayTags(tags: Tag[]): string {
return tags.map(tag => tag.Name).join(", ");
}
export default function RecipeListItem({ recipe }: RecipeListItemProps) {
// TODO: Click event
return <>
<li className="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 duration-150">
<p className="text-base md:text-lg hover:text-blue-600 duration-100 cursor-pointer">
{recipe.Title}
</p>
<p className="hidden md:block text-sm text-gray-700 my-1.5">
Difficulty: <span className="font-semibold">{displayDifficulty(recipe.Difficulty)}</span>
{" "} | Duration: <span className="font-semibold">{recipe.Duration.Total} min</span>
{" "} | Category: <span className="font-semibold">{recipe.Category}</span>
</p>
<p className="md:hidden text-xs md:text-sm text-gray-700 my-1">
Difficulty: <span className="font-semibold">{displayDifficulty(recipe.Difficulty)}</span>
</p>
<p className="md:hidden text-xs md:text-sm text-gray-700 my-1">
Duration: <span className="font-semibold">{recipe.Duration.Total} min</span>
</p>
<p className="md:hidden text-xs md:text-sm text-gray-700 my-1">
Category: <span className="font-semibold">{recipe.Category}</span>
</p>
{recipe.Tags && (
<p className="text-xs italic text-gray-500">
Tags: {displayTags(recipe.Tags)}
</p>
)}
</li>
</>
}

View File

@ -1,8 +1,333 @@
import Banner from "../components/Banner";
export default function Create() {
const keyDownHandler = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault();
return false;
}
return true;
}
return (
<>
<p>Create</p>
<Banner content="Create Your Masterpiece" />
<div className="mx-4 md:mx-16 my-8">
<p className="mb-8">
Welcome to the Recipe Creation Wizard! Simply fill in the details about your culinary creation,
including the recipe's name, a description, and other specifics like its category, duration,
and difficulty. Don't forget to dynamically add all your ingredients and instructions using
the dedicated buttons, and feel free to upload an appealing image. All required fields are
marked with an <span className="text-red-500">*</span>. Once everything looks perfect, just hit the "Create Recipe"
button to
share your masterpiece!
</p>
<form>
<div className="flex flex-col">
<label htmlFor="title" className="text-sm">
Recipe Title
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide a unique title for your recipe. This is the most important part!
</p>
<input
onKeyDown={keyDownHandler}
className="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
type="text"
id="title"
name="title"
required
maxLength={128}
minLength={1}
placeholder="e.g., Classic Chicken Curry"
/>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">Please enter a title. Between 1-128 characters.</p>
</div>
<div className="flex flex-col my-4">
<label htmlFor="description" className="text-sm">
Description
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide a description for your recipe. This can be short and sweet or long and detailed!
</p>
<textarea
className="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm invalid:border-red-500"
id="description"
name="description"
rows={4}
required
maxLength={1024}
minLength={1}
placeholder="A brief description of your delicious recipe..."
></textarea>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter a description. Between 1-1000 characters.
</p>
</div>
<div className="my-4 flex flex-col gap-x-2">
<div className="flex flex-col flex-grow">
<label htmlFor="tags" className="text-sm">
Recipe Tags
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide a list of tags. <span className="italic">e.g., easy, dairy-free, gluten-free, high protein.</span>
</p>
<input
onKeyDown={keyDownHandler}
className="border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm"
maxLength={32}
enterKeyHint="done"
type="text"
id="tag"
name="tag"
placeholder="e.g., Healthy"
/>
<input type="hidden" name="tags" id="tags" value="" />
</div>
<ul id="tag-list" className="my-2 flex gap-1 flex-wrap"></ul>
</div>
<div className="my-4 flex gap-x-2">
<div className="flex flex-col flex-grow w-1/3">
<label htmlFor="preparation-time" className="text-sm">
Prep Time
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide the estimated prep time (minutes).
</p>
<input
onKeyDown={keyDownHandler}
className="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
type="number"
id="preparation-time"
name="preparation-time"
required
min="0"
max="120"
placeholder="e.g., 20"
/>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter a time (minutes).
</p>
</div>
<div className="flex flex-col flex-grow w-1/3">
<label htmlFor="cook-time" className="text-sm">
Cook Time
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide the estimated cook time (minutes).
</p>
<input
onKeyDown={keyDownHandler}
className="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
type="number"
id="cook-time"
name="cook-time"
required
min="0"
max="120"
placeholder="e.g., 45"
/>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter a time (minutes).
</p>
</div>
<div className="flex flex-col flex-grow w-1/3">
<label htmlFor="serving-size" className="text-sm">
Serving Size
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide the estimated serving size.
</p>
<input
onKeyDown={keyDownHandler}
className="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all shadow-sm invalid:border-red-500"
type="number"
max="16"
min="1"
required
id="serving-size"
name="serving-size"
placeholder="e.g., 4"
/>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter a serving size.
</p>
</div>
</div>
<div className="my-4 flex gap-x-2">
<div className="flex flex-col flex-grow w-1/3">
<label htmlFor="category" className="text-sm">
Category
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide the meal category.
</p>
<select
id="category"
name="category"
required
className="peer border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
invalid:border-red-500"
>
<option value="">Select a category</option>
<option value="breakfast">Breakfast</option>
<option value="lunch">Lunch</option>
<option value="dinner">Dinner</option>
<option value="dessert">Dessert</option>
<option value="snack">Snack</option>
<option value="side">Side</option>
<option value="other">Other</option>
</select>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">
Please select a category.
</p>
</div>
<div className="flex flex-col flex-grow w-1/3">
<label htmlFor="difficulty" className="text-sm">
Difficulty
<span className="text-red-500">*</span>
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide a baseline difficulty.
</p>
<select
id="difficulty"
name="difficulty"
required
className="peer border border-gray-300 bg-gray-200 px-4 py-2 rounded-lg focus:outline-none
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
invalid:border-red-500"
>
<option value="">Select a difficulty</option>
<option value="1">Beginner</option>
<option value="2">Easy</option>
<option value="3">Intermediate</option>
<option value="4">Challenging</option>
<option value="5">Extreme</option>
</select>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">
Please select a difficulty.
</p>
</div>
</div>
<div className="flex flex-col my-4">
<label htmlFor="ingredients" className="text-sm">
Ingredients
<span className="text-red-500">*</span>
</label>
<p className="text-xs py-1 text-gray-700">Please provide a list of ingredients and their quantities.</p>
<ul id="ingredient-list">
<li className="w-full flex gap-x-2 py-2">
<div className="flex-grow">
<input
onKeyDown={keyDownHandler}
className="peer w-full border border-gray-300 px-4 py-2 rounded-lg focus:outline-none
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
invalid:border-red-500"
type="text"
id="ingredients"
name="ingredients"
required
minLength={1}
placeholder="Ingredient name (e.g., Chicken Breast)"
/>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter at least one ingredient.
</p>
</div>
<div className="w-1/3">
<input
onKeyDown={keyDownHandler}
className="peer w-full border border-gray-300 px-4 py-2 rounded-lg focus:outline-none
focus:ring-blue-500 focus:ring-2 duration-200 ease-in-out transition-all shadow-sm
invalid:border-red-500"
type="text"
id="quantity"
name="quantity"
required
minLength={1}
placeholder="Quantity (e.g., 1lb)"
/>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">
Please provide a quantity.
</p>
</div>
</li>
</ul>
<button
type="button"
className="text-base md:text-lg text-white bg-blue-500 w-fit px-5 py-2 rounded-lg cursor-pointer"
>
Add Ingredient
</button>
</div>
<div className="flex flex-col my-4">
<label htmlFor="instructions" className="text-sm">
Instructions
<span className="text-red-500">*</span>
</label>
<p className="text-xs py-1 text-gray-700">
Please provide a list of instructions. You do not need to include step number, they will be added automatically!
</p>
<div id="instruction-list" className="flex flex-col">
<textarea
className="peer border border-gray-300 px-4 py-2 rounded-lg focus:outline-none focus:ring-blue-500
focus:ring-2 duration-200 ease-in-out transition-all resize-none shadow-sm invalid:border-red-500
valid:my-2 invalid:mt-2"
id="instructions"
name="instructions"
rows={3}
required
minLength={1}
placeholder="Step 1: Describe this step..."
></textarea>
<p className="hidden peer-invalid:block text-xs text-red-500 my-1">
Please enter at least one step.
</p>
</div>
<button
type="button"
className="text-base md:text-lg text-white bg-blue-500 w-fit px-5 py-2 rounded-lg cursor-pointer"
>
Add Instruction Step
</button>
</div>
<div className="flex flex-col my-4">
<label htmlFor="image" className="text-sm">
Recipe Image
</label>
<p className="text-xs pt-1 pb-2 text-gray-700">
Please provide an image of your creation. This is optional but is a nice touch!
</p>
<input
type="file"
accept="image/*"
name="image"
id="image"
className="my-2 block w-full text-sm text-placeholder file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:bg-blue-100 file:text-blue-700 cursor-pointer"
/>
</div>
<p id="response" className="hidden"></p>
<button
type="submit"
className="w-full mt-8 bg-gradient-to-r from-blue-200 to-purple-200 py-2 rounded-lg text-lg cursor-pointer shadow-md"
>
Create Recipe
</button>
</form>
</div>
</>
);
}

View File

@ -1,7 +1,111 @@
import { useEffect, useState } from "react";
import Banner from "../components/Banner";
import RecipeSearchBar from "../components/inputs/RecipeSearchBar";
import type { Recipe } from "../types/recipe";
import FavoriteResult from "../components/results/FavoriteResult";
export default function Favorites() {
const [recipes, setRecipes] = useState<Recipe[]>([]);
// BUG: Remove this
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
};
setRecipes([recipe, recipe2]);
}, []);
return (
<>
<p>Favorites</p>
<Banner content="Favorites" />
<RecipeSearchBar filters={null} redirect={false} searchOnLoad={true} favorites={true} />
<hr className="text-gray-300 w-full" />
<div id="result-list" className="flex flex-col w-full p-4 items-center">
{recipes.length < 1 ? (
<p className="text-gray-700 text-sm py-4">No results</p>
) : (
<>
{recipes.map(recipe => <FavoriteResult key={recipe.Id} recipe={recipe} />)}
<p className="text-gray-700 text-sm py-4">End of results</p>
</>
)}
</div>
</>
);
}

View File

@ -1,7 +1,204 @@
import { useEffect, useState } from "react";
import type { User } from "../types/user";
import type { Recipe } from "../types/recipe";
import RecipeListItem from "../components/results/RecipeListItem";
import type { Engagement } from "../types/engagement";
import ActivityListItem from "../components/results/ActivityListItem";
export default function Profile() {
const [user, setUser] = useState<User | null>(null);
const [recipes, setRecipes] = useState<Recipe[]>([]);
const [favorites, setFavorites] = useState<Recipe[]>([]);
const [activity, setActivity] = useState<Engagement[]>([]);
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
};
const eng: Engagement = {
Id: 1,
Type: "made",
Message: "Created some shit",
Entity: 1,
UserId: 1,
Created: new Date(),
};
const user: User = {
Id: 1,
GoogleId: "a",
Name: "Hayden Hargreaves",
Email: "hhargreaves2006@gmail.com",
ImageUrl: "https://lh3.googleusercontent.com/a/ACg8ocLeT6ltjQIkiBy1MgMJDbQxtBfMVfn8sP4e1t7d0bCJeHFdpcea=s96-c",
GoogleRefreshToken: "a",
Created: new Date(),
};
setUser(user);
setRecipes([recipe, recipe2, recipe, recipe, recipe, recipe, recipe]);
setFavorites([recipe, recipe2]);
setActivity([eng]);
}, []);
return (
<>
<p>Profile</p>
{/* User Details Section */}
<section className="w-full flex flex-col justify-center my-8 py-4 border-b border-gray-300">
<div className="w-full p-4 md:p-8 flex items-center gap-x-8">
{user?.ImageUrl != "" ? (
<img
className="w-24 md:w-32 border-2 border-blue-500 rounded-full shadow-blue-500 shadow select-none"
src={user?.ImageUrl ?? ""}
/>
) : (
<img
className="w-24 md:w-32 border-2 border-blue-500 rounded-full shadow-blue-500 shadow select-none"
src={`https://ui-avatars.com/api/?name=${user?.Name.split(" ")[0]}+${user?.Name.split(" ")[1]}&size=150`}
/>
)}
<div className="flex flex-col gap-y-4">
<div className="">
<h1 className="text-md md:text-2xl font-semibold">{user?.Name}</h1>
<p className="text-xs md:text-sm">{user?.Email}</p>
</div>
<div className="flex gap-x-4">
<p className="text-xs md:text-sm"><span className="font-bold">{recipes.length}</span> recipes</p>
<p className="text-xs md:text-sm"><span className="font-bold">{favorites.length}</span> favorites</p>
</div>
</div>
</div>
</section>
{/* Recipe Section */}
<section className="p-8">
<h2 className="text-2xl font-semibold text-gray-800">My Recipes</h2>
<ul className="w-full my-2">
{recipes.length <= 4 ? (
recipes.map(recipe => <RecipeListItem key={recipe.Id} recipe={recipe} />)
) : (
recipes.slice(0, 4).map(recipe => <RecipeListItem key={recipe.Id} recipe={recipe} />)
)}
<a href="">
<li className="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 hover:text-blue-600 duration-150 text-center">
See all...
</li>
</a>
</ul>
</section>
{/* Favorites Section */}
<section className="p-8">
<h2 className="text-2xl font-semibold text-gray-800">My Favorites</h2>
<ul className="w-full my-2">
{favorites.length <= 4 ? (
favorites.map(recipe => <RecipeListItem key={recipe.Id} recipe={recipe} />)
) : (
favorites.slice(0, 4).map(recipe => <RecipeListItem key={recipe.Id} recipe={recipe} />)
)}
<a href="">
<li className="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 hover:text-blue-600 duration-150 text-center">
See all...
</li>
</a>
</ul>
</section>
{/* Activity Section */}
<section className="p-8">
<h2 className="text-2xl font-semibold text-gray-800">Recent Activity</h2>
<ul className="w-full my-2">
{activity?.map(act => <ActivityListItem key={act.Id} engagement={act} />)}
<a href="">
<li className="w-full border-b border-gray-300 px-2 py-4 even:bg-gray-50 hover:bg-gray-100 hover:text-blue-600 duration-150 text-center">
See all...
</li>
</a>
</ul>
</section>
{/* Logout Section TODO: Click event*/}
<section className="w-full flex flex-col justify-center items-center py-8 border-t border-gray-300 mt-auto">
<a href="" className="text-center border border-red-500 text-red-500 w-9/10 md:w-1/3 py-2 rounded-lg hover:cursor-pointer hover:bg-red-100 duration-300">
Logout
</a>
</section>
</>
);
}

View File

@ -0,0 +1,11 @@
export type EngagementType = "made" | "liked" | "viewed" | "shared" | "reviewed" | "rated";
export interface Engagement {
Id: number;
Type: EngagementType;
Message: string;
Entity: number;
UserId: number;
Created: Date;
};

19
web/src/types/user.ts Normal file
View File

@ -0,0 +1,19 @@
export interface GoogleUserInfo {
Id: string;
Email: string;
Verified: boolean;
Name: string;
GivenName: string;
FamilyName: string;
Picture: string;
}
export interface User {
Id: number;
GoogleId: string;
Name: string;
Email: string;
ImageUrl: string;
GoogleRefreshToken: string;
Created: Date;
}