docs and background for details page
This commit is contained in:
parent
3f075d509b
commit
ef8388be6d
5 changed files with 123 additions and 106 deletions
|
|
@ -10,6 +10,10 @@
|
||||||
@apply flex items-center w-screen justify-center min-h-screen bg-gray-50;
|
@apply flex items-center w-screen justify-center min-h-screen bg-gray-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
@apply bg-gray-100 w-full min-h-screen max-w-6xl shadow-xl p-8
|
||||||
|
}
|
||||||
|
|
||||||
/* headings */
|
/* headings */
|
||||||
.content-title{
|
.content-title{
|
||||||
@apply text-3xl font-black mb-8 text-blue-900;
|
@apply text-3xl font-black mb-8 text-blue-900;
|
||||||
|
|
@ -61,7 +65,6 @@
|
||||||
.dark-button-text{
|
.dark-button-text{
|
||||||
@apply text-white;
|
@apply text-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transparent-button-bg {
|
.transparent-button-bg {
|
||||||
@apply bg-transparent hover:bg-transparent;
|
@apply bg-transparent hover:bg-transparent;
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +74,7 @@
|
||||||
|
|
||||||
/* input fields like input and textarea */
|
/* input fields like input and textarea */
|
||||||
.input-field {
|
.input-field {
|
||||||
@apply p-2 w-full border rounded-md placeholder-gray-400 border-gray-600 hover:border-blue-800 transition-colors text-gray-600 focus:outline-none focus:border-blue-900;
|
@apply p-2 w-full border rounded-md bg-white placeholder-gray-400 border-gray-600 hover:border-blue-800 transition-colors text-gray-600 focus:outline-none focus:border-blue-900;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* groups */
|
/* groups */
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic definitions used by all Button types, such as Button.tsx and ButtonLink.tsx
|
||||||
|
*/
|
||||||
export type BasicButtonProps = {
|
export type BasicButtonProps = {
|
||||||
/** Optional Lucide icon (e.g. Plus, X, Check) */
|
/** Optional Lucide icon (e.g. Plus, X, Check) */
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
|
|
@ -15,20 +18,20 @@ export type BasicButtonProps = {
|
||||||
*/
|
*/
|
||||||
export const ButtonType = {
|
export const ButtonType = {
|
||||||
DarkButton: {
|
DarkButton: {
|
||||||
textColor: "text-dark-button-text",
|
textColor: "dark-button-text",
|
||||||
backgroundColor: "bg-dark-button-bg",
|
backgroundColor: "dark-button-bg",
|
||||||
},
|
},
|
||||||
PrimaryButton: {
|
PrimaryButton: {
|
||||||
textColor: "text-primary-button-text",
|
textColor: "primary-button-text",
|
||||||
backgroundColor: "bg-primary-button-bg",
|
backgroundColor: "primary-button-bg",
|
||||||
},
|
},
|
||||||
DefaultButton: {
|
DefaultButton: {
|
||||||
textColor: "text-default-button-text",
|
textColor: "default-button-text",
|
||||||
backgroundColor: "bg-default-button-bg",
|
backgroundColor: "default-button-bg",
|
||||||
},
|
},
|
||||||
TransparentButton: {
|
TransparentButton: {
|
||||||
textColor: "text-transparent-button-text",
|
textColor: "transparent-button-text",
|
||||||
backgroundColor: "bg-transparent-button-bg",
|
backgroundColor: "transparent-button-bg",
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ type ButtonProps = BasicButtonProps & {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button component. Styles are defined in BasicButtonDefinitions.ts
|
||||||
|
*/
|
||||||
export default function Button({
|
export default function Button({
|
||||||
onClick,
|
onClick,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
|
|
@ -15,7 +18,7 @@ export default function Button({
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`basic-button ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`}
|
className={`basic-button bg-primary-button-bg ${buttonType.backgroundColor} ${buttonType.textColor} ${className}`}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -17,32 +17,32 @@ export default function RecipeDetailPage() {
|
||||||
// the recipe loaded from the backend, don't change this! it's required for scaling
|
// the recipe loaded from the backend, don't change this! it's required for scaling
|
||||||
const [recipe, setRecipe] = useState<RecipeModel | null>(null)
|
const [recipe, setRecipe] = useState<RecipeModel | null>(null)
|
||||||
// Working copy for re-calculating ingredients
|
// Working copy for re-calculating ingredients
|
||||||
const [recipeWorkingCopy, setRecipeWorkingCopy] = useState<RecipeModel|null>(null)
|
const [recipeWorkingCopy, setRecipeWorkingCopy] = useState<RecipeModel | null>(null)
|
||||||
// load recipe data whenever id changes
|
// load recipe data whenever id changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadRecipe = async () => {
|
const loadRecipe = async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
try {
|
try {
|
||||||
// Fetch recipe data when editing an existing one
|
// Fetch recipe data when editing an existing one
|
||||||
console.log("loading recipe with id", id)
|
console.log("loading recipe with id", id)
|
||||||
const data = await fetchRecipe(id)
|
const data = await fetchRecipe(id)
|
||||||
if(data.id != id){
|
if (data.id != id) {
|
||||||
throw new Error("Id mismatch when loading recipes: " + id + " requested and " + data.id + " received!");
|
throw new Error("Id mismatch when loading recipes: " + id + " requested and " + data.id + " received!");
|
||||||
}
|
|
||||||
setRecipe(mapRecipeDtoToModel(data))
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
}
|
||||||
|
setRecipe(mapRecipeDtoToModel(data))
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadRecipe()
|
loadRecipe()
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
// set original recipe data and working copy when recipe changes
|
// set original recipe data and working copy when recipe changes
|
||||||
useEffect( ()=> {
|
useEffect(() => {
|
||||||
setRecipeWorkingCopy(recipe);
|
setRecipeWorkingCopy(recipe);
|
||||||
}, [recipe])
|
}, [recipe])
|
||||||
|
|
||||||
|
|
||||||
if (!recipe || !recipeWorkingCopy) {
|
if (!recipe || !recipeWorkingCopy) {
|
||||||
|
|
@ -50,7 +50,7 @@ export default function RecipeDetailPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** recalculate ingredients based on the amount of servings */
|
/** recalculate ingredients based on the amount of servings */
|
||||||
const recalculateIngredients = (newAmount: number) => {
|
const recalculateIngredients = (newAmount: number) => {
|
||||||
// Always calculate factor from the *original recipe*, not the working copy
|
// Always calculate factor from the *original recipe*, not the working copy
|
||||||
const factor = newAmount / recipe.servings.amount
|
const factor = newAmount / recipe.servings.amount
|
||||||
|
|
@ -76,81 +76,89 @@ export default function RecipeDetailPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-2xl mx-auto">
|
/*Container spanning entire screen used to center content horizontally */
|
||||||
<h1 className="content-title">{recipeWorkingCopy.title}</h1>
|
<div className="app-bg">
|
||||||
|
{/* Container defining the maximum width of the content */}
|
||||||
{/* Recipe image */}
|
<div className="content-container">
|
||||||
{recipe.imageUrl && (
|
{/* Header - remains in position when scrolling */}
|
||||||
<img
|
<div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300">
|
||||||
src={recipe.imageUrl}
|
<h1 className="content-title">{recipeWorkingCopy.title}</h1>
|
||||||
alt={recipe.title}
|
|
||||||
className="w-full rounded-xl mb-4 border"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Servings */}
|
|
||||||
<div className="flex flex-row items-center gap-2 bg-blue-100 columns-2 rounded p-2 mb-4">
|
|
||||||
<p className="mb-2">For {recipeWorkingCopy.servings.amount} {recipeWorkingCopy.servings.unit}</p>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
className="input-field w-20 ml-auto"
|
|
||||||
value={recipeWorkingCopy.servings.amount}
|
|
||||||
onChange={
|
|
||||||
e => {
|
|
||||||
recalculateIngredients(Number(e.target.value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/* Ingredients */}
|
|
||||||
<h2 className="section-heading">Zutaten</h2>
|
<div className="p-6 max-w-2xl mx-auto">
|
||||||
<ul>
|
{/* Recipe image */}
|
||||||
{recipeWorkingCopy.ingredientGroupList.map((group,i) => (
|
{recipe.imageUrl && (
|
||||||
<div key={i}>
|
<img
|
||||||
{/* the title is optional, only print if present */}
|
src={recipe.imageUrl}
|
||||||
{group.title && group.title.trim() !== "" && (
|
alt={recipe.title}
|
||||||
<h3 className="subsection-heading">{group.title}</h3>
|
className="w-full rounded-xl mb-4 border"
|
||||||
)}
|
/>
|
||||||
<ul className="default-list">
|
)}
|
||||||
{group.ingredientList.map((ing, j) => (
|
|
||||||
<li key={j}>
|
{/* Servings */}
|
||||||
{ing.amount} {ing.unit ?? ""} {ing.name}
|
<div className="flex flex-row items-center gap-2 bg-blue-100 columns-2 rounded p-2 mb-4">
|
||||||
</li>
|
<p className="mb-2">For {recipeWorkingCopy.servings.amount} {recipeWorkingCopy.servings.unit}</p>
|
||||||
))}
|
<input
|
||||||
</ul>
|
type="number"
|
||||||
|
className="input-field w-20 ml-auto"
|
||||||
|
value={recipeWorkingCopy.servings.amount}
|
||||||
|
onChange={
|
||||||
|
e => {
|
||||||
|
recalculateIngredients(Number(e.target.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
{/* Ingredients */}
|
||||||
</ul>
|
<h2 className="section-heading">Zutaten</h2>
|
||||||
|
<ul>
|
||||||
{/* Instructions - @todo add reasonable list delegate component*/}
|
{recipeWorkingCopy.ingredientGroupList.map((group, i) => (
|
||||||
<ol className="space-y-4">
|
<div key={i}>
|
||||||
{recipe.instructionStepList.map((step, j) => (
|
{/* the title is optional, only print if present */}
|
||||||
<li key={j} className="flex items-start gap-4">
|
{group.title && group.title.trim() !== "" && (
|
||||||
{/* Step number circle */}
|
<h3 className="subsection-heading">{group.title}</h3>
|
||||||
<div className="enumeration-indicator">
|
)}
|
||||||
{j + 1}
|
<ul className="default-list">
|
||||||
|
{group.ingredientList.map((ing, j) => (
|
||||||
|
<li key={j}>
|
||||||
|
{ing.amount} {ing.unit ?? ""} {ing.name}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
{/* Step text */}
|
{/* Instructions - @todo add reasonable list delegate component*/}
|
||||||
<p className="leading-relaxed">{step.text}</p>
|
<ol className="space-y-4">
|
||||||
</li>
|
{recipe.instructionStepList.map((step, j) => (
|
||||||
))}
|
<li key={j} className="flex items-start gap-4">
|
||||||
</ol>
|
{/* Step number circle */}
|
||||||
|
<div className="enumeration-indicator">
|
||||||
|
{j + 1}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Step text */}
|
||||||
<div className="button-group">
|
<p className="leading-relaxed">{step.text}</p>
|
||||||
<ButtonLink
|
</li>
|
||||||
to={recipe.id !== undefined ? getRecipeEditUrl(recipe.id) : getRecipeListUrl()} // @todo show error instead
|
))}
|
||||||
className="basic-button primary-button-bg primary-button-text"
|
</ol>
|
||||||
text="Bearbeiten"
|
|
||||||
/>
|
{/* Action buttons */}
|
||||||
<ButtonLink
|
<div className="button-group">
|
||||||
to={getRecipeListUrl()}
|
<ButtonLink
|
||||||
className="basic-button default-button-bg default-button-text"
|
to={recipe.id !== undefined ? getRecipeEditUrl(recipe.id) : getRecipeListUrl()} // @todo show error instead
|
||||||
text="Zurueck"
|
className="basic-button primary-button-bg primary-button-text"
|
||||||
/>
|
text="Bearbeiten"
|
||||||
|
/>
|
||||||
|
<ButtonLink
|
||||||
|
to={getRecipeListUrl()}
|
||||||
|
className="basic-button default-button-bg default-button-text"
|
||||||
|
text="Zurueck"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export default function RecipeListPage() {
|
||||||
/*Container spanning entire screen used to center content horizontally */
|
/*Container spanning entire screen used to center content horizontally */
|
||||||
<div className="app-bg">
|
<div className="app-bg">
|
||||||
{/* Container defining the maximum width of the content */}
|
{/* Container defining the maximum width of the content */}
|
||||||
<div className="bg-gray-100 w-full min-h-screen max-w-6xl shadow-xl p-8">
|
<div className="content-container">
|
||||||
{/* Header - remains in position when scrolling */}
|
{/* Header - remains in position when scrolling */}
|
||||||
<div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300">
|
<div className="sticky bg-gray-100 top-0 left-0 right-0 pb-4 border-b-2 border-gray-300">
|
||||||
<h1 className="content-title">Recipes</h1>
|
<h1 className="content-title">Recipes</h1>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue