Compare commits
3 Commits
2916eeef61
...
5db803d033
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5db803d033 | ||
|
|
c9be9876e3 | ||
|
|
45a0d0e54c |
16
web/src/components/icons/ServingSizeIconSmall.tsx
Normal file
16
web/src/components/icons/ServingSizeIconSmall.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
25
web/src/components/icons/StarIconSmall.tsx
Normal file
25
web/src/components/icons/StarIconSmall.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
9
web/src/components/icons/TimeIconSmall.tsx
Normal file
9
web/src/components/icons/TimeIconSmall.tsx
Normal 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>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
19
web/src/components/results/ActivityListItem.tsx
Normal file
19
web/src/components/results/ActivityListItem.tsx
Normal 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>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
56
web/src/components/results/FavoriteResult.tsx
Normal file
56
web/src/components/results/FavoriteResult.tsx
Normal 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>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
56
web/src/components/results/RecipeListItem.tsx
Normal file
56
web/src/components/results/RecipeListItem.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@ -1,8 +1,333 @@
|
|||||||
|
import Banner from "../components/Banner";
|
||||||
|
|
||||||
export default function Create() {
|
export default function Create() {
|
||||||
|
const keyDownHandler = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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() {
|
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 (
|
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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() {
|
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 (
|
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
11
web/src/types/engagement.ts
Normal file
11
web/src/types/engagement.ts
Normal 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
19
web/src/types/user.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user