refactor: rename DemoRecipeApp, redesign UI with recipe categories, bookmarks, and detailed recipe views
This commit is contained in:
BIN
03_source/mobile/public/assets/DemoRecipeApp/bookmark.png
Normal file
BIN
03_source/mobile/public/assets/DemoRecipeApp/bookmark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
03_source/mobile/public/assets/DemoRecipeApp/icon/favicon.png
Normal file
BIN
03_source/mobile/public/assets/DemoRecipeApp/icon/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 930 B |
BIN
03_source/mobile/public/assets/DemoRecipeApp/icon/icon.png
Normal file
BIN
03_source/mobile/public/assets/DemoRecipeApp/icon/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
03_source/mobile/public/assets/DemoRecipeApp/placeholder.png
Normal file
BIN
03_source/mobile/public/assets/DemoRecipeApp/placeholder.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
1
03_source/mobile/public/assets/DemoRecipeApp/shapes.svg
Normal file
1
03_source/mobile/public/assets/DemoRecipeApp/shapes.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="350" height="140" xmlns="http://www.w3.org/2000/svg" style="background:#f6f7f9"><g fill="none" fill-rule="evenodd"><path fill="#F04141" style="mix-blend-mode:multiply" d="M61.905-34.23l96.194 54.51-66.982 54.512L22 34.887z"/><circle fill="#10DC60" style="mix-blend-mode:multiply" cx="155.5" cy="135.5" r="57.5"/><path fill="#3880FF" style="mix-blend-mode:multiply" d="M208.538 9.513l84.417 15.392L223.93 93.93z"/><path fill="#FFCE00" style="mix-blend-mode:multiply" d="M268.625 106.557l46.332-26.75 46.332 26.75v53.5l-46.332 26.75-46.332-26.75z"/><circle fill="#7044FF" style="mix-blend-mode:multiply" cx="299.5" cy="9.5" r="38.5"/><rect fill="#11D3EA" style="mix-blend-mode:multiply" transform="rotate(-60 148.47 37.886)" x="143.372" y="-7.056" width="10.196" height="89.884" rx="5.098"/><path d="M-25.389 74.253l84.86 8.107c5.498.525 9.53 5.407 9.004 10.905a10 10 0 0 1-.057.477l-12.36 85.671a10.002 10.002 0 0 1-11.634 8.42l-86.351-15.226c-5.44-.959-9.07-6.145-8.112-11.584l13.851-78.551a10 10 0 0 1 10.799-8.219z" fill="#7044FF" style="mix-blend-mode:multiply"/><circle fill="#0CD1E8" style="mix-blend-mode:multiply" cx="273.5" cy="106.5" r="20.5"/></g></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,74 @@
|
|||||||
|
import {
|
||||||
|
IonBackButton,
|
||||||
|
IonButtons,
|
||||||
|
IonCol,
|
||||||
|
IonContent,
|
||||||
|
IonHeader,
|
||||||
|
IonImg,
|
||||||
|
IonList,
|
||||||
|
IonNote,
|
||||||
|
IonPage,
|
||||||
|
IonRow,
|
||||||
|
IonText,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import { RecipeListItem } from '../components/RecipeListItem';
|
||||||
|
|
||||||
|
import { useStoreState } from 'pullstate';
|
||||||
|
import { BookmarkStore } from '../store';
|
||||||
|
import { getBookmarks } from '../store/Selectors';
|
||||||
|
|
||||||
|
const Bookmarks = () => {
|
||||||
|
const bookmarks = useStoreState(BookmarkStore, getBookmarks);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonBackButton text="Categories" />
|
||||||
|
</IonButtons>
|
||||||
|
<IonTitle>Bookmarks</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent fullscreen>
|
||||||
|
<IonHeader collapse="condense">
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle size="large">Bookmarks ({bookmarks.length})</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
|
||||||
|
<IonList>
|
||||||
|
{bookmarks.map((bookmark, index) => {
|
||||||
|
return (
|
||||||
|
<RecipeListItem recipe={bookmark} key={`recipe_${index}`} fromBookmarks={true} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</IonList>
|
||||||
|
|
||||||
|
{bookmarks.length < 1 && (
|
||||||
|
<>
|
||||||
|
<IonRow className="ion-justify-content-center ion-text-center ion-margin-top ion-padding-top">
|
||||||
|
<IonCol size="8">
|
||||||
|
<IonText>You don't have any bookmarks yet</IonText>
|
||||||
|
|
||||||
|
<IonNote>
|
||||||
|
<p>When viewing a recipe, press the bookmark icon to add it</p>
|
||||||
|
</IonNote>
|
||||||
|
</IonCol>
|
||||||
|
</IonRow>
|
||||||
|
|
||||||
|
<IonRow className="ion-justify-content-center">
|
||||||
|
<IonCol size="8">
|
||||||
|
<IonImg src="/assets/DemoRecipeApp/bookmark.png" />
|
||||||
|
</IonCol>
|
||||||
|
</IonRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Bookmarks;
|
126
03_source/mobile/src/pages/DemoRecipeApp/AppPages/Categories.jsx
Normal file
126
03_source/mobile/src/pages/DemoRecipeApp/AppPages/Categories.jsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonCardTitle,
|
||||||
|
IonCol,
|
||||||
|
IonContent,
|
||||||
|
IonGrid,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonPage,
|
||||||
|
IonRow,
|
||||||
|
IonSearchbar,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
useIonRouter,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import styles from './Categories.module.scss';
|
||||||
|
import { recipes } from '../recipes';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { bookmarkOutline, chevronBackOutline } from 'ionicons/icons';
|
||||||
|
import { getBookmarks } from '../store/Selectors';
|
||||||
|
import { useStoreState } from 'pullstate';
|
||||||
|
import { BookmarkStore } from '../store';
|
||||||
|
|
||||||
|
const Categories = () => {
|
||||||
|
const router = useIonRouter();
|
||||||
|
const pageRef = useRef();
|
||||||
|
const [recipeCategories, setRecipeCategories] = useState([]);
|
||||||
|
const bookmarks = useStoreState(BookmarkStore, getBookmarks);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getAllRecipes = async () => {
|
||||||
|
const tempRecipeCategories = [
|
||||||
|
{
|
||||||
|
name: 'Chicken',
|
||||||
|
data: recipes.chicken.hits[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Beef',
|
||||||
|
data: recipes.beef.hits[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Fish',
|
||||||
|
data: recipes.fish.hits[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Fruit',
|
||||||
|
data: recipes.fruit.hits[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Salad',
|
||||||
|
data: recipes.salad.hits[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Vegan',
|
||||||
|
data: recipes.vegan.hits[0],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
setRecipeCategories(tempRecipeCategories);
|
||||||
|
};
|
||||||
|
|
||||||
|
getAllRecipes();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function handleBackClick() {
|
||||||
|
router.goBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonPage ref={pageRef}>
|
||||||
|
<IonHeader className="ion-no-border">
|
||||||
|
<IonToolbar className="ion-no-border">
|
||||||
|
<IonTitle>Recipe Categories</IonTitle>
|
||||||
|
|
||||||
|
<IonButtons slot="end">
|
||||||
|
<IonButton routerLink="/bookmarks">
|
||||||
|
<IonIcon icon={bookmarkOutline} />
|
||||||
|
|
||||||
|
{bookmarks.length}
|
||||||
|
</IonButton>
|
||||||
|
</IonButtons>
|
||||||
|
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonButton onClick={() => handleBackClick()}>
|
||||||
|
<IonIcon icon={chevronBackOutline} color="primary" />
|
||||||
|
</IonButton>
|
||||||
|
</IonButtons>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent fullscreen>
|
||||||
|
<div className={styles.searchArea}>
|
||||||
|
<IonSearchbar
|
||||||
|
className="ion-justify-content-center"
|
||||||
|
placeholder="Try 'Chicken Piccata'"
|
||||||
|
onClick={() => router.push('/search')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IonGrid>
|
||||||
|
<IonRow className={styles.row}>
|
||||||
|
{recipeCategories.map((category, index) => {
|
||||||
|
const { name, data } = category;
|
||||||
|
const { recipe } = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonCol className={styles.col} size="6" key={`recipe_${index}`}>
|
||||||
|
<Link to={`/category/${name}`}>
|
||||||
|
<img src={recipe.image} alt="cover" />
|
||||||
|
<div className={styles.categoryName}>
|
||||||
|
<IonCardTitle>{name}</IonCardTitle>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</IonCol>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</IonRow>
|
||||||
|
</IonGrid>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Categories;
|
@@ -0,0 +1,65 @@
|
|||||||
|
.categoryName {
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
ion-card-title {
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
width: fit-content;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row,
|
||||||
|
.col,
|
||||||
|
.card {
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
|
|
||||||
|
border: 5px solid white;
|
||||||
|
|
||||||
|
img {
|
||||||
|
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchArea {
|
||||||
|
|
||||||
|
background-color: var(--ion-toolbar-background);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
|
||||||
|
ion-searchbar {
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
--background: rgb(49, 49, 49);
|
||||||
|
--icon-color: rgb(27, 173, 100);
|
||||||
|
// --border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton {
|
||||||
|
|
||||||
|
height: 2.2rem;
|
||||||
|
margin-top: 0.9rem;
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
IonBackButton,
|
||||||
|
IonButtons,
|
||||||
|
IonContent,
|
||||||
|
IonHeader,
|
||||||
|
IonList,
|
||||||
|
IonPage,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import { RecipeListItem } from '../components/RecipeListItem';
|
||||||
|
import { recipes } from '../recipes';
|
||||||
|
|
||||||
|
const Category = () => {
|
||||||
|
const { name } = useParams();
|
||||||
|
const [categoryRecipes, setCategoryRecipes] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCategoryRecipes(recipes[name.toLowerCase()].hits);
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonBackButton text="Categories" />
|
||||||
|
</IonButtons>
|
||||||
|
<IonTitle>{name} Recipes</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent fullscreen>
|
||||||
|
<IonHeader collapse="condense">
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle size="large">{name} Recipes</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
|
||||||
|
<IonList>
|
||||||
|
{categoryRecipes.map((categoryRecipe, index) => {
|
||||||
|
const { recipe } = categoryRecipe;
|
||||||
|
|
||||||
|
return <RecipeListItem recipe={recipe} key={`recipe_${index}`} />;
|
||||||
|
})}
|
||||||
|
</IonList>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Category;
|
193
03_source/mobile/src/pages/DemoRecipeApp/AppPages/Recipe.jsx
Normal file
193
03_source/mobile/src/pages/DemoRecipeApp/AppPages/Recipe.jsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import {
|
||||||
|
IonBackButton,
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonCardSubtitle,
|
||||||
|
IonCardTitle,
|
||||||
|
IonCol,
|
||||||
|
IonContent,
|
||||||
|
IonGrid,
|
||||||
|
IonHeader,
|
||||||
|
IonIcon,
|
||||||
|
IonList,
|
||||||
|
IonListHeader,
|
||||||
|
IonPage,
|
||||||
|
IonRow,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
useIonModal,
|
||||||
|
useIonToast,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import {
|
||||||
|
bookmark,
|
||||||
|
bookmarkOutline,
|
||||||
|
informationCircleOutline,
|
||||||
|
layersOutline,
|
||||||
|
peopleOutline,
|
||||||
|
timeOutline,
|
||||||
|
} from 'ionicons/icons';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
|
import { Ingredient } from '../components/Ingredient';
|
||||||
|
import IngredientsModal from '../components/IngredientsModal';
|
||||||
|
import NutritionModal from '../components/NutritionModal';
|
||||||
|
import BookmarkStore, { addToBookmarks } from '../store/BookmarkStore';
|
||||||
|
import styles from './Recipe.module.scss';
|
||||||
|
|
||||||
|
import { useStoreState } from 'pullstate';
|
||||||
|
import { getBookmarks } from '../store/Selectors';
|
||||||
|
|
||||||
|
const Recipe = () => {
|
||||||
|
const pageRef = useRef();
|
||||||
|
const { state } = useLocation();
|
||||||
|
const [recipe, setRecipe] = useState([]);
|
||||||
|
const [fromSearch, setFromSearch] = useState(false);
|
||||||
|
const [fromBookmarks, setFromBookmarks] = useState(false);
|
||||||
|
|
||||||
|
const bookmarks = useStoreState(BookmarkStore, getBookmarks);
|
||||||
|
|
||||||
|
const [showToast] = useIonToast();
|
||||||
|
|
||||||
|
const handleDismissIngredientsModal = () => {
|
||||||
|
hideIngredientsModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDismissNutritionModal = () => {
|
||||||
|
hideNutritionModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [showIngredientsModal, hideIngredientsModal] = useIonModal(IngredientsModal, {
|
||||||
|
dismiss: handleDismissIngredientsModal,
|
||||||
|
ingredients: recipe.ingredients,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showNutritionModal, hideNutritionModal] = useIonModal(NutritionModal, {
|
||||||
|
dismiss: handleDismissNutritionModal,
|
||||||
|
recipe,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state && state.recipe) {
|
||||||
|
setRecipe(state.recipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state && state.fromSearch) {
|
||||||
|
setFromSearch(state.fromSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state && state.fromBookmarks) {
|
||||||
|
setFromBookmarks(state.fromBookmarks);
|
||||||
|
}
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
const addBookmark = async () => {
|
||||||
|
const added = addToBookmarks(recipe);
|
||||||
|
showToast({
|
||||||
|
message: added
|
||||||
|
? 'This recipe has been bookmarked!'
|
||||||
|
: 'This recipe has been removed from your bookmarks.',
|
||||||
|
duration: 2000,
|
||||||
|
color: 'main',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonPage ref={pageRef}>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>View Recipe</IonTitle>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonBackButton
|
||||||
|
text={fromSearch ? 'Search' : fromBookmarks ? 'Bookmarks' : 'Recipes'}
|
||||||
|
color="main"
|
||||||
|
/>
|
||||||
|
</IonButtons>
|
||||||
|
|
||||||
|
<IonButtons slot="end">
|
||||||
|
<IonButton onClick={addBookmark}>
|
||||||
|
<IonIcon icon={bookmarks.includes(recipe) ? bookmark : bookmarkOutline} />
|
||||||
|
</IonButton>
|
||||||
|
</IonButtons>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent fullscreen>
|
||||||
|
<div className={styles.headerImage}>
|
||||||
|
<img src={recipe.image} alt="main cover" />
|
||||||
|
<div className={`${styles.headerInfo} animate__animated animate__slideInLeft`}>
|
||||||
|
<h1>{recipe.label}</h1>
|
||||||
|
<p>{recipe.dishType && recipe.dishType[0]}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IonGrid>
|
||||||
|
<IonRow className="ion-text-center">
|
||||||
|
<IonCol size="4">
|
||||||
|
<IonCardTitle>
|
||||||
|
<IonIcon icon={peopleOutline} />
|
||||||
|
</IonCardTitle>
|
||||||
|
<IonCardSubtitle>serves {recipe.yield && recipe.yield}</IonCardSubtitle>
|
||||||
|
</IonCol>
|
||||||
|
<IonCol size="4">
|
||||||
|
<IonCardTitle>
|
||||||
|
<IonIcon icon={timeOutline} />
|
||||||
|
</IonCardTitle>
|
||||||
|
<IonCardSubtitle>
|
||||||
|
{recipe.totalTime !== 0
|
||||||
|
? `${recipe.totalWeight && recipe.totalWeight.toFixed(0)} mins`
|
||||||
|
: 'N/A'}
|
||||||
|
</IonCardSubtitle>
|
||||||
|
</IonCol>
|
||||||
|
<IonCol size="4">
|
||||||
|
<IonCardTitle>
|
||||||
|
<IonIcon icon={layersOutline} />
|
||||||
|
</IonCardTitle>
|
||||||
|
<IonCardSubtitle>
|
||||||
|
{recipe.totalWeight && recipe.totalWeight.toFixed(0)}g
|
||||||
|
</IonCardSubtitle>
|
||||||
|
</IonCol>
|
||||||
|
</IonRow>
|
||||||
|
|
||||||
|
<IonRow className="ion-text-center">
|
||||||
|
<IonCol size="6">
|
||||||
|
{/* <IonButton color="main" onClick={ () => showIngredientsModal({
|
||||||
|
|
||||||
|
presentingElement: pageRef.current,
|
||||||
|
cssClass: "customModal"
|
||||||
|
})}>
|
||||||
|
<IonIcon icon={ informationCircleOutline } />
|
||||||
|
View Ingredients
|
||||||
|
</IonButton> */}
|
||||||
|
</IonCol>
|
||||||
|
<IonCol size="12">
|
||||||
|
<IonButton
|
||||||
|
expand="block"
|
||||||
|
color="main"
|
||||||
|
onClick={() =>
|
||||||
|
showNutritionModal({
|
||||||
|
presentingElement: pageRef.current,
|
||||||
|
cssClass: 'customModal',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IonIcon icon={informationCircleOutline} />
|
||||||
|
View Nutrition
|
||||||
|
</IonButton>
|
||||||
|
</IonCol>
|
||||||
|
</IonRow>
|
||||||
|
|
||||||
|
{recipe.ingredients && (
|
||||||
|
<IonList>
|
||||||
|
<IonListHeader>Ingredients ({recipe.ingredients.length})</IonListHeader>
|
||||||
|
{recipe.ingredients.map((ingredient, index) => {
|
||||||
|
return <Ingredient key={`ingredient_${index}`} ingredient={ingredient} />;
|
||||||
|
})}
|
||||||
|
</IonList>
|
||||||
|
)}
|
||||||
|
</IonGrid>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Recipe;
|
@@ -0,0 +1,37 @@
|
|||||||
|
.headerImage {
|
||||||
|
|
||||||
|
img {
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
margin-top: -5rem;
|
||||||
|
border-bottom: 5px solid var(--ion-color-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerInfo {
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 10rem;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
background-color: rgba($color: #000000, $alpha: 0.8);
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--ion-color-main);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
120
03_source/mobile/src/pages/DemoRecipeApp/AppPages/Search.jsx
Normal file
120
03_source/mobile/src/pages/DemoRecipeApp/AppPages/Search.jsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
IonBackButton,
|
||||||
|
IonButton,
|
||||||
|
IonButtons,
|
||||||
|
IonCol,
|
||||||
|
IonContent,
|
||||||
|
IonGrid,
|
||||||
|
IonHeader,
|
||||||
|
IonImg,
|
||||||
|
IonList,
|
||||||
|
IonNote,
|
||||||
|
IonPage,
|
||||||
|
IonRow,
|
||||||
|
IonSearchbar,
|
||||||
|
IonText,
|
||||||
|
IonTitle,
|
||||||
|
IonToolbar,
|
||||||
|
useIonLoading,
|
||||||
|
useIonViewDidEnter,
|
||||||
|
} from '@ionic/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import styles from './Categories.module.scss';
|
||||||
|
import { performSearch } from '../utils';
|
||||||
|
import { RecipeListItem } from '../components/RecipeListItem';
|
||||||
|
|
||||||
|
const Search = () => {
|
||||||
|
const searchRef = useRef();
|
||||||
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
const [showLoader, hideLoader] = useIonLoading();
|
||||||
|
|
||||||
|
useIonViewDidEnter(() => {
|
||||||
|
searchRef.current.setFocus();
|
||||||
|
});
|
||||||
|
|
||||||
|
const search = async () => {
|
||||||
|
showLoader({
|
||||||
|
cssClass: 'customLoader',
|
||||||
|
message: 'Searching...',
|
||||||
|
duration: 9999999,
|
||||||
|
spinner: 'dots',
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchTerm = searchRef.current.value;
|
||||||
|
const data = await performSearch(searchTerm);
|
||||||
|
|
||||||
|
setSearchResults(data.hits);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
hideLoader();
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Search Recipes</IonTitle>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonBackButton color="main" text="Categories" />
|
||||||
|
</IonButtons>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
|
||||||
|
<IonContent>
|
||||||
|
<div className={styles.searchArea}>
|
||||||
|
<IonGrid>
|
||||||
|
<IonRow>
|
||||||
|
<IonCol size="9">
|
||||||
|
<IonSearchbar ref={searchRef} placeholder="Try 'Chicken Piccata'" />
|
||||||
|
</IonCol>
|
||||||
|
<IonCol size="3">
|
||||||
|
<IonButton
|
||||||
|
className={styles.searchButton}
|
||||||
|
expand="block"
|
||||||
|
color="main"
|
||||||
|
onClick={search}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</IonButton>
|
||||||
|
</IonCol>
|
||||||
|
</IonRow>
|
||||||
|
</IonGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{searchResults.length > 0 && (
|
||||||
|
<IonList>
|
||||||
|
{searchResults.map((result, index) => {
|
||||||
|
const { recipe } = result;
|
||||||
|
|
||||||
|
return <RecipeListItem recipe={recipe} key={`result_${index}`} fromSearch={true} />;
|
||||||
|
})}
|
||||||
|
</IonList>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{searchResults.length < 1 && (
|
||||||
|
<>
|
||||||
|
<IonRow className="ion-justify-content-center ion-text-center ion-margin-top ion-padding-top">
|
||||||
|
<IonCol size="8">
|
||||||
|
<IonText>Search for a recipe then select from the list to view it</IonText>
|
||||||
|
|
||||||
|
<IonNote>
|
||||||
|
<p>For development purposes, only 20 results will be returned</p>
|
||||||
|
</IonNote>
|
||||||
|
</IonCol>
|
||||||
|
</IonRow>
|
||||||
|
|
||||||
|
<IonRow className="ion-justify-content-center">
|
||||||
|
<IonCol size="8">
|
||||||
|
<IonImg src="/assets/DemoRecipeApp/placeholder.png" />
|
||||||
|
</IonCol>
|
||||||
|
</IonRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Search;
|
@@ -1,95 +0,0 @@
|
|||||||
import {
|
|
||||||
IonButton,
|
|
||||||
IonButtons,
|
|
||||||
IonCol,
|
|
||||||
IonContent,
|
|
||||||
IonHeader,
|
|
||||||
IonIcon,
|
|
||||||
IonPage,
|
|
||||||
IonRow,
|
|
||||||
IonTitle,
|
|
||||||
IonToolbar,
|
|
||||||
useIonRouter,
|
|
||||||
} from '@ionic/react';
|
|
||||||
|
|
||||||
import { Geolocation } from '@capacitor/geolocation';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { SkeletonDashboard } from '../components/SkeletonDashboard';
|
|
||||||
import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
|
|
||||||
import { CurrentWeather } from '../components/CurrentWeather';
|
|
||||||
|
|
||||||
function Tab1() {
|
|
||||||
const router = useIonRouter();
|
|
||||||
|
|
||||||
const [currentWeather, setCurrentWeather] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getCurrentPosition();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getCurrentPosition = async () => {
|
|
||||||
setCurrentWeather(false);
|
|
||||||
const coordinates = await Geolocation.getCurrentPosition();
|
|
||||||
getAddress(coordinates.coords);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAddress = async (coords) => {
|
|
||||||
const query = `${coords.latitude},${coords.longitude}`;
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${query}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
console.log(data);
|
|
||||||
setCurrentWeather(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleBackClick() {
|
|
||||||
router.goBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonPage>
|
|
||||||
<IonHeader>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>My Weather</IonTitle>
|
|
||||||
|
|
||||||
<IonButtons slot="end">
|
|
||||||
<IonButton onClick={() => getCurrentPosition()}>
|
|
||||||
<IonIcon icon={refreshOutline} color="primary" />
|
|
||||||
</IonButton>
|
|
||||||
</IonButtons>
|
|
||||||
|
|
||||||
<IonButtons slot="start">
|
|
||||||
<IonButton onClick={() => handleBackClick()}>
|
|
||||||
<IonIcon icon={chevronBackOutline} color="primary" />
|
|
||||||
</IonButton>
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
<IonContent fullscreen>
|
|
||||||
<IonHeader collapse="condense">
|
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle size="large">Dashboard</IonTitle>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
|
|
||||||
<IonRow className="ion-margin-start ion-margin-end ion-justify-content-center ion-text-center">
|
|
||||||
<IonCol size="12">
|
|
||||||
<h4>Here's your location based weather</h4>
|
|
||||||
</IonCol>
|
|
||||||
</IonRow>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '-1.5rem' }}>
|
|
||||||
{currentWeather ? (
|
|
||||||
<CurrentWeather currentWeather={currentWeather} />
|
|
||||||
) : (
|
|
||||||
<SkeletonDashboard />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</IonContent>
|
|
||||||
</IonPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Tab1;
|
|
@@ -1,81 +0,0 @@
|
|||||||
import {
|
|
||||||
IonButton,
|
|
||||||
IonCol,
|
|
||||||
IonContent,
|
|
||||||
IonHeader,
|
|
||||||
IonPage,
|
|
||||||
IonRow,
|
|
||||||
IonSearchbar,
|
|
||||||
IonTitle,
|
|
||||||
IonToolbar,
|
|
||||||
} from '@ionic/react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { CurrentWeather } from '../components/CurrentWeather';
|
|
||||||
|
|
||||||
function Tab2() {
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
const [currentWeather, setCurrentWeather] = useState(false);
|
|
||||||
|
|
||||||
const performSearch = async () => {
|
|
||||||
getAddress(search);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAddress = async (city) => {
|
|
||||||
const response = await fetch(
|
|
||||||
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${city}&aqi=no`
|
|
||||||
);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data && data.current && data.location) {
|
|
||||||
setCurrentWeather(data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonPage>
|
|
||||||
<IonHeader>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>Search</IonTitle>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
<IonContent fullscreen>
|
|
||||||
<IonHeader collapse="condense">
|
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle size="large">Search</IonTitle>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
|
|
||||||
<IonRow className="ion-justify-content-center ion-margin-top ion-align-items-center">
|
|
||||||
<IonCol size="7">
|
|
||||||
<IonSearchbar
|
|
||||||
placeholder="Try 'London'"
|
|
||||||
animated
|
|
||||||
value={search}
|
|
||||||
onIonChange={(e) => setSearch(e.target.value)}
|
|
||||||
/>
|
|
||||||
</IonCol>
|
|
||||||
|
|
||||||
<IonCol size="5">
|
|
||||||
<IonButton
|
|
||||||
expand="block"
|
|
||||||
className="ion-margin-start ion-margin-end"
|
|
||||||
onClick={performSearch}
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</IonButton>
|
|
||||||
</IonCol>
|
|
||||||
</IonRow>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '-0.8rem' }}>
|
|
||||||
{currentWeather ? (
|
|
||||||
<CurrentWeather currentWeather={currentWeather} />
|
|
||||||
) : (
|
|
||||||
<h3 className="ion-text-center">Your search result will appear here</h3>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</IonContent>
|
|
||||||
</IonPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Tab2;
|
|
@@ -1,62 +0,0 @@
|
|||||||
import { IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from '@ionic/react';
|
|
||||||
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export const WeatherProperty = ({ type, currentWeather }: { type: any; currentWeather: any }) => {
|
|
||||||
const [property, setProperty] = useState(false);
|
|
||||||
|
|
||||||
const properties = {
|
|
||||||
wind: {
|
|
||||||
isIcon: false,
|
|
||||||
icon: '/assets/WeatherDemo/wind.png',
|
|
||||||
alt: 'wind',
|
|
||||||
label: 'Wind',
|
|
||||||
value: `${currentWeather.current.wind_mph}mph`,
|
|
||||||
},
|
|
||||||
feelsLike: {
|
|
||||||
isIcon: true,
|
|
||||||
icon: thermometerOutline,
|
|
||||||
alt: 'feels like',
|
|
||||||
label: 'Feels like',
|
|
||||||
value: `${currentWeather.current.feelslike_c}°C`,
|
|
||||||
},
|
|
||||||
indexUV: {
|
|
||||||
isIcon: true,
|
|
||||||
icon: sunnyOutline,
|
|
||||||
alt: 'index uv',
|
|
||||||
label: 'Index UV',
|
|
||||||
value: currentWeather.current.uv,
|
|
||||||
},
|
|
||||||
pressure: {
|
|
||||||
isIcon: true,
|
|
||||||
icon: pulseOutline,
|
|
||||||
alt: 'pressure',
|
|
||||||
label: 'Pressure',
|
|
||||||
value: `${currentWeather.current.pressure_mb} mbar`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setProperty(properties[type]);
|
|
||||||
}, [type]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonCol size="6">
|
|
||||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
|
||||||
<IonCol size="3">
|
|
||||||
{!property.isIcon && (
|
|
||||||
<img alt={property.alt} src={property.icon} height="32" width="32" />
|
|
||||||
)}
|
|
||||||
{property.isIcon && (
|
|
||||||
<IonIcon icon={property.icon} color="medium" style={{ fontSize: '2rem' }} />
|
|
||||||
)}
|
|
||||||
</IonCol>
|
|
||||||
|
|
||||||
<IonCol size="9">
|
|
||||||
<IonCardSubtitle>{property.label}</IonCardSubtitle>
|
|
||||||
<IonNote>{property.value}</IonNote>
|
|
||||||
</IonCol>
|
|
||||||
</IonRow>
|
|
||||||
</IonCol>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -1,48 +0,0 @@
|
|||||||
import { IonCard, IonCardContent, IonGrid, IonRow, IonText, IonCardTitle } from '@ionic/react';
|
|
||||||
import { WeatherProperty } from './WeatherProperty';
|
|
||||||
|
|
||||||
export const CurrentWeather = ({ currentWeather }: { currentWeather: any }) => (
|
|
||||||
<IonGrid>
|
|
||||||
<IonCard>
|
|
||||||
<IonCardContent className="ion-text-center">
|
|
||||||
<IonText color="primary">
|
|
||||||
<h1>
|
|
||||||
{currentWeather.location.region},{' '}
|
|
||||||
<span style={{ color: 'gray' }}>{currentWeather.location.country}</span>
|
|
||||||
</h1>
|
|
||||||
</IonText>
|
|
||||||
|
|
||||||
<div className="ion-margin-top">
|
|
||||||
<img
|
|
||||||
alt="condition"
|
|
||||||
src={currentWeather.current.condition.icon.replace('//', 'https://')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<IonText color="dark">
|
|
||||||
<h1 style={{ fontWeight: 'bold' }}>{currentWeather.current.condition.text}</h1>
|
|
||||||
</IonText>
|
|
||||||
|
|
||||||
<IonText color="medium">
|
|
||||||
<p>{new Date(currentWeather.location.localtime).toDateString()}</p>
|
|
||||||
</IonText>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
|
|
||||||
{currentWeather.current.temp_c}℃
|
|
||||||
</IonCardTitle>
|
|
||||||
|
|
||||||
<IonGrid className="ion-margin-top">
|
|
||||||
<IonRow>
|
|
||||||
<WeatherProperty type="wind" currentWeather={currentWeather} />
|
|
||||||
<WeatherProperty type="feelsLike" currentWeather={currentWeather} />
|
|
||||||
</IonRow>
|
|
||||||
|
|
||||||
<IonRow className="ion-margin-top">
|
|
||||||
<WeatherProperty type="indexUV" currentWeather={currentWeather} />
|
|
||||||
<WeatherProperty type="pressure" currentWeather={currentWeather} />
|
|
||||||
</IonRow>
|
|
||||||
</IonGrid>
|
|
||||||
</IonCardContent>
|
|
||||||
</IonCard>
|
|
||||||
</IonGrid>
|
|
||||||
);
|
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { IonItem, IonLabel } from "@ionic/react";
|
||||||
|
import styles from "./Ingredient.module.scss";
|
||||||
|
|
||||||
|
export const Ingredient = ({ ingredient }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<IonItem lines="full" className={ styles.ingredientItem }>
|
||||||
|
<img alt="ingredient" src={ ingredient.image } className={ styles.ingredientImage } />
|
||||||
|
<IonLabel className="ion-text-wrap ion-margin-start">
|
||||||
|
<h3>{ ingredient.text }</h3>
|
||||||
|
<p>{ ingredient.weight.toFixed(2) }g</p>
|
||||||
|
</IonLabel>
|
||||||
|
</IonItem>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,13 @@
|
|||||||
|
.ingredientImage {
|
||||||
|
|
||||||
|
height: 3rem;
|
||||||
|
width: 3rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgb(172, 172, 172);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredientItem {
|
||||||
|
|
||||||
|
--padding-top: 1rem;
|
||||||
|
--padding-bottom: 1rem;
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
import { IonButton, IonButtons, IonContent, IonGrid, IonHeader, IonList, IonPage, IonRow, IonTitle, IonToolbar } from "@ionic/react"
|
||||||
|
import { Ingredient } from "./Ingredient";
|
||||||
|
|
||||||
|
const IngredientsModal = ({ dismiss, ingredients}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>View Ingredients</IonTitle>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonButton color="main" onClick={ dismiss }>Close</IonButton>
|
||||||
|
</IonButtons>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
|
||||||
|
<IonContent>
|
||||||
|
<IonList>
|
||||||
|
{ ingredients.map((ingredient, index) => {
|
||||||
|
|
||||||
|
return <Ingredient key={ `ingredient_${ index }` } ingredient={ ingredient } />;
|
||||||
|
})}
|
||||||
|
</IonList>
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IngredientsModal;
|
@@ -0,0 +1,48 @@
|
|||||||
|
import { IonButton, IonButtons, IonCardSubtitle, IonCol, IonContent, IonGrid, IonHeader, IonPage, IonRow, IonTitle, IonToolbar } from "@ionic/react"
|
||||||
|
import NutritionalFact from "./NutritionalFact";
|
||||||
|
|
||||||
|
const NutritionModal = ({ dismiss, recipe }) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>View Nutrition</IonTitle>
|
||||||
|
<IonButtons slot="start">
|
||||||
|
<IonButton color="main" onClick={ dismiss }>Close</IonButton>
|
||||||
|
</IonButtons>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
|
||||||
|
<IonContent>
|
||||||
|
|
||||||
|
{ (recipe && recipe.digest) &&
|
||||||
|
|
||||||
|
<IonGrid>
|
||||||
|
<IonRow>
|
||||||
|
<IonCol size="12">
|
||||||
|
<IonCardSubtitle className="ion-text-center" color="main">
|
||||||
|
Based on a serving size of { recipe.totalWeight.toFixed(0) }g
|
||||||
|
</IonCardSubtitle>
|
||||||
|
<NutritionalFact type="calories" amount={ recipe.calories.toFixed(0) } />
|
||||||
|
<NutritionalFact type="fat" amount={ recipe.digest[0].total.toFixed(0) } />
|
||||||
|
<NutritionalFact type="trans_fat" amount={ recipe.digest[0].sub[1].total.toFixed(0) } inset={ true } />
|
||||||
|
<NutritionalFact type="saturated_fat" amount={ recipe.digest[0].sub[0].total.toFixed(0) } inset={ true } />
|
||||||
|
<NutritionalFact type="polyunsaturated_fat" amount={ recipe.digest[0].sub[3].total.toFixed(0) } inset={ true } />
|
||||||
|
<NutritionalFact type="monounsaturated_fat" amount={ recipe.digest[0].sub[2].total.toFixed(0) } inset={ true } />
|
||||||
|
<NutritionalFact type="carbs" amount={ recipe.digest[1].total.toFixed(0) } />
|
||||||
|
<NutritionalFact type="sugar" amount={ recipe.digest[1].sub[2].total.toFixed(0) } inset={ true } />
|
||||||
|
<NutritionalFact type="fibre" amount={ recipe.digest[1].sub[1].total.toFixed(0) } inset={ true } />
|
||||||
|
<NutritionalFact type="sugars_added" amount={ recipe.digest[1].sub[3].total.toFixed(0) } inset={ true } />
|
||||||
|
<NutritionalFact type="protein" amount={ recipe.digest[2].total.toFixed(0) } />
|
||||||
|
</IonCol>
|
||||||
|
</IonRow>
|
||||||
|
</IonGrid>
|
||||||
|
}
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NutritionModal;
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { IonCardTitle, IonCol, IonRow } from "@ionic/react";
|
||||||
|
|
||||||
|
const NutritionalFact = ({ type, amount, inset }) => {
|
||||||
|
|
||||||
|
const label = type.replace("_", " ").replace(/(^\w{1})|(\s+\w{1})/g, letter => letter.toUpperCase());
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<IonRow style={{ borderBottom: "1px solid #242424", padding: "0.5rem" }}>
|
||||||
|
<IonCol size="9">
|
||||||
|
<IonCardTitle style={{ fontSize: "0.9rem", marginLeft: inset ? "1.5rem" : "" }}>
|
||||||
|
{ label }
|
||||||
|
</IonCardTitle>
|
||||||
|
</IonCol>
|
||||||
|
|
||||||
|
<IonCol size="3">
|
||||||
|
<IonCardTitle style={{ fontSize: "0.9rem" }}>
|
||||||
|
{ amount }{ type !== "calories" && "g" }
|
||||||
|
</IonCardTitle>
|
||||||
|
</IonCol>
|
||||||
|
</IonRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NutritionalFact;
|
@@ -0,0 +1,18 @@
|
|||||||
|
import { IonItem, IonLabel } from "@ionic/react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import styles from "./RecipeListItem.module.scss";
|
||||||
|
|
||||||
|
export const RecipeListItem = ({ recipe, fromSearch = false, fromBookmarks = false }) => (
|
||||||
|
|
||||||
|
<Link to={{ pathname: `/recipe/${ recipe.label.replace(" ", "").toLowerCase() }`, state: { recipe, fromSearch, fromBookmarks }}}>
|
||||||
|
<IonItem detail={ true } lines="full" className={ styles.categoryItem }>
|
||||||
|
|
||||||
|
<img src={ recipe.image } alt="cover" className={ styles.categoryImage } />
|
||||||
|
|
||||||
|
<IonLabel className={ styles.categoryDetails }>
|
||||||
|
<h2>{ recipe.label }</h2>
|
||||||
|
<p>{ recipe.dishType && recipe.dishType[0] }</p>
|
||||||
|
</IonLabel>
|
||||||
|
</IonItem>
|
||||||
|
</Link>
|
||||||
|
);
|
@@ -0,0 +1,17 @@
|
|||||||
|
.categoryDetails {
|
||||||
|
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categoryImage {
|
||||||
|
|
||||||
|
width: 5rem;
|
||||||
|
height: 5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categoryItem {
|
||||||
|
|
||||||
|
--padding-top: 1.5rem;
|
||||||
|
--padding-bottom: 1.5rem;
|
||||||
|
}
|
@@ -1,117 +0,0 @@
|
|||||||
import {
|
|
||||||
IonCard,
|
|
||||||
IonCardContent,
|
|
||||||
IonCardSubtitle,
|
|
||||||
IonCardTitle,
|
|
||||||
IonCol,
|
|
||||||
IonGrid,
|
|
||||||
IonIcon,
|
|
||||||
IonNote,
|
|
||||||
IonRow,
|
|
||||||
IonSkeletonText,
|
|
||||||
IonText,
|
|
||||||
IonThumbnail,
|
|
||||||
} from '@ionic/react';
|
|
||||||
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
|
|
||||||
|
|
||||||
export const SkeletonDashboard = () => (
|
|
||||||
<IonGrid>
|
|
||||||
<IonCard>
|
|
||||||
<IonCardContent className="ion-text-center">
|
|
||||||
<IonText color="primary">
|
|
||||||
<h1>
|
|
||||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
|
||||||
</h1>
|
|
||||||
</IonText>
|
|
||||||
|
|
||||||
<div className="ion-margin-top">
|
|
||||||
<IonThumbnail>
|
|
||||||
<IonSkeletonText animated style={{ width: '2rem', height: '2rem' }} />
|
|
||||||
</IonThumbnail>
|
|
||||||
|
|
||||||
<IonText color="dark">
|
|
||||||
<h1 style={{ fontWeight: 'bold' }}>
|
|
||||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
|
||||||
</h1>
|
|
||||||
</IonText>
|
|
||||||
|
|
||||||
<IonText color="medium">
|
|
||||||
<p>
|
|
||||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
|
||||||
</p>
|
|
||||||
</IonText>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
|
|
||||||
<IonSkeletonText animated style={{ height: '3rem', width: '30%', textAlign: 'center' }} />
|
|
||||||
</IonCardTitle>
|
|
||||||
|
|
||||||
<IonGrid className="ion-margin-top">
|
|
||||||
<IonRow>
|
|
||||||
<IonCol size="6">
|
|
||||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
|
||||||
<IonCol size="3">
|
|
||||||
<img alt="wind" src="/assets/WeatherDemo/wind.png" height="32" width="32" />
|
|
||||||
</IonCol>
|
|
||||||
|
|
||||||
<IonCol size="9">
|
|
||||||
<IonCardSubtitle>Wind</IonCardSubtitle>
|
|
||||||
<IonNote>
|
|
||||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
|
||||||
</IonNote>
|
|
||||||
</IonCol>
|
|
||||||
</IonRow>
|
|
||||||
</IonCol>
|
|
||||||
|
|
||||||
<IonCol size="6">
|
|
||||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
|
||||||
<IonCol size="3">
|
|
||||||
<IonIcon icon={thermometerOutline} color="medium" style={{ fontSize: '2rem' }} />
|
|
||||||
</IonCol>
|
|
||||||
|
|
||||||
<IonCol size="9">
|
|
||||||
<IonCardSubtitle>Feels like</IonCardSubtitle>
|
|
||||||
<IonNote>
|
|
||||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
|
||||||
</IonNote>
|
|
||||||
</IonCol>
|
|
||||||
</IonRow>
|
|
||||||
</IonCol>
|
|
||||||
</IonRow>
|
|
||||||
|
|
||||||
<IonRow className="ion-margin-top">
|
|
||||||
<IonCol size="6">
|
|
||||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
|
||||||
<IonCol size="3">
|
|
||||||
<IonIcon icon={sunnyOutline} color="medium" style={{ fontSize: '2rem' }} />
|
|
||||||
</IonCol>
|
|
||||||
|
|
||||||
<IonCol size="9">
|
|
||||||
<IonCardSubtitle>Index UV</IonCardSubtitle>
|
|
||||||
<IonNote>
|
|
||||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
|
||||||
</IonNote>
|
|
||||||
</IonCol>
|
|
||||||
</IonRow>
|
|
||||||
</IonCol>
|
|
||||||
|
|
||||||
<IonCol size="6">
|
|
||||||
<IonRow className="ion-justify-content-center ion-align-items-center">
|
|
||||||
<IonCol size="3">
|
|
||||||
<IonIcon icon={pulseOutline} color="medium" style={{ fontSize: '2rem' }} />
|
|
||||||
</IonCol>
|
|
||||||
|
|
||||||
<IonCol size="9">
|
|
||||||
<IonCardSubtitle>Pressure</IonCardSubtitle>
|
|
||||||
<IonNote>
|
|
||||||
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
|
|
||||||
</IonNote>
|
|
||||||
</IonCol>
|
|
||||||
</IonRow>
|
|
||||||
</IonCol>
|
|
||||||
</IonRow>
|
|
||||||
</IonGrid>
|
|
||||||
</IonCardContent>
|
|
||||||
</IonCard>
|
|
||||||
</IonGrid>
|
|
||||||
);
|
|
@@ -3,36 +3,39 @@ import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs }
|
|||||||
import { cloudOutline, searchOutline } from 'ionicons/icons';
|
import { cloudOutline, searchOutline } from 'ionicons/icons';
|
||||||
import { Route, Redirect } from 'react-router';
|
import { Route, Redirect } from 'react-router';
|
||||||
|
|
||||||
import Tab1 from './AppPages/Tab1';
|
import Categories from './AppPages/Categories';
|
||||||
import Tab2 from './AppPages/Tab2';
|
import Recipe from './AppPages/Recipe';
|
||||||
|
import Category from './AppPages/Category';
|
||||||
|
import Search from './AppPages/Search';
|
||||||
|
import Bookmarks from './AppPages/Bookmarks';
|
||||||
|
|
||||||
function DemoWeatherApp() {
|
import './style.scss';
|
||||||
|
|
||||||
|
function DemoRecipeApp() {
|
||||||
return (
|
return (
|
||||||
<IonTabs>
|
<IonRouterOutlet className="demo-recipe-app">
|
||||||
<IonRouterOutlet>
|
<Route exact path="/demo-recipe-app/categories">
|
||||||
<Route exact path="/demo-recipe-app/tab1">
|
<Categories />
|
||||||
<Tab1 />
|
</Route>
|
||||||
</Route>
|
<Route exact path="/demo-recipe-app/recipe/:id">
|
||||||
<Route exact path="/demo-recipe-app/tab2">
|
<Recipe />
|
||||||
<Tab2 />
|
</Route>
|
||||||
</Route>
|
|
||||||
<Route exact path="/demo-recipe-app">
|
<Route exact path="/demo-recipe-app/category/:name">
|
||||||
<Redirect to="/demo-recipe-app/tab1" />
|
<Category />
|
||||||
</Route>
|
</Route>
|
||||||
</IonRouterOutlet>
|
|
||||||
{/* */}
|
<Route exact path="/demo-recipe-app/search">
|
||||||
<IonTabBar slot="bottom">
|
<Search />
|
||||||
<IonTabButton tab="tab1" href="/demo-recipe-app/tab1">
|
</Route>
|
||||||
<IonIcon icon={cloudOutline} />
|
|
||||||
<IonLabel>Dashboard</IonLabel>
|
<Route exact path="/demo-recipe-app/bookmarks">
|
||||||
</IonTabButton>
|
<Bookmarks />
|
||||||
<IonTabButton tab="tab2" href="/demo-recipe-app/tab2">
|
</Route>
|
||||||
<IonIcon icon={searchOutline} />
|
|
||||||
<IonLabel>Search</IonLabel>
|
<Redirect exact path="/demo-recipe-app" to="/demo-recipe-app/categories" />
|
||||||
</IonTabButton>
|
</IonRouterOutlet>
|
||||||
</IonTabBar>
|
|
||||||
</IonTabs>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DemoWeatherApp;
|
export default DemoRecipeApp;
|
||||||
|
85889
03_source/mobile/src/pages/DemoRecipeApp/recipes.js
Normal file
85889
03_source/mobile/src/pages/DemoRecipeApp/recipes.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
import { Store } from "pullstate";
|
||||||
|
|
||||||
|
const BookmarkStore = new Store({
|
||||||
|
|
||||||
|
recipes: []
|
||||||
|
});
|
||||||
|
|
||||||
|
export default BookmarkStore;
|
||||||
|
|
||||||
|
export const addToBookmarks = (passedRecipe) => {
|
||||||
|
|
||||||
|
const currentBookmarks = BookmarkStore.getRawState().recipes;
|
||||||
|
const added = !currentBookmarks.includes(passedRecipe);
|
||||||
|
|
||||||
|
BookmarkStore.update(s => {
|
||||||
|
|
||||||
|
if (currentBookmarks.includes(passedRecipe)) {
|
||||||
|
|
||||||
|
s.recipes = currentBookmarks.filter(bookmark => bookmark !== passedRecipe);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
s.recipes = [ ...s.recipes, passedRecipe ];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
@@ -0,0 +1,6 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const getState = state => state;
|
||||||
|
|
||||||
|
// General getters
|
||||||
|
export const getBookmarks = createSelector(getState, state => state.recipes);
|
1
03_source/mobile/src/pages/DemoRecipeApp/store/index.js
Normal file
1
03_source/mobile/src/pages/DemoRecipeApp/store/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as BookmarkStore } from "./BookmarkStore";
|
@@ -1,103 +1,137 @@
|
|||||||
#about-page {
|
/* Ionic Variables and Theming. For more info, please see:
|
||||||
ion-toolbar {
|
http://ionicframework.com/docs/theming/ */
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
top: 0;
|
/** Ionic CSS Variables **/
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
--background: transparent;
|
.demo-recipe-app {
|
||||||
|
* {
|
||||||
|
/** primary **/
|
||||||
|
--ion-color-primary: #3880ff;
|
||||||
|
--ion-color-primary-rgb: 56, 128, 255;
|
||||||
|
--ion-color-primary-contrast: #ffffff;
|
||||||
|
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||||
|
--ion-color-primary-shade: #3171e0;
|
||||||
|
--ion-color-primary-tint: #4c8dff;
|
||||||
|
|
||||||
|
/** secondary **/
|
||||||
|
--ion-color-secondary: #3dc2ff;
|
||||||
|
--ion-color-secondary-rgb: 61, 194, 255;
|
||||||
|
--ion-color-secondary-contrast: #ffffff;
|
||||||
|
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||||
|
--ion-color-secondary-shade: #36abe0;
|
||||||
|
--ion-color-secondary-tint: #50c8ff;
|
||||||
|
|
||||||
|
/** tertiary **/
|
||||||
|
--ion-color-tertiary: #5260ff;
|
||||||
|
--ion-color-tertiary-rgb: 82, 96, 255;
|
||||||
|
--ion-color-tertiary-contrast: #ffffff;
|
||||||
|
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||||
|
--ion-color-tertiary-shade: #4854e0;
|
||||||
|
--ion-color-tertiary-tint: #6370ff;
|
||||||
|
|
||||||
|
/** success **/
|
||||||
|
--ion-color-success: #2dd36f;
|
||||||
|
--ion-color-success-rgb: 45, 211, 111;
|
||||||
|
--ion-color-success-contrast: #ffffff;
|
||||||
|
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||||
|
--ion-color-success-shade: #28ba62;
|
||||||
|
--ion-color-success-tint: #42d77d;
|
||||||
|
|
||||||
|
/** warning **/
|
||||||
|
--ion-color-warning: #ffc409;
|
||||||
|
--ion-color-warning-rgb: 255, 196, 9;
|
||||||
|
--ion-color-warning-contrast: #000000;
|
||||||
|
--ion-color-warning-contrast-rgb: 0, 0, 0;
|
||||||
|
--ion-color-warning-shade: #e0ac08;
|
||||||
|
--ion-color-warning-tint: #ffca22;
|
||||||
|
|
||||||
|
/** danger **/
|
||||||
|
--ion-color-danger: #eb445a;
|
||||||
|
--ion-color-danger-rgb: 235, 68, 90;
|
||||||
|
--ion-color-danger-contrast: #ffffff;
|
||||||
|
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||||
|
--ion-color-danger-shade: #cf3c4f;
|
||||||
|
--ion-color-danger-tint: #ed576b;
|
||||||
|
|
||||||
|
/** dark **/
|
||||||
|
--ion-color-dark: #222428;
|
||||||
|
--ion-color-dark-rgb: 34, 36, 40;
|
||||||
|
--ion-color-dark-contrast: #ffffff;
|
||||||
|
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||||
|
--ion-color-dark-shade: #1e2023;
|
||||||
|
--ion-color-dark-tint: #383a3e;
|
||||||
|
|
||||||
|
/** medium **/
|
||||||
|
--ion-color-medium: #92949c;
|
||||||
|
--ion-color-medium-rgb: 146, 148, 156;
|
||||||
|
--ion-color-medium-contrast: #ffffff;
|
||||||
|
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||||
|
--ion-color-medium-shade: #808289;
|
||||||
|
--ion-color-medium-tint: #9d9fa6;
|
||||||
|
|
||||||
|
/** light **/
|
||||||
|
--ion-color-light: #f4f5f8;
|
||||||
|
--ion-color-light-rgb: 244, 245, 248;
|
||||||
|
--ion-color-light-contrast: #000000;
|
||||||
|
--ion-color-light-contrast-rgb: 0, 0, 0;
|
||||||
|
--ion-color-light-shade: #d7d8da;
|
||||||
|
--ion-color-light-tint: #f5f6f9;
|
||||||
|
|
||||||
|
--ion-color-main: #1bad64;
|
||||||
|
--ion-color-main-rgb: 27, 173, 100;
|
||||||
|
--ion-color-main-contrast: #ffffff;
|
||||||
|
--ion-color-main-contrast-rgb: 255, 255, 255;
|
||||||
|
--ion-color-main-shade: #189858;
|
||||||
|
--ion-color-main-tint: #32b574;
|
||||||
|
|
||||||
|
--ion-background-color: white;
|
||||||
|
--ion-tab-bar-background: rgb(36, 36, 36);
|
||||||
|
--ion-toolbar-background: rgb(36, 36, 36);
|
||||||
|
--ion-tab-bar-color-selected: rgb(27, 173, 100);
|
||||||
|
--ion-tab-bar-color: rgb(97, 97, 97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ion-color-main {
|
||||||
|
--ion-color-base: var(--ion-color-main);
|
||||||
|
--ion-color-base-rgb: var(--ion-color-main-rgb);
|
||||||
|
--ion-color-contrast: var(--ion-color-main-contrast);
|
||||||
|
--ion-color-contrast-rgb: var(--ion-color-main-contrast-rgb);
|
||||||
|
--ion-color-shade: var(--ion-color-main-shade);
|
||||||
|
--ion-color-tint: var(--ion-color-main-tint);
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-toolbar ion-title {
|
||||||
--color: white;
|
--color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
ion-toolbar ion-back-button,
|
ion-toolbar ion-back-button,
|
||||||
ion-toolbar ion-button,
|
ion-toolbar ion-button {
|
||||||
ion-toolbar ion-menu-button {
|
--color: rgb(27, 173, 100);
|
||||||
--color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-header {
|
ion-content ion-toolbar ion-title {
|
||||||
position: relative;
|
color: rgb(36, 36, 36);
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 30%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-header .about-image {
|
* {
|
||||||
position: absolute;
|
font-family: 'Ubuntu', sans-serif;
|
||||||
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
background-position: center;
|
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
transition: opacity 500ms ease-in-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-header .madison {
|
ion-toolbar {
|
||||||
background-image: url('/assets/WeatherDemo/img/about/madison.jpg');
|
--border-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-header .austin {
|
.customLoader {
|
||||||
background-image: url('/assets/WeatherDemo/img/about/austin.jpg');
|
--background: var(--ion-toolbar-background);
|
||||||
|
--spinner-color: var(--ion-tab-bar-color-selected);
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-header .chicago {
|
.customModal {
|
||||||
background-image: url('/assets/WeatherDemo/img/about/chicago.jpg');
|
--background: var(--ion-toolbar-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.about-header .seattle {
|
a {
|
||||||
background-image: url('/assets/WeatherDemo/img/about/seattle.jpg');
|
text-decoration: none;
|
||||||
}
|
|
||||||
|
|
||||||
.about-info {
|
|
||||||
position: relative;
|
|
||||||
margin-top: -10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--ion-background-color, #fff);
|
|
||||||
z-index: 2; // display rounded border above header image
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-info h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-info ion-list {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-info p {
|
|
||||||
line-height: 130%;
|
|
||||||
|
|
||||||
color: var(--ion-color-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-info ion-icon {
|
|
||||||
margin-inline-end: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* iOS Only
|
|
||||||
*/
|
|
||||||
|
|
||||||
.ios .about-info {
|
|
||||||
--ion-padding: 19px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ios .about-info h3 {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#date-input-popover {
|
|
||||||
--offset-y: -var(--ion-safe-area-bottom);
|
|
||||||
|
|
||||||
--max-width: 90%;
|
|
||||||
--width: 336px;
|
|
||||||
}
|
|
||||||
|
9
03_source/mobile/src/pages/DemoRecipeApp/utils.js
Normal file
9
03_source/mobile/src/pages/DemoRecipeApp/utils.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const APP_ID = "ea1d37d5";
|
||||||
|
const APP_KEY = "fd382a172ba8d6668c0430dc9c14a181";
|
||||||
|
|
||||||
|
export const performSearch = async searchTerm => {
|
||||||
|
|
||||||
|
const response = await fetch(`https://api.edamam.com/api/recipes/v2?type=public&q=${ searchTerm }&app_id=${ APP_ID }&app_key=${ APP_KEY }`);
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
}
|
Reference in New Issue
Block a user