remake app ohome setup /notworking

This commit is contained in:
2021-10-24 22:18:31 +02:00
parent 4568076445
commit 55c6704d1b
76 changed files with 22173 additions and 749 deletions

33
App.js
View File

@@ -1,12 +1,35 @@
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
// import 'bootstrap/dist/css/bootstrap.min.css';
export default function App() {
import App from './src/App';
// //styles
import { ThemeProvider } from 'styled-components'
// import GlobalStyle from './src/styles/globalStyles'
import Theme from './src/styles/theme'
// //redux
import { Provider } from 'react-redux'
import store from './src/redux/store';
import { fetchAll } from './src/utils'
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, View } from 'react-native';
fetchAll()
export default function Index() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<Provider store={store}>
<ThemeProvider theme={Theme}>
{/* <GlobalStyle /> */}
<App />
<StatusBar style="auto" />
</ThemeProvider>
</Provider>
</View>
);
}
@@ -15,7 +38,5 @@ const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

21
index.js Normal file
View File

@@ -0,0 +1,21 @@
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});

16730
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,26 @@
"eject": "expo eject"
},
"dependencies": {
"@react-native-community/masked-view": "^0.1.11",
"@react-navigation/elements": "^1.2.1",
"@react-navigation/native": "^6.0.6",
"@react-navigation/native-stack": "^6.2.5",
"@react-navigation/stack": "^6.0.11",
"@reduxjs/toolkit": "^1.6.2",
"expo": "~43.0.0",
"expo-status-bar": "~1.1.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-icons": "^4.3.1",
"react-native": "0.64.2",
"react-native-web": "0.17.1"
"react-native-gesture-handler": "~1.10.2",
"react-native-modal": "^13.0.0",
"react-native-reanimated": "~2.2.0",
"react-native-safe-area-context": "3.3.2",
"react-native-screens": "~3.8.0",
"react-native-web": "0.17.1",
"react-redux": "^7.2.5",
"styled-components": "^5.3.3"
},
"devDependencies": {
"@babel/core": "^7.12.9"

99
src/App.js Normal file
View File

@@ -0,0 +1,99 @@
import React, { useEffect } from 'react';
import { TouchableOpacity, View } from 'react-native';
//components
import HomePage from './pages/HomePage';
import GroceryListPage from './pages/groceryList/GroceryListPage';
import RecipePage from './pages/recipes/RecipesPage';
import AddRecipe from './pages/recipes/AddRecipe';
import theme from './styles/theme';
//dependencies
//navigation
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useSelector } from 'react-redux';
const MyTheme = {
dark: true,
colors: {
background: theme.colors.dp00,
},
};
// const ScrollToTop = () => {
// const { pathname } = useLocation();
// useEffect(() => {
// window.scrollTo(0, 0);
// }, [pathname]);
// return null
// }
const Stack = createNativeStackNavigator();
function App() {
const statusRecipes = useSelector(state => state.recipes.status)
return (
<NavigationContainer theme={MyTheme}>
<Stack.Navigator screenOptions={{
headerStyle: {
backgroundColor: theme.colors.dp00 + 'bb'
}, //COLORS.dp08
headerShadowVisible: false,
headerTintColor: theme.colors.textW0,
headerTitleStyle: { fontSize: 32 },
cardOverlayEnabled: true,
headerTransparent: true,
}}>
<Stack.Screen name="Home" component={HomePage} options={{
headerLeft: () => (
< TouchableOpacity
onPress={() => alert('This is a button!')}
>
</TouchableOpacity>
),
}} />
<Stack.Screen name="Groceries" component={GroceryListPage} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
{/* <Router>
<View>
<ScrollToTop />
<Switch>
<Route exact path="/groceryList">
<GroceryListPage />
</Route>
<Route path="/recipes/addRecipe/:id">
{statusRecipes === 'idle' &&
< AddRecipe />
}
</Route>
<Route exact path="/recipes/addRecipe">
<AddRecipe />
</Route>
<Route path="/recipes/:id">
{statusRecipes === 'idle' &&
< Recipe />
}
</Route>
<Route path="/recipes">
<RecipePage />
</Route>
<Route path="/">
<HomePage />
</Route>
<Route exact path="/">
<Redirect to="/" />
</Route>
</Switch>
</View>
</Router> */}

View File

@@ -0,0 +1,68 @@
import React from 'react'
import styled from 'styled-components'
import { Text, View } from 'react-native'
const WrapperDropdown = styled(View)`
display: flex;
flex-direction: column;
position: absolute;
z-index:2;
overflow-x: hidden;
overflow-y: auto;
background-color: ${({ theme }) => theme.colors.dp01 + 'cc'};
width: 200px;
max-height: 100px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
`
const DropdownItem = styled(Text)`
color: ${({ theme }) => theme.colors.textW2};
font-size: 20px;
&:hover{
background-color: #0001;
}
`
const HorizontalSeperator = styled(View)`
width: 100%;
height: 1px;
background-color: ${({ theme }) => theme.colors.dp02};
`
const Dropdown = (props) => {
let text = props.text
text = text.replace(/\W/g, '')
let regex = new RegExp(text, "i");
let newArray = props.array.filter((element) =>
regex.test(element)
)
if (newArray.length === 1 && newArray[0] === text) {
newArray = [];
}
let dropdownList = newArray.map((element, index) => {
return (
<View key={index}>
<DropdownItem onClick={() => props.setElement(element)}>{element}</DropdownItem>
<HorizontalSeperator />
</View>
)
})
return (
<WrapperDropdown>
{dropdownList}
</WrapperDropdown>
)
}
export const QtDropdown = (props) => {
const quantities = ["g", "kg", "mL", "L"]
return <Dropdown
array={quantities}
text={props.text}
setElement={props.setElement} />
}
export default Dropdown

View File

@@ -0,0 +1,28 @@
import React from 'react'
//styles
import {Wrapper,WrapperSelected, WrapperGroceries,IconGroceries, WrapperProducts, IconProducts} from './styles/tabMenu'
const calcPos = (pos) => {
return pos === 'Groceries' ? '-75px'
: '75px'
}
let previousPos = '-150px'
const TabMenu = (props) => {
const handleTabPress = (tab) => {
previousPos = calcPos(props.currentTab)
props.setCurrentTab(tab)
}
return (
<Wrapper>
<WrapperSelected previousPos = {previousPos} currenttab={props.currentTab}/>
<WrapperGroceries currenttab={props.currentTab} onClick={()=>handleTabPress("Groceries")}>
<IconGroceries currenttab={props.currentTab}/>
</WrapperGroceries>
<WrapperProducts currenttab={props.currentTab} onClick={()=>handleTabPress("Products")}>
<IconProducts currenttab={props.currentTab}/>
</WrapperProducts>
</Wrapper>
)
}
export default TabMenu

View File

@@ -0,0 +1,77 @@
import React, { useState } from 'react'
//components
import ModalAddItem from '../../modals/groceries/ModalAddItem'
import ModalAddList from '../../modals/groceries/ModalAddList'
//styles
import { WrapperButtons, WrapperAddItem, WrapperAddList, WrapperRemove, WrapperSelect, PlusIcon, MenuIcon, RemoveIcon, CheckIcon, ListIcon } from './styles/buttons'
//redux
import { useSelector, useDispatch } from 'react-redux'
import { itemsRemoved, checkAll } from '../../../redux/slices/groceryList/itemsSlice'
import { selectOpenListId } from '../../../redux/slices/groceryList/listsSlice'
const SelectAllItemsButton = (props) => {
const dispatch = useDispatch()
const listId = useSelector(selectOpenListId)
const [toggleAnim, setToggleAnim] = useState(false)
const handlePress = () => {
listId && dispatch(checkAll(listId));
listId && setToggleAnim(!toggleAnim)
}
return (
<WrapperSelect toggle={toggleAnim} visible={props.visible} onClick={handlePress}>
<CheckIcon />
</WrapperSelect>
)
}
const RemoveItemsButton = (props) => {
const dispatch = useDispatch()
const [toggleAnim, setToggleAnim] = useState(false)
const handlePress = () => {
if (window.confirm("Do you really want to remove the selected items?")) {
dispatch(itemsRemoved())
setToggleAnim(!toggleAnim)
}
}
return (
<WrapperRemove toggle={toggleAnim} visible={props.visible} onClick={handlePress}>
<RemoveIcon />
</WrapperRemove>
)
}
export const ContainerButtons = (props) => {
const [visible, setVisible] = useState(false);
return (
<>
<SelectAllItemsButton visible={visible} />
<RemoveItemsButton visible={visible} />
<WrapperButtons toggle={visible} onClick={() => setVisible(!visible)}>
<MenuIcon />
</WrapperButtons >
</>
)
}
export const AddItemButton = (props) => {
const [visibleItem, setVisibleItem] = useState(false);
const [visibleList, setVisibleList] = useState(false);
const lists = useSelector(state => state.lists.entities)
if (lists.find((list) => list.open === true)) {
return (
<>
<WrapperAddItem toggle={visibleItem} onClick={() => setVisibleItem(true)}>
<PlusIcon />
</WrapperAddItem>
<ModalAddItem visible={visibleItem} closeModal={() => setVisibleItem(false)} />
</>
)
}
return (
<>
<WrapperAddList toggle={visibleList} onClick={() => setVisibleList(true)}>
<ListIcon />
</WrapperAddList>
<ModalAddList visible={visibleList} closeModal={() => setVisibleList(false)} />
</>
)
}

View File

@@ -0,0 +1,35 @@
import React from 'react'
//components
import List from './List'
//redux
import { useSelector } from 'react-redux';
import { selectAllSortedItems } from '../../../redux/slices/groceryList/itemsSlice'
//styles
import { WrapperGroceryList, TextTotalAmount } from './styles/list'
import { selectAllProducts } from '../../../redux/slices/groceryList/productsSlice';
import { View } from 'react-native';
const GroceryList = (props) => {
const items = useSelector(selectAllSortedItems)
const products = useSelector(selectAllProducts)
const lists = useSelector(state => state.lists.entities)
let Lists = lists.map((list, index) => {
return <List key={index} listId={list._id} />
})
const totalPrice = Math.round(items.reduce((accumulator, item) => {
let product = products.find(product => product._id === item._id && item.checked)
accumulator += (product ? product.price : 0) * item.amount.am
accumulator += (item.price && item.checked ? item.price : 0) * item.amount.am
return accumulator
}, 0) * 100) / 100
return (
<WrapperGroceryList>
<TextTotalAmount>total: {totalPrice}</TextTotalAmount>
{Lists}
<View style={{ height: 150 }} />
</WrapperGroceryList>
)
}
export default GroceryList

View File

@@ -0,0 +1,69 @@
import React, { useEffect, useRef, useState } from 'react'
//components
import ListedItem from './ListedItem'
import ModalEditItem from '../../modals/groceries/ModalEditItem';
import ModalEditList from '../../modals/groceries/ModalEditList';
// Styling
import { Wrapper, WrapperList, WrapperListTitle, ListTitle, ListSubtitle, ButtonRemoveList, ButtonEditList, IconArrowDown, ListSizeWrapper } from './styles/list'
//redux
import { useSelector, useDispatch } from 'react-redux';
import { selectAllSortedItems, itemsRemovedByList } from '../../../redux/slices/groceryList/itemsSlice'
import { toggleOpen, listRemoved } from '../../../redux/slices/groceryList/listsSlice'
import { View } from 'react-native';
export default React.memo((props) => {
const items = useSelector(selectAllSortedItems)
const list = useSelector(state => state.lists.entities.find((list) => list._id === props.listId))
const [height, setHeight] = useState(0);
const ref = useRef(null);
useEffect(() => {
setHeight(ref.current.clientHeight);
}, [list.open, items]);
const dispatch = useDispatch()
const [modalItemVisible, setModalItemVisible] = useState(false);
const [modalListVisible, setModalListVisible] = useState(false);
const itemList = items.filter(item => item.listId === props.listId).map((item, index) => {
return (
<ListedItem key={index} item={item} setVisible={setModalItemVisible} />
)
})
const removeList = () => {
if (window.confirm("Do you really want to remove this list and the groceries within")) {
dispatch(itemsRemovedByList(props.listId))
dispatch(listRemoved(props.listId))
}
}
return (
<Wrapper >
<WrapperListTitle onClick={() => { dispatch(toggleOpen(props.listId)) }}>
<View id="left">
<ListTitle>{list.listName}</ListTitle>
<ListSubtitle>{list.listDescription && list.listDescription}</ListSubtitle>
</View>
<View id="right">
{list.open && <>
<ButtonEditList onClick={() => setModalListVisible(true)} />
<ButtonRemoveList onClick={removeList} />
</>}
<IconArrowDown visible={list.open.toString()} />
</View>
</WrapperListTitle>
<ListSizeWrapper height={height} visible={list.open} >
< WrapperList listLength={items.length} ref={ref}>
{list.open && itemList}
{
itemList.length === 0 && <ListSubtitle style={{ fontSize: 22 }} >Add a grocery</ListSubtitle>
}
{items.find(item => item.modalVisible) && <ModalEditItem visible={modalItemVisible} setVisible={setModalItemVisible} />}
</WrapperList >
<ModalEditList id={props.listId} visible={modalListVisible} setVisible={setModalListVisible} />
</ListSizeWrapper>
</Wrapper>
)
})

View File

@@ -0,0 +1,34 @@
import React from 'react'
//components
//styling
import { Wrapper,DarkLayer, WrapperItem, WrapperButton, TextProductName, TextDetails, TextAmount, TextPerson } from './styles/listedItem'
import { CheckButton } from '../../../styles/componentBlueprints';
//redux
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { selectItemById, checkToggle, modalToggle } from '../../../redux/slices/groceryList/itemsSlice'
import { findTag } from '../../../redux/slices/groceryList/tagsSlice'
const ListedItem = React.memo((props) => {
console.log("Rendering item")
const dispatch = useDispatch()
const item = useSelector(state => selectItemById(state, props.item._id), shallowEqual)
const tag = useSelector(state => findTag(state, item.tag))
return (
<Wrapper>
<DarkLayer>
<WrapperItem person={item.person} color={tag.color} checked={item.checked} onClick={() => { props.setVisible(true); return dispatch(modalToggle(item._id)) }}>
<TextProductName >{item.productName}</TextProductName>
<TextDetails>{item.details}</TextDetails>
<TextAmount>{item.amount.am}{item.amount.qt && " " + item.amount.qt}</TextAmount>
{item.person && <TextPerson>{item.person}</TextPerson>}
{/* {product.image && <Image src={product.image} />} */}
</WrapperItem>
</DarkLayer>
<WrapperButton onClick={() => dispatch(checkToggle(item._id))} >
<CheckButton checked = {item.checked} />
</WrapperButton>
</Wrapper>
)
})
export default ListedItem

View File

@@ -0,0 +1,123 @@
import styled, { css } from 'styled-components'
//dependencies
import { MdAddShoppingCart, MdDeleteForever} from 'react-icons/md'
import { FiMenu,FiCheckSquare } from 'react-icons/fi'
import { CgFileAdd } from 'react-icons/cg'
//blueprints
import { Button } from '../../../../styles/componentBlueprints'
export const WrapperButtons = styled(Button)`
position:absolute;
background-color: ${({ theme }) => theme.colors.primary};
bottom:50px;
left:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
bottom:70px;
left:10px;
}
`
export const WrapperAddItem = styled(Button)`
position:absolute;
background-color: ${({ theme }) => theme.colors.primary};
bottom:50px;
right:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
bottom:70px;
right:10px;
}
`
export const WrapperAddList = styled(Button)`
position:absolute;
background-color: ${({ theme }) => theme.colors.secondary};
bottom:50px;
right:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
bottom:70px;
right:10px;
}
`
export const WrapperRemove = styled(Button)`
position:absolute;
background-color: ${({ theme }) => theme.colors.error};
left:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
left:10px;
${(props) =>
props.visible
? css`
bottom:140px;
`
: css`
bottom:70px;
`}
}
${(props) =>
props.visible
? css`
bottom:140px;
`
: css`
bottom:50px;
`}
`
export const WrapperSelect = styled(Button)`
position:absolute;
background-color: ${({ theme }) => theme.colors.primary};
left:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
left:10px;
${(props) =>
props.visible
? css`
bottom:210px;
`
: css`
bottom:70px;
`};
}
${(props) =>
props.visible
? css`
bottom:230px;
`
: css`
bottom:50px;
`};
`
export const MenuIcon = styled(FiMenu)`
color: ${({theme})=> theme.colors.textB2};
font-size: 60px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 47px;
}
`
export const PlusIcon = styled(MdAddShoppingCart)`
color: ${({theme})=> theme.colors.textB2};
font-size: 60px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 45px;
}
`
export const RemoveIcon = styled(MdDeleteForever)`
color: ${({theme})=> theme.colors.textB2};
font-size: 60px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 45px;
}
`
export const CheckIcon = styled(FiCheckSquare)`
color: ${({theme})=> theme.colors.textB2};
font-size: 50px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 40px;
}
`
export const ListIcon = styled(CgFileAdd)`
color: ${({theme})=> theme.colors.textB2};
font-size: 50px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 40px;
}
`

View File

@@ -0,0 +1,100 @@
import { Text, View } from 'react-native'
import styled, { css } from 'styled-components'
import { MdClose } from 'react-icons/md'
import { BiEditAlt } from 'react-icons/bi'
import { IoChevronDown } from 'react-icons/io5'
export const WrapperGroceryList = styled(View)`
width:100%;
display: flex;
flex-direction: column;
align-items: center;
`
export const Wrapper = styled(View)`
border-bottom-color: ${({ theme }) => theme.colors.dp01};
border-bottom-width: 3px;
/* border-radius: 15px; */
margin-bottom: 10px;
width: 768px;
@media ${({ theme }) => theme.mediaQueries.below768}{
width: 100%;
}
`
export const ListSizeWrapper = styled(View)`
height: ${(props) => props.visible ? props.height + 'px' : css`0px`};
overflow: hidden;
`
export const WrapperList = styled(View)`
display: flex;
width: 100%;
/* height: ${props => props.height}; */
flex-direction: column;
align-items: center;
overflow: hidden;
`
export const WrapperListTitle = styled(View)`
display: flex;
width: 100%;
min-height: 60px;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 3px;
#left {
display: flex;
flex-direction: column;
width: 50%;
}
#right {
width: 50%;
display: flex;
align-items: flex-start;
justify-content: flex-end;
flex-direction: row;
}
`
export const ListTitle = styled(Text)`
overflow-wrap: break-word;
font-size: 20px;
`
export const ListSubtitle = styled(Text)`
overflow-wrap: break-word;
color: ${({ theme }) => theme.colors.textB5};
font-size: 17px;
`
export const ButtonRemoveList = styled(MdClose)`
color: ${({ theme }) => theme.colors.textW5};
height: 28px;
width: 28px;
margin: 4px 10px 4px 0px;
`
export const ButtonEditList = styled(BiEditAlt)`
color: ${({ theme }) => theme.colors.textW5};
height: 28px;
width: 28px;
margin: 4px 10px 4px 0px;
`
export const IconArrowDown = styled(IoChevronDown)`
transform: rotate(${props => props.visible === 'true' ? css`-180deg` : css`0deg`});
color: ${({ theme }) => theme.colors.textW5};
height: 28px;
width: 28px;
margin: 4px;
`
export const TextTotalAmount = styled(Text)`
position: absolute;
align-self: flex-end;
z-index: 2;
top:60px;
right:0px;
font-size: 20px;
color: ${({ theme }) => theme.colors.textW1};
background-color: ${({ theme }) => theme.colors.dp00 + '88'};
padding : 2px 20px;
margin: 20px 0px 10px 0px;
border-bottom-right-radius: 20px;
`

View File

@@ -0,0 +1,107 @@
import { Text, View, Image} from 'react-native'
import styled, { css } from 'styled-components'
export const Wrapper = styled(View)`
display: flex;
flex-direction: row;
align-items: flex-start;
width: 768px;
@media ${({ theme }) => theme.mediaQueries.below768}{
width: 100vw;
}
`
export const DarkLayer = styled(View)`
background-color: ${({ theme }) => theme.colors.dp00};
display: flex;
flex: 1;
border-radius: 15px;
margin-bottom:7px;
margin-left: 7px;
`
export const WrapperItem = styled(View)`
box-shadow: ${({ theme }) => theme.colors.shadow};
display: flex;
flex: 1;
min-height: ${props => props.person ? css`60px` : css`30px`};
position: relative;
flex-direction: column;
justify-content: center;
border-radius: 15px;
background-color: ${props => props.checked ? props.theme.colors.itemSelected : props.color + '66'};
&:hover{
background-color: ${props => props.checked ? props.theme.colors.itemSelected : props.color + '77'};
}
`
export const WrapperButton = styled(View)`
height: 30px;
width: 40px;
`
export const TextProductName = styled(Text)`
/* word-wrap: break-word; */
word-break: break-all;
margin: 2px 0px 0px 8px;
max-width: 70%;
font-size: 20px;
color: ${props => props.theme.colors.textW1};
`
export const TextDetails = styled(Text)`
word-wrap: break-word;
margin: 2px 0px 3px 8px;
max-width: 65%;
font-size: 16px;
color: ${({ theme }) => theme.colors.primary};
`
export const TextAmount = styled(Text)`
position: absolute;
max-width: 100px;
max-height: 30px;
word-wrap: break-word;
text-overflow: ellipsis;
overflow:hidden;
white-space:nowrap;
right: 0px;
margin: 3px 3px 3px 3px;
font-size: 16px;
border-radius: 12px;
border-top-right-radius: 15px;
padding:2px 8px 2px 6px;
background-color:${({ theme }) => theme.colors.dp00 + '88'};
color: ${({ theme }) => theme.colors.textW1};
`
export const TextPerson = styled(Text)`
position: absolute;
word-wrap: break-word;
max-width: 100px;
text-overflow: ellipsis;
overflow:hidden;
white-space:nowrap;
right: 0px;
bottom: 0px;
font-size: 16px;
border-top-left-radius: 12px;
border-bottom-right-radius: 15px;
padding:2px 8px 0px 6px;
background-color:${({ theme }) => theme.colors.dp00 + '88'};
color: ${({ theme }) => theme.colors.textW1};
`
export const StyledImage = styled(Image)`
position: absolute;
object-fit: cover;
opacity: 0.05;
height: 100%;
width: 200px;
border-top-left-radius: 30px;
border-top-right-radius: 15px;
border-bottom-left-radius: 15px;
border-bottom-right-radius: 30px;
right:0px;
`

View File

@@ -0,0 +1,33 @@
import React from 'react'
//components
//styling
import { Wrapper, WrapperProduct, WrapperButton, CheckButton, WrapperText, TextProductName, TextTag, TextPrice, StyledImage, IconCheck} from './styles/listedProduct'
//redux
import { useSelector, useDispatch, shallowEqual} from 'react-redux';
import { selectProductById, checkToggle, modalToggle} from '../../../redux/slices/groceryList/productsSlice'
import { findTag } from '../../../redux/slices/groceryList/tagsSlice'
const ListedProduct = React.memo((props) => {
const dispatch = useDispatch()
const product = useSelector(state => selectProductById(state, props._id), shallowEqual)
const tag = useSelector(state => findTag(state, product.tag))
return (
<Wrapper>
<WrapperProduct color={tag.color} checked={product.checked} onClick={()=>{props.setVisible(true); return dispatch(modalToggle(product._id))}}>
<WrapperText>
<TextProductName >{product.productName}</TextProductName>
<TextTag color={tag.color}>{tag.tagName}</TextTag>
{product.price !== 0&& <TextPrice> {product.price}</TextPrice>}
</WrapperText>
{product.image && <StyledImage src={product.image}/>}
</WrapperProduct>
<WrapperButton onClick={()=>dispatch(checkToggle(product._id))} >
<CheckButton checked={product.checked}>
<IconCheck checked={product.checked} />
</CheckButton>
</WrapperButton>
</Wrapper>
)
})
export default ListedProduct

View File

@@ -0,0 +1,88 @@
import React, { useState } from 'react'
//components
import { ModalAddProduct } from '../../modals/groceries/ModalAddProduct'
//styles
import { WrapperButtons, WrapperAddProduct, WrapperUpload, UploadInput, WrapperDownload, WrapperRemove, MenuIcon, PlusIcon, UploadIcon, DownloadIcon, RemoveIcon } from './styles/buttons'
//redux
import { useDispatch, useSelector } from 'react-redux'
import { productsRemoved, productsUploaded } from '../../../redux/slices/groceryList/productsSlice'
// var FileSaver = require('file-saver');
const UploadProductsButton = (props) => {
const dispatch = useDispatch()
const [toggleAnim, setToggleAnim] = useState(false)
const openFile = (evt) => {
setToggleAnim(!toggleAnim)
const fileObj = evt.target.files[0];
const reader = new FileReader();
reader.onload = () => { dispatch(productsUploaded(JSON.parse(reader.result)['products'])) }
reader.readAsText(fileObj);
}
return (
<WrapperUpload toggle={toggleAnim} visible={props.visible}>
<UploadInput type="file"
onChange={evt => openFile(evt)}
multiple={false} />
<UploadIcon />
</WrapperUpload>
)
}
const DownloadProductsButton = (props) => {
const products = useSelector(state => state.products.entities)
const [toggleAnim, setToggleAnim] = useState(false)
const handlePress = () => {
if (window.confirm("Do you want to download all of your products?")) {
setToggleAnim(!toggleAnim)
let productsJSON = JSON.stringify({ products: products }, null, 4)
let blob = new Blob([productsJSON], { type: "application/json" });
// FileSaver.saveAs(blob, "productList" + Date.now() + ".json");
}
}
return (
<WrapperDownload toggle={toggleAnim} visible={props.visible} onClick={handlePress}>
<DownloadIcon />
</WrapperDownload>
)
}
const RemoveProductsButton = (props) => {
const dispatch = useDispatch()
const [toggleAnim, setToggleAnim] = useState(false)
const handlePress = () => {
if (window.confirm("Do you really want to remove the selected products?")) {
dispatch(productsRemoved());
setToggleAnim(!toggleAnim)
}
}
return (
<WrapperRemove toggle={toggleAnim} visible={props.visible} onClick={handlePress}>
<RemoveIcon />
</WrapperRemove>
)
}
export const ContainerButtons = (props) => {
const [visible, setVisible] = useState(false);
return (
<>
<UploadProductsButton visible={visible} />
<DownloadProductsButton visible={visible} />
<RemoveProductsButton visible={visible} />
<WrapperButtons toggle={visible} onClick={() => setVisible(!visible)}>
<MenuIcon />
</WrapperButtons >
</>
)
}
export const AddProductButton = (props) => {
const [visible, setVisible] = useState(false);
const closeModal = () => setVisible(false)
return (
<>
<WrapperAddProduct toggle={visible} onClick={() => setVisible(true)}>
<PlusIcon />
</WrapperAddProduct>
<ModalAddProduct visible={visible} closeModal={closeModal} />
</>
)
}

View File

@@ -0,0 +1,43 @@
import React, { useState } from 'react'
//components
import ListedProduct from './ListedProduct'
//redux
import { useSelector } from 'react-redux';
import { selectAllSortedProducts } from '../../../redux/slices/groceryList/productsSlice'
//styles
import { WrapperList } from './styles/list'
import ModalEditProduct from '../../modals/groceries/ModalEditProduct';
import { View } from 'react-native';
const ProductsList = (props) => {
const products = useSelector(selectAllSortedProducts)
const [count, setCount] = useState(11)
const [hasMore, setHasMore] = useState(true);
const [visible, setVisible] = useState(false);
const productList = products.slice(0, count).map((product, index) => {
return (
<ListedProduct key={index} _id={product._id} setVisible={setVisible} />
)
})
// const [current, setCurrent] = useState())
const getMoreData = () => {
if (productList.length === products.length) {
setHasMore(false);
return;
}
setCount((prevState) => prevState + 10)
}
return (
<WrapperList>
{productList}
<View style={{ height: 150 }} />
{products.find(product => product.modalVisible) && <ModalEditProduct visible={visible} setVisible={setVisible} />}
</WrapperList>
)
}
export default ProductsList

View File

@@ -0,0 +1,106 @@
import styled from 'styled-components'
//dependencies
import { FiPlus, FiMenu , FiUploadCloud, FiDownloadCloud } from 'react-icons/fi'
import { MdDeleteForever } from 'react-icons/md'
//blueprints
import { Button } from '../../../../styles/componentBlueprints'
import { TextInput } from 'react-native'
export const WrapperButtons = styled(Button)`
position:absolute;
background-color: ${({ theme }) => theme.colors.primary};
bottom:50px;
left:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
bottom:70px;
left:10px;
}
`
export const WrapperAddProduct = styled(Button)`
position:absolute;
background-color: ${({ theme }) => theme.colors.primary};
bottom:50px;
right:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
bottom:70px;
right:10px;
}
`
export const WrapperRemove = styled(Button)`
position:absolute;
background-color: ${({ theme }) => theme.colors.error};
left:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
left:10px;
bottom: ${(props) => props.visible ? '140px' : '70px'};
}
bottom: ${(props) => props.visible ? '140px' : '50px'};
`
export const WrapperUpload = styled(Button)`
position:absolute;
background-color: #ddb5e4;
left:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
left:10px;
bottom: ${(props) => props.visible ? '280px' : '70px'};
}
bottom: ${(props) => props.visible ? '320px' : '50px'};
`
export const UploadInput = styled(TextInput)`
position: absolute;
opacity: 0;
border-radius: 50px;
width: 80px;
height:80px;
`
export const WrapperDownload = styled(Button)`
position:absolute;
background-color: #fff7ad;
left:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
left:10px;
bottom: ${(props) => props.visible ? '210px' : '70px'};
}
bottom: ${(props) => props.visible ? '230px' : '50px'};
`
export const MenuIcon = styled(FiMenu)`
color: ${({theme})=> theme.colors.textB2};
font-size: 60px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 50px;
}
`
export const PlusIcon = styled(FiPlus)`
color: ${({theme})=> theme.colors.textB2};
font-size: 60px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 60px;
}
`
export const UploadIcon = styled(FiUploadCloud)`
font-size: 50px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 40px;
}
`
export const DownloadIcon = styled(FiDownloadCloud)`
font-size: 50px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 40px;
}
`
export const RemoveIcon = styled(MdDeleteForever)`
color: ${({theme})=> theme.colors.textB2};
font-size: 60px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 45px;
}
`

View File

@@ -0,0 +1,9 @@
import { View } from 'react-native'
import styled from 'styled-components'
export const WrapperList = styled(View)`
width:100%;
display: flex;
flex-direction: column;
align-items: center;
`

View File

@@ -0,0 +1,101 @@
import { View, Text, Image } from 'react-native'
import styled, { css } from 'styled-components'
//dependencies
import { HiCheck } from 'react-icons/hi'
//functions
import LightenDarken from '../../../../functions'
export const Wrapper = styled(View)`
display: flex;
flex-direction: row;
width: 768px;
align-items: flex-start;
@media ${({ theme }) => theme.mediaQueries.below768}{
width: 100vw;
}
`
export const WrapperProduct = styled(View)`
box-shadow: ${({theme})=> theme.colors.shadow};
display: flex;
flex: 1;
position: relative;
flex-direction: row;
justify-content: center;
border-radius: 15px;
margin-bottom:7px;
margin-left: 7px;
background-color: ${props => props.checked ? props.theme.colors.itemSelected : props.color + '66'};
&:hover{
background-color: ${props => props.checked ? props.theme.colors.itemSelected : props.color + '77'};
}
`
export const WrapperButton = styled(View)`
height: 60px;
width: 40px;
`
export const CheckButton = styled(View)`
display:flex;
justify-content: center;
align-items: center;
height: 30px;
min-width: 30px;
border: 1px solid ${props => props.checked ? props.theme.colors.selected : props.theme.colors.dp04};
border-radius: 15px;
margin: 0px 5px ;
background-color: ${props => props.checked ? props.theme.colors.selected : props.theme.colors.buttonGrey};
&:hover{
background-color: ${props => props.checked ? props.theme.colors.selected : LightenDarken(props.theme.colors.buttonGrey, 5)}
}
`
export const WrapperText = styled(View)`
flex:1;
max-width:600px;
@media ${({ theme }) => theme.mediaQueries.below768}{
max-width: 60vw;
}
`
export const TextProductName = styled(Text)`
word-wrap: break-word;
max-width: 70%;
margin: 0px 0px 0px 5px;
font-size: 16px;
color: ${props => props.theme.colors.textW1};
`
export const TextTag = styled(Text)`
word-wrap: break-word;
margin: 2px 0px 0px 5px;
font-size: 16px;
color: ${props => LightenDarken(props.color,-30)};
`
export const TextPrice = styled(Text)`
word-wrap: break-word;
margin: 0px 6px 3px 3px;
font-size: 16px;
border-radius: 12px;
border-bottom-left-radius: 15px;
padding:2px;
background-color:${({ theme }) => theme.colors.dp00 + '88'};
color: ${({ theme }) => theme.colors.textW1};
`
export const StyledImage = styled(Image)`
position: absolute;
top:0;
right:0;
object-fit: cover;
width: 100px;
height: 100%;
max-height:120px;
border-radius: 15px;
`
export const IconCheck = styled(HiCheck)`
font-size: 16px;
color: ${({ theme }) => theme.colors.dp00};
`

View File

@@ -0,0 +1,88 @@
import { View } from 'react-native'
import styled, { css } from 'styled-components'
import { RiShoppingBasketLine } from 'react-icons/ri'
import { FaAppleAlt } from 'react-icons/fa'
const iconSize = '2.0px'
const iconSizeSelected = '2.5px'
const WrapperIcon = styled(View)`
display: flex;
align-items: center;
justify-content: center;
color: ${props => props.theme.colors.textW5};;
`
export const Wrapper = styled(View)`
position: absolute;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
/* background-color: #1A2C31cc; */
background-color: ${({ theme }) => theme.colors.dp00 +'88'};
width: 250px;
height: 60px;
bottom: 0px;
padding-bottom: 10px;
left: 50%;
border-top-left-radius: 25px;
border-top-right-radius: 25px;
margin-left: -125px;
`
export const WrapperSelected = styled(View)`
position: absolute;
z-index: -1;
background-color: black;
opacity: 0.7;
width: 125px;
height: 50px;
border-radius:25px;
//anims
${(props) =>
props.currenttab === 'Groceries'
? css`
right: 125px;
background-color: #59D8E6;
`
: css`
right: 0px;
background-color: ${({ theme }) => theme.colors.primary};
`}
`
//groceries
export const WrapperGroceries = styled(WrapperIcon)`
flex:1;
height: 65px;
/* &:hover{
background-color: #fffa;
} */
`
export const IconGroceries = styled(RiShoppingBasketLine)`
font-size: ${iconSize};
${props => props.currenttab === 'Groceries' &&
css`
color: ${props => props.theme.colors.textW0};
font-size: ${iconSizeSelected};`
};
`
//products
export const WrapperProducts = styled(WrapperIcon)`
flex:1;
height: 65px;
/* &:hover{
background-color: #fff4;
} */
`
export const IconProducts = styled(FaAppleAlt)`
font-size: 16px;
margin-top:-5px;
${props => props.currenttab === 'Products' &&
css`
color: ${props => props.theme.colors.textW0};
font-size: 16px;`
};
`

11
src/components/Header.js Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import { View } from 'react-native';
import { useHeaderHeight } from '@react-navigation/elements';
const HeaderPadding = (props) => {
const headerHeight = useHeaderHeight();
return (
<View style={{ height: headerHeight + 10 }} />
)
}
export default HeaderPadding;

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { TouchableOpacity, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
//dependencies
//styles
import { WrapperCard, TextCard, IconCoffee } from './styles/groceryCard'
const GroceryCard = (props) => {
const navigation = useNavigation();
return (
<WrapperCard onPress={() => navigation.navigate('Groceries')}>
<TextCard>Groceries</TextCard>
<View style={{ height: 100, width: '100%' }} />
</WrapperCard>
)
}
export default GroceryCard

View File

@@ -0,0 +1,19 @@
import React from 'react';
//dependencies
//styles
import {WrapperCard, TextCard, IconRecipe} from './styles/groceryCard'
const RecipeCard = () => {
return (
// <Link to="/recipes">
<WrapperCard>
<TextCard>Recipes</TextCard>
{/* <IconRecipe/> */}
</WrapperCard>
// </Link>
)
}
export default RecipeCard

View File

@@ -0,0 +1,42 @@
import { Text, TouchableOpacity, View } from 'react-native'
import styled from 'styled-components'
import { FiCoffee } from 'react-icons/fi'
import { GiHotMeal } from 'react-icons/gi'
export const WrapperLink = styled(View)`
position: absolute;
width: 100px;
height: 100px;
`
export const WrapperCard = styled(TouchableOpacity)`
display: flex;
flex-direction: column;
height: 150px;
border-radius: 20px;
align-items: center;
background-color: ${props => props.theme.colors.dp02};
`
export const TextCard = styled(Text)`
text-decoration:none;
font-size: 32px;
align-self: center;
display: flex;
justify-content: center;
align-items: center;
height: 50px;
border-top-right-radius: 20px;
border-top-left-radius: 20px;
width:100%;
color: ${props => props.theme.colors.textW2};
background-color: #0003;
`
export const IconCoffee = styled(FiCoffee)`
margin-top: 15px;
color: ${({ theme }) => theme.colors.primaryVar};
font-size: 50px;
`
export const IconRecipe = styled(GiHotMeal)`
margin-top: 15px;
color: ${({ theme }) => theme.colors.primaryVar};
font-size: 50px;
`

View File

@@ -0,0 +1,141 @@
import React, { useState } from 'react'
//components
import Dropdown, { QtDropdown } from '../../Dropdown'
//styles
import {
StyledModal, ModalHeader, WrapperInput, WrapperDropdown, WrapperButtons, VerticalSeperator, Input, Button, IconWrong, IconCheck, InputSmall
} from '../styles/modal'
import { IconProduct, IconAmount, IconPerson, IconDetails } from './styles/addItem'
import { IconTag, IconDollar } from './styles/addProduct'
//redux
import { useDispatch, useSelector } from 'react-redux';
import { itemAdded } from '../../../redux/slices/groceryList/itemsSlice';
const ModalAddItem = (props) => {
const dispatch = useDispatch()
const products = useSelector(state => state.products.entities)
const tags = useSelector(state => state.tags.entities)
const lists = useSelector(state => state.lists.entities)
const [productName, setProductName] = useState('');
const [amount, setAmount] = useState('');
const [qt, setQt] = useState('')
const [person, setPerson] = useState('');
const [details, setDetails] = useState('');
const [tag, setTag] = useState('');
const [price, setPrice] = useState('');
const [focusedProduct, setFocusedProduct] = React.useState(false)
const [focusedQt, setFocusedQt] = React.useState(false)
const [focusedTag, setFocusedTag] = React.useState(false)
const handleSubmit = () => {
if (props.visible && productName !== '') {
let amountObj = {
am: amount ? Number(amount) : 1,
qt: qt
}
console.log(amountObj)
let listId = lists.find((list) => list.open === true)._id
if (tags.find(t => t.tagName === tag)) {
dispatch(itemAdded({ productName, amount: amountObj, person, details, tag, price, listId }));
}
else {
dispatch(itemAdded({ productName, amount: amountObj, person, details, tag: 'uncategorized', price, listId }));
}
setProductName('');
setAmount('');
setQt('');
setPerson('');
setDetails('');
setTag('');
setPrice('');
props.closeModal();
}
else {
alert('Fill in a valid product name');
}
}
const handleDropdownPress = (pName) => {
let product = products.find((product) => product.productName === pName);
setProductName(product.productName)
setTag(product.tag)
setPrice(product.price)
}
return (
<StyledModal show={props.visible} centered={true} onHide={props.closeModal} animation={false}>
<ModalHeader>Add product</ModalHeader>
{/* {product && product.image && <Image src={product.image} />} */}
<WrapperInput>
<IconProduct />
<Input onFocus={() => setFocusedProduct(true)} onBlur={() => { setTimeout(() => { setFocusedProduct(false) }, 100) }}
type="text"
value={productName}
onChange={(text) => setProductName(text.target.value)}
placeholder="Product name" />
{productName ? <IconCheck /> : <IconWrong />}
</WrapperInput>
{focusedProduct && <WrapperDropdown>
<Dropdown array={products.map(product => product.productName)} text={productName} setElement={handleDropdownPress} />
</WrapperDropdown>}
<WrapperInput>
<IconAmount />
<Input style={{ width: '69%' }}
type="number"
value={amount}
onChange={(text) => setAmount(text.target.value)}
placeholder="Amount" />
{amount ? <IconCheck /> : <IconWrong />}
<InputSmall style={{ marginLeft: "1%", width: '10%' }}
onFocus={() => setFocusedQt(true)} onBlur={() => { setTimeout(() => { setFocusedQt(false) }, 100) }}
type="text"
value={qt}
onChange={(text) => setQt(text.target.value)}
placeholder="Qt." />
</WrapperInput>
{focusedQt && <WrapperDropdown style={{ marginLeft: "60%"}}>
<QtDropdown text={qt} setElement={setQt} />
</WrapperDropdown>}
<WrapperInput>
<IconPerson />
<Input type="text"
value={person}
onChange={(text) => setPerson(text.target.value)}
placeholder="For" />
</WrapperInput>
<WrapperInput>
<IconDetails />
<Input type="text"
value={details}
onChange={(text) => setDetails(text.target.value)}
placeholder="Additional details" />
</WrapperInput>
<WrapperInput>
<IconTag />
<Input type="text"
onFocus={() => setFocusedTag(true)} onBlur={() => { setTimeout(() => { setFocusedTag(false) }, 100) }}
value={tag}
onChange={(text) => setTag(text.target.value)}
placeholder="Tag" />
</WrapperInput>
{focusedTag && <WrapperDropdown>
<Dropdown array={tags.map(tag => tag.tagName)} text={tag} setElement={setTag} />
</WrapperDropdown>}
<WrapperInput>
<IconDollar />
<Input type="text"
value={price}
onChange={(text) => setPrice(text.target.value)}
placeholder="Price" />
</WrapperInput>
<WrapperButtons>
<Button onClick={props.closeModal}>Close</Button>
<VerticalSeperator />
<Button onClick={() => handleSubmit()}>Add Item</Button>
</WrapperButtons>
</StyledModal >
)
}
export default ModalAddItem

View File

@@ -0,0 +1,54 @@
import React, { useState } from 'react'
//styles
import {
StyledModal, ModalHeader, WrapperInput, WrapperButtons, VerticalSeperator, Input, Button, IconWrong, IconCheck
} from '../styles/modal'
//redux
import { useDispatch } from 'react-redux';
import { listAdded } from '../../../redux/slices/groceryList/listsSlice';
const ModalAddList = (props) => {
const dispatch = useDispatch()
const [listName, setListName] = useState('');
const [description, setDescription] = useState('');
const handleSubmit = () => {
if (props.visible && listName !== '') {
dispatch(listAdded({ listName, listDescription: description }));
setListName('');
setDescription('');
props.closeModal();
}
else {
alert('Give a valid name for your grocery list!');
}
}
return (
<StyledModal show={props.visible} centered={true} onHide={props.closeModal} animation={false}>
<ModalHeader>Add a grocery list</ModalHeader>
<WrapperInput>
<Input
type="text"
value={listName}
onChange={(text) => setListName(text.target.value)}
// onKeyDown={(event) => handleKeyDown(event)}
placeholder="List name" />
{listName ? <IconCheck /> : <IconWrong />}
</WrapperInput>
<WrapperInput>
<Input type="text"
value={description}
onChange={(text) => setDescription(text.target.value)}
placeholder="List description" />
</WrapperInput>
<WrapperButtons>
<Button onClick={props.closeModal}>Close</Button>
<VerticalSeperator />
<Button onClick={() => handleSubmit()}>Add List</Button>
</WrapperButtons>
</StyledModal >
)
}
export default ModalAddList

View File

@@ -0,0 +1,90 @@
import React, { useState } from 'react'
//components
import Dropdown from '../../Dropdown'
//styles
import {
StyledModal, ModalHeader, WrapperInput, WrapperDropdown, WrapperButtons, VerticalSeperator, InputSmall, Button, IconWrong, IconCheck
} from '../styles/modal'
import {
StyledImage, IconProduct, IconTag, IconDollar, IconLink
} from './styles/addProduct'
//redux
import { useDispatch, useSelector } from 'react-redux';
import { productAdded } from '../../../redux/slices/groceryList/productsSlice';
export const ModalAddProduct = (props) => {
const dispatch = useDispatch()
const tags = useSelector(state => state.tags.entities)
const [productName, setProductName] = useState('');
const [tag, setTag] = useState('');
const [price, setPrice] = useState('');
const [image, setImage] = useState('');
const [focused, setFocused] = React.useState(false)
// const handleKeyDown = (event) => {
// if (event.key === 'Enter' && productName !== '') {
// setProductName('')
// dispatch(addNewProduct({ productName, tag: 'Vegetables & fruit', price: 5, image: 'https://media-cdn.tripadvisor.com/media/photo-s/15/2d/23/07/domino-s-pizza.jpg' }))
// }
// }
const handleSubmit = () => {
if (props.visible && productName) {
dispatch(productAdded({ productName, tag: tags.find(t => t.tagName === tag) ? tag : "uncategorized", price, image }));
setProductName('');
setTag('');
setPrice('');
setImage('');
props.closeModal();
}
else {
alert('You should give a valid product name');
}
}
return (
<StyledModal show={props.visible} centered={true} onHide={props.closeModal} animation={false}>
<ModalHeader>Add product</ModalHeader>
<WrapperInput>
<IconProduct />
<InputSmall type="text"
value={productName}
onChange={(text) => setProductName(text.target.value)}
// onKeyDown={(event) => handleKeyDown(event)}
placeholder="Product name" />
{productName ? <IconCheck /> : <IconWrong />}
</WrapperInput>
<WrapperInput>
<IconTag />
<InputSmall onFocus={() => setFocused(true)} onBlur={() => { setTimeout(() => { setFocused(false) }, 100) }}
type="text"
value={tag}
onChange={(text) => setTag(text.target.value)}
placeholder="Tag" />
{tag ? <IconCheck /> : <IconWrong />}
</WrapperInput>
{focused && <WrapperDropdown>
<Dropdown array={tags.map(tag => tag.tagName)} text={tag} setElement={setTag} />
</WrapperDropdown>}
<WrapperInput>
<IconDollar />
<InputSmall type="number"
value={price}
onChange={(text) => setPrice(text.target.value)}
placeholder="Price" />
</WrapperInput>
<WrapperInput>
<IconLink />
<InputSmall type="text"
value={image}
onChange={(text) => setImage(text.target.value)}
placeholder="Image link" />
</WrapperInput>
{image && <StyledImage src={image} alt={'No link...'} />}
<WrapperButtons>
<Button onClick={props.closeModal}>Close</Button>
<VerticalSeperator />
<Button onClick={() => handleSubmit()}>Add </Button>
</WrapperButtons>
</StyledModal>
)
}

View File

@@ -0,0 +1,141 @@
import React, { useState } from 'react'
//components
import Dropdown, { QtDropdown } from '../../Dropdown'
//styles
import {
StyledModal, WrapperInput, WrapperDropdown, WrapperButtons, Input, Button, IconWrong, IconCheck, InputSmall
} from '../styles/modal'
import { IconProduct, IconAmount, IconPerson, IconDetails } from './styles/addItem'
import { WrapperProduct, WrapperProductInfo, TextPrice, TextTag, StyledImage } from './styles/edit'
import { IconTag, IconDollar } from './styles/addProduct'
//redux
import { useDispatch, useSelector } from 'react-redux';
import { itemAdded, itemRemoved, modalToggle, selectItemModalVisible } from '../../../redux/slices/groceryList/itemsSlice';
const ModalEditItem = (props) => {
const dispatch = useDispatch()
const item = useSelector(selectItemModalVisible)
const products = useSelector(state => state.products.entities)
const tags = useSelector(state => state.tags.entities)
const [productName, setProductName] = useState(item.productName);
const [amount, setAmount] = useState(item.amount.am);
const [qt, setQt] = useState(item.amount.qt)
const [person, setPerson] = useState(item.person);
const [details, setDetails] = useState(item.details);
const [tag, setTag] = useState(item.tag);
const [price, setPrice] = useState(item.price);
const [focusedProduct, setFocusedProduct] = React.useState(false)
const [focusedQt, setFocusedQt] = React.useState(false)
const [focusedTag, setFocusedTag] = React.useState(false)
let product = products.find((product) => product.productName === productName);
let tagState = tags.find(t=>t.tagName===tag)
tagState = tagState ? tagState : tags[0]
const handleSubmit = async () => {
let amountObj = {
am: amount ? Number(amount) : 1,
qt: qt
}
if (props.visible && productName !== '') {
await dispatch(itemRemoved(item._id))
await dispatch(itemAdded({ productName, amount: amountObj, person, details, tag: tagState.tagName, price, listId: item.listId }));
props.setVisible(false);
}
else {
alert('Fill in a valid product name');
}
}
const handleDropdownPress = (pName) => {
let product = products.find((product) => product.productName === pName);
setProductName(product.productName)
setTag(product.tag)
setPrice(product.price)
}
const closeModal = () => {
dispatch(modalToggle(item._id))
props.setVisible(false)
}
return (
<StyledModal show={props.visible} centered={true} onHide={closeModal} animation={false}>
<WrapperProduct color={tagState.color}>
{product && product.image && <StyledImage src={product.image} />}
<WrapperProductInfo>
<TextPrice> {price}</TextPrice>
<TextTag color={tagState.color}>{tagState.tagName}</TextTag>
</WrapperProductInfo>
</WrapperProduct>
<WrapperInput>
<IconProduct />
<Input onFocus={() => setFocusedProduct(true)} onBlur={() => { setTimeout(() => { setFocusedProduct(false) }, 100) }}
type="text"
value={productName}
onChange={(text) => setProductName(text.target.value)}
// onKeyDown={(event) => handleKeyDown(event)}
placeholder="Product name" />
{productName ? <IconCheck /> : <IconWrong />}
</WrapperInput>
{focusedProduct && <WrapperDropdown>
<Dropdown array={products.map(product => product.productName)} text={productName} setElement={handleDropdownPress} />
</WrapperDropdown>}
<WrapperInput>
<IconAmount />
<Input style={{ width: '67%' }}
type="number"
value={amount}
onChange={(text) => setAmount(text.target.value)}
placeholder="Amount" />
{amount ? <IconCheck /> : <IconWrong />}
<InputSmall style={{ marginLeft: "1%", width: '10%' }}
onFocus={() => setFocusedQt(true)} onBlur={() => { setTimeout(() => { setFocusedQt(false) }, 100) }}
type="text"
value={qt}
onChange={(text) => setQt(text.target.value)}
placeholder="Qt." />
</WrapperInput>
{focusedQt && <WrapperDropdown style={{ marginLeft: "60%"}}>
<QtDropdown text={qt} setElement={setQt} />
</WrapperDropdown>}
<WrapperInput>
<IconPerson />
<Input type="text"
value={person}
onChange={(text) => setPerson(text.target.value)}
placeholder="For" />
</WrapperInput>
<WrapperInput>
<IconDetails />
<Input type="text"
value={details}
onChange={(text) => setDetails(text.target.value)}
placeholder="Additional details" />
</WrapperInput>
<WrapperInput>
<IconTag />
<Input type="text"
onFocus={() => setFocusedTag(true)} onBlur={() => { setTimeout(() => { setFocusedTag(false) }, 100) }}
value={tag}
onChange={(text) => setTag(text.target.value)}
placeholder="Tag" />
</WrapperInput>
{focusedTag && <WrapperDropdown>
<Dropdown array={tags.map(t => t.tagName)} text={tag} setElement={setTag} />
</WrapperDropdown>}
<WrapperInput>
<IconDollar />
<Input type="text"
value={price}
onChange={(text) => setPrice(text.target.value)}
placeholder="Price" />
</WrapperInput>
<WrapperButtons>
<Button onClick={() => handleSubmit()}>Save</Button>
</WrapperButtons>
</StyledModal>
)
}
export default ModalEditItem

View File

@@ -0,0 +1,50 @@
import React, { useState } from 'react'
//styles
import {
StyledModal, ModalHeader, WrapperInput, WrapperButtons, VerticalSeperator, Input, Button, IconWrong, IconCheck
} from '../styles/modal'
//redux
import { useDispatch, useSelector } from 'react-redux';
import { listEdited } from '../../../redux/slices/groceryList/listsSlice';
const ModalEditList = (props) => {
const dispatch = useDispatch()
const list = useSelector(state => state.lists.entities.find(list=>list._id === props.id))
const [listName, setListName] = useState(list.listName);
const [description, setDescription] = useState(list.listDescription);
const handleSubmit = () => {
if (props.visible && listName !== '') {
dispatch(listEdited({listName, _id: props.id, listDescription: description }));
props.setVisible(false)
}
else {
alert('You need to give a list name');
}
}
return (
<StyledModal show={props.visible} centered={true} onHide={() => props.setVisible(false)} animation={false}>
<ModalHeader>Edit the list</ModalHeader>
<WrapperInput>
<Input
type="text"
value={listName}
onChange={(text) => setListName(text.target.value)}
placeholder="List name" />
{listName ? <IconCheck /> : <IconWrong />}
</WrapperInput>
<WrapperInput>
<Input type="text"
value={description}
onChange={(text) => setDescription(text.target.value)}
placeholder="List description" />
</WrapperInput>
<WrapperButtons>
<Button onClick={() => props.setVisible(false)}>Close</Button>
<VerticalSeperator />
<Button onClick={() => handleSubmit()}>Edit List</Button>
</WrapperButtons>
</StyledModal >
)
}
export default ModalEditList

View File

@@ -0,0 +1,98 @@
import React, { useState } from 'react'
//components
import Dropdown from '../../Dropdown'
//styles
import {
StyledModal, WrapperInput, WrapperDropdown, WrapperButtons, Input, Button, IconWrong, IconCheck
} from '../styles/modal'
import {
IconProduct, IconTag, IconDollar, IconLink
} from './styles/addProduct'
import { WrapperProduct, WrapperProductInfo, TextPrice, TextTag, StyledImage } from './styles/edit'
//redux
import { useDispatch, useSelector } from 'react-redux';
import { modalToggle, productAdded, productRemoved } from '../../../redux/slices/groceryList/productsSlice';
import { selectProductModalVisible } from '../../../redux/slices/groceryList/productsSlice'
const ModalEditProduct = (props) => {
const dispatch = useDispatch()
const product = useSelector(selectProductModalVisible)
const tags = useSelector(state => state.tags.entities)
const [productName, setProductName] = useState(product.productName);
const [tag, setTag] = useState(product.tag);
const [price, setPrice] = useState(product.price);
const [image, setImage] = useState(product.image);
const [focused, setFocused] = React.useState(false)
let usedTag = tags.find(t => t.tagName === tag)
if (!usedTag) {
usedTag = tags[0]
}
const handleSubmit = () => {
if (props.visible && productName) {
dispatch(productRemoved(product._id))
dispatch(productAdded({ productName, tag: tags.find(t=>t.tagName===tag) ? tag : "uncategorized", price, image}))
props.setVisible(false);
}
else {
alert('You should give product name');
}
}
const closeModal = () => {
dispatch(modalToggle(product._id))
props.setVisible(false)
}
return (
<StyledModal show={props.visible} centered={true} onHide={closeModal} animation={false}>
<WrapperProduct color={usedTag.color}>
{image && <StyledImage src={image} alt="No image found" />}
<WrapperProductInfo>
<TextPrice> {price}</TextPrice>
<TextTag color={usedTag.color}>{usedTag.tagName}</TextTag>
</WrapperProductInfo>
</WrapperProduct>
<WrapperInput>
<IconProduct />
<Input type="text"
value={productName}
onChange={(text) => setProductName(text.target.value)}
// onKeyDown={(event) => handleKeyDown(event)}
placeholder="Product name" />
{productName ? <IconCheck /> : <IconWrong />}
</WrapperInput>
<WrapperInput>
<IconTag />
<Input onFocus={() => setFocused(true)} onBlur={() => { setTimeout(() => { setFocused(false) }, 100) }}
type="text"
value={tag}
onChange={(text) => setTag(text.target.value)}
placeholder="Tag" />
{tag ? <IconCheck /> : <IconWrong />}
</WrapperInput>
{focused && <WrapperDropdown>
<Dropdown array={tags.map(tag => tag.tagName)} text={tag} setElement={setTag} />
</WrapperDropdown>}
<WrapperInput>
<IconDollar />
<Input type="number"
value={price}
onChange={(text) => setPrice(text.target.value)}
placeholder="Price" />
</WrapperInput>
<WrapperInput>
<IconLink />
<Input type="text"
value={image}
onChange={(text) => setImage(text.target.value)}
placeholder="Image link" />
</WrapperInput>
<WrapperButtons>
<Button onClick={handleSubmit}>Save</Button>
</WrapperButtons>
</StyledModal>
)
}
export default ModalEditProduct

View File

@@ -0,0 +1,37 @@
import styled from 'styled-components'
import { IoFastFood } from 'react-icons/io5'
import {AiOutlineFieldNumber} from 'react-icons/ai'
import {IoPersonCircleSharp} from 'react-icons/io5'
import {BiCommentAdd} from 'react-icons/bi'
import { Image } from 'react-native'
export const StyledImage = styled(Image)`
position: absolute;
object-fit: cover;
z-index:0;
opacity: 0.1;
width: 70%;
height: 100%;
right:0;
border-top-left-radius: 100px;
border-top-right-radius: 15px;
border-bottom-left-radius: 15px;
border-bottom-right-radius: 100px;
`
const iconSize = '32px'
export const IconProduct = styled(IoFastFood)`
font-size: ${iconSize};
color: ${({theme})=>theme.colors.iconGrey};
`
export const IconAmount = styled(AiOutlineFieldNumber)`
font-size: ${iconSize};
color: ${({theme})=>theme.colors.iconGrey};
`
export const IconPerson = styled(IoPersonCircleSharp)`
font-size: ${iconSize};
color: ${({theme})=>theme.colors.iconGrey};
`
export const IconDetails = styled(BiCommentAdd)`
font-size: ${iconSize};
color: ${({theme})=>theme.colors.iconGrey};
`

View File

@@ -0,0 +1,32 @@
import styled from 'styled-components'
import { IoFastFood } from 'react-icons/io5'
import { BsTag } from 'react-icons/bs'
import { BiDollar ,BiLinkAlt} from 'react-icons/bi'
import { Image } from 'react-native'
export const StyledImage = styled(Image)`
position: absolute;
object-fit: cover;
width: 35%;
height: 50%;
right:0;
border-radius: 15px;
`
const iconSize = '32px'
export const IconProduct = styled(IoFastFood)`
font-size: ${iconSize};
color: ${({theme})=>theme.colors.iconGrey};
`
export const IconTag = styled(BsTag)`
font-size: ${iconSize};
color: ${({theme})=>theme.colors.iconGrey};
`
export const IconDollar = styled(BiDollar)`
font-size: ${iconSize};
color: ${({theme})=>theme.colors.iconGrey};
`
export const IconLink = styled(BiLinkAlt)`
font-size: ${iconSize};
color: ${({theme})=>theme.colors.iconGrey};
`

View File

@@ -0,0 +1,42 @@
import { Image, Text, View } from 'react-native'
import styled from 'styled-components'
import LightenDarkenColor from '../../../../functions'
export const WrapperProduct = styled(View)`
display: flex;
align-items: center;
background-color: ${(props) => props.color + '22'};
width: 100%;
height:100px;
border-radius: 15px;
margin-bottom: 5px;
`
export const WrapperProductInfo = styled(View)`
flex:1;
`
export const TextPrice = styled(Text)`
background-color: #0004;
color: ${({ theme }) => theme.colors.textW2};
font-size: 16px;
width: 100%;
height:30px;
padding-left: 5px;
`
export const TextTag = styled(Text)`
color: ${(props) => LightenDarkenColor(props.color,-30)};
font-size: 16px;
margin: 0px 5px;
`
export const StyledImage = styled(Image)`
object-fit: cover;
width: 150px;
height: 150px;
margin-top:-50px;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
`

View File

@@ -0,0 +1,104 @@
import React, { useState } from 'react'
import styled from "styled-components"
import { CgFile } from "react-icons/cg"
import { GiMeal } from "react-icons/gi"
import { useDispatch, useSelector } from 'react-redux'
import { findRecipeById } from '../../../redux/slices/recipesSlice'
//styles
import {
StyledModal, ModalHeader, WrapperButtons, VerticalSeperator, Button, Input, WrapperDropdown, ModalDescription, WrapperInput, IconCheck, IconWrong
} from '../styles/modal'
import Ingredient from '../../recipes/Ingredient&Button'
import { itemAdded } from '../../../redux/slices/groceryList/itemsSlice'
import Dropdown from '../../Dropdown'
const IconList = styled(CgFile)`
font-size: 16px;
color: ${({ theme }) => theme.colors.primaryVar};
`
const IconMeal = styled(GiMeal)`
font-size: 16px;
color: ${({ theme }) => theme.colors.primaryVar};
`
const ModalAddIngredients = (props) => {
const dispatch = useDispatch()
const recipe = useSelector(state => findRecipeById(state, props.id))
let amountOfServings = recipe.servings ? recipe.servings : 1
const lists = useSelector(state => state.lists.entities)
const [listName, setListName] = useState('')
const [focused, setFocused] = useState(false)
const [servings, setServings] = useState(amountOfServings)
let servingsMultiplier = Math.ceil(servings / amountOfServings)
const IngredientsList = recipe.ingredients.map((ingredient, index) => {
return (
<Ingredient key={index} recipeId={recipe._id} multiplier={servingsMultiplier} ingredient={ingredient} />
)
})
const handleSubmit = () => {
if (lists.find(list => list.listName === listName) && recipe.ingredients.find(ingredient => ingredient.checked)) {
console.log('were sending the ingredients baby')
recipe.ingredients.forEach(ingredient => ingredient.checked === true && dispatch(itemAdded({
productName: ingredient.productName,
amount: {
am: ingredient.amount * servingsMultiplier,
qt: ingredient.portion,
},
tag: ingredient.tag,
price: ingredient.price,
person: '',
details: `For ${recipe.name} (${ingredient.portion}) `,
listId: lists.find(list => list.listName === listName)._id
})))
props.closeModal()
}
else {
alert('You should fill in a valid grocery list and/or select at least 1 ingredient')
}
}
return (
<StyledModal show={props.visible} centered={true} onHide={props.closeModal} animation={false}>
<ModalHeader>Add to grocerylist</ModalHeader>
<ModalDescription>Choose a grocery list and select the ingredients you would like to add to it</ModalDescription>
<WrapperInput>
<IconList />
<Input
style={{ fontSize: 20 }}
type="text"
value={listName}
onChange={(text) => setListName(text.target.value)}
onFocus={() => setFocused(true)} onBlur={() => { setTimeout(() => { setFocused(false) }, 100) }}
placeholder="Grocery list" />
{listName ? <IconCheck /> : <IconWrong />}
</WrapperInput>
{focused && <WrapperDropdown>
<Dropdown array={lists.map(list => list.listName)} text={listName} setElement={setListName} />
</WrapperDropdown>
}
<WrapperInput>
<IconMeal />
<Input
style={{ fontSize: 20, width: 100 }}
type="number"
value={servings}
onChange={(text) => setServings(Number(text.target.value))}
min={amountOfServings}
step={amountOfServings}
placeholder="Multiplier" />
<p style={{ marginLeft: 5 }}>servings</p>
</WrapperInput>
{IngredientsList}
<WrapperButtons>
<Button onClick={props.closeModal}>Close</Button>
<VerticalSeperator />
<Button onClick={() => handleSubmit()}>Add</Button>
</WrapperButtons>
</StyledModal >
)
}
export default ModalAddIngredients

View File

@@ -0,0 +1,96 @@
import { Text, View, TextInput } from 'react-native'
import styled from 'styled-components'
import Modal from 'react-native-modal';
import { FaAsterisk } from 'react-icons/fa'
import { HiCheck } from 'react-icons/hi'
export const StyledModal = styled(Modal)`
.modal-content{
background-color: ${({ theme }) => theme.colors.dp01};
border-radius: 15px;
}
`
export const WrapperInput = styled(View)`
padding-left: 5px;
z-index:1;
display: flex;
flex-direction: row;
align-items: center;
margin-bottom:5px;
`
export const WrapperDropdown = styled(View)`
margin-left: 16px;
margin-top: -5px;
margin-bottom: 5px;
`
export const WrapperButtons = styled(View)`
z-index:1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
margin-top: 5px;
`
export const VerticalSeperator = styled(View)`
width: 1px;
height: 20px;
background-color: ${({ theme }) => theme.colors.dp12};
`
export const InputSmall = styled(TextInput)`
border-bottom-color: ${props => props.theme.colors.primary + 'aa'};
border-bottom-width: 1px;
font-size: ${({ theme }) => theme.fontSize.fontS};
width: 50%;
height: 24px;
margin-left: 5px;
`
export const Input = styled(TextInput)`
border-bottom-color: ${props => props.theme.colors.primary + 'aa'};
border-bottom-width: 1px;
font-size: ${({ theme }) => theme.fontSize.fontS};
width: 80%;
height: 24px;
margin-left: 5px;
padding-right: 20px;
`
export const Button = styled(Text)`
color: ${props => props.theme.colors.primary};
font-weight: bold;
font-size: ${({ theme }) => theme.fontSize.fontS};
margin-bottom: 10px;
`
export const ModalHeader = styled(Text)`
font-size: ${({ theme }) => theme.fontSize.fontM};
margin: 5px;
color: ${({ theme }) => theme.colors.textW1};
`
export const ModalDescription = styled(Text)`
background-color: ${({ theme }) => theme.colors.dp24};
color: ${({ theme }) => theme.colors.textW2};
margin: 5px 0px;
padding: 2px;
`
export const IconWrong = styled(FaAsterisk)`
margin-left: -10px;
margin-top: -10px;
font-size: ${({ theme }) => theme.fontSize.fontS};
color: ${({ theme }) => theme.colors.error + 'aa'};
`
export const IconCheck = styled(HiCheck)`
margin-left: -15px;
margin-top: -3px;
font-size: ${({ theme }) => theme.fontSize.fontM};
color: ${({ theme }) => theme.colors.primary};
`

View File

@@ -0,0 +1,32 @@
import { View } from 'react-native'
import styled from "styled-components"
import { Ingredient } from "./addRecipe/Ingredient"
import { CheckButton } from "../../styles/componentBlueprints"
import { ingredientCheckToggle } from "../../redux/slices/recipesSlice"
import { useDispatch } from "react-redux"
export const Wrapper = styled(View)`
display: flex;
flex-direction: row;
justify-content: center;
border-bottom-color: ${props => props.theme.colors.dp1};
border-bottom-width: 1px;
border-radius: 20px;
background-color: ${props => props.checked === true && props.theme.colors.selected + '33'};
`
const IngredientButton = (props) => {
const dispatch = useDispatch()
let ingredient = props.ingredient
return (
<Wrapper checked={ingredient.checked}>
<Ingredient ingredient={ingredient} multiplier={props.multiplier}/>
<View onClick={() => dispatch(ingredientCheckToggle({ id: props.recipeId, productName: ingredient.productName }))} >
<CheckButton checked={ingredient.checked} />
</View>
</Wrapper>
)
}
export default IngredientButton

View File

@@ -0,0 +1,14 @@
import { WrapperRecipeCard, RecipeName, ImageRecipe } from "./styles/recipeCard"
const RecipeCard = (props) => {
let recipe = props.recipe
return (
<WrapperRecipeCard to={"/recipes/"+recipe._id}>
{recipe.image && <ImageRecipe src={recipe.image} />}
<RecipeName>{recipe.name}</RecipeName>
</WrapperRecipeCard>
)
}
export default RecipeCard

View File

@@ -0,0 +1,17 @@
import { WrapperRecipeList } from "./styles/recipeList"
import RecipeCard from "./RecipeCard"
import { useSelector } from "react-redux"
const RecipeList = () => {
const recipes = useSelector(state=> state.recipes.entities)
let recipeList = recipes.map((recipe, index) => {
return (<RecipeCard key={index} recipe={recipe} />)
})
return (
<WrapperRecipeList>
{recipeList}
</WrapperRecipeList>
)
}
export default RecipeList

View File

@@ -0,0 +1,51 @@
import { useState } from "react"
import {
WrapperAddRecipe, IconPlus,
WrapperOptions, IconOptions, WrapperOptionButtons, IconRemove, IconEdit
} from "./styles/buttons"
import { useDispatch } from "react-redux"
import { removeRecipe } from "../../../redux/slices/recipesSlice"
import { PlusIcon, WrapperAddItem } from "../../GroceryList/groceries/styles/buttons"
import ModalAddIngredients from "../../modals/recipes/ModalAddIngredients"
export const AddRecipeButton = () => {
return (
// < Link to="/recipes/addRecipe">
<WrapperAddRecipe>
<IconPlus />
</WrapperAddRecipe>
// </Link>
)
}
export const AddIngredientsButton = (props) => {
const [visible, setVisible] = useState(false)
return (<>
<WrapperAddItem style={{ bottom: 10 }} onClick={()=>setVisible(true)}>
<PlusIcon />
</WrapperAddItem>
<ModalAddIngredients id={props.id} visible={visible} closeModal={() => setVisible(false)} />
</>
)
}
export const OptionsButtonRecipe = (props) => {
const dispatch = useDispatch()
const [toggled, setToggled] = useState(false)
const handleRemove = () => {
if (window.confirm("Do you really want to remove this recipe?")) {
dispatch(removeRecipe(props.id))
}
}
return (
<WrapperOptions toggled={toggled} >
<IconOptions toggled={toggled} onClick={() => setToggled(!toggled)} />
<WrapperOptionButtons toggled={toggled}>
<IconRemove onClick={() => handleRemove()} />
{/* <IconEdit onClick={() => history.push("/recipes/addRecipe/" + props.id)} /> */}
</WrapperOptionButtons>
</WrapperOptions>
)
}

View File

@@ -0,0 +1,22 @@
import { View } from "react-native"
import { useSelector } from "react-redux"
import { WrapperIngredient, TagCircle, IngredientName, IngredientPortion, IngredientAmount, IconEdit, IconRemove } from './styles/ingredient'
export const Ingredient = (props) => {
let ingredient = props.ingredient
let tag = useSelector(state => state.tags.entities.find(tag => tag.tagName === ingredient.tag))
let multiplier = props.multiplier ? props.multiplier : 1
return (
<WrapperIngredient>
<View id="left">
<TagCircle tagColor={tag.color} />
<IngredientPortion>{ingredient.portion}</IngredientPortion>
<IngredientName>{ingredient.productName}</IngredientName>
</View>
<IngredientAmount>{ingredient.amount*multiplier}</IngredientAmount>
{props.EditIngredient && <IconEdit onClick={() => props.EditIngredient(props.index)} />}
{props.RemoveIngredient && <IconRemove onClick={() => props.RemoveIngredient(props.index)} />}
</WrapperIngredient>
)
}

View File

@@ -0,0 +1,119 @@
import { useState } from "react"
import { View } from "react-native"
import { useSelector } from "react-redux"
import { selectAllProducts } from "../../../redux/slices/groceryList/productsSlice"
import Dropdown from "../../Dropdown"
import { Input } from "../../modals/styles/modal"
import { Ingredient } from "./Ingredient"
import {
WrapperIngredients, InputIngredientName, InputAmount, InputPortion, Button
} from "./styles/inputs"
export const Ingredients = (props) => {
let ingredients = props.ingredients
const products = useSelector(selectAllProducts)
const tags = useSelector(state => state.tags.entities)
const [ingredientName, setIngredientName] = useState('')
const [amount, setAmount] = useState('')
const [portion, setPortion] = useState('')
const [tag, setTag] = useState('')
const [price, setPrice] = useState('')
const [focused, setFocused] = useState(false)
const [focusedTag, setFocusedTag] = useState(false)
const [index, setIndex] = useState(-1)
const EditIngredient = (i) => {
setIndex(i)
setIngredientName(ingredients[i].productName)
setAmount(ingredients[i].amount)
setPortion(ingredients[i].portion)
}
const RemoveIngredient = (i) => {
let ingredientsEdited = ingredients.slice()
ingredientsEdited.splice(i, 1)
props.setIngredients(ingredientsEdited)
}
const IngredientList = ingredients.map((ingredient, key) => {
return (<Ingredient ingredient={ingredient} EditIngredient={EditIngredient} RemoveIngredient={RemoveIngredient} index={key} key={key} />)
}
)
const submitIngredient = () => {
let ingredientTag = tags.find(t => t.tagName === tag)
ingredientTag = ingredientTag ? ingredientTag.tagName : 'uncategorized'
if (ingredientName !== '' && index === -1) {
props.setIngredients([...ingredients, { productName: ingredientName, amount: Number(amount), portion, tag: ingredientTag, price }])
}
else if (index !== -1) {
let ingredientsEdited = ingredients.slice()
ingredientsEdited.splice(index, 1, { productName: ingredientName, amount: Number(amount), portion, tag: ingredientTag, price })
props.setIngredients(ingredientsEdited)
setIndex(-1)
}
else {
alert("Fill in the name of the ingredient")
return
}
setIngredientName('')
setAmount('')
setPortion('')
setTag('')
setPrice('')
}
const handleDropdownPress = (pName) => {
let product = products.find((product) => product.productName === pName);
setIngredientName(product.productName)
setTag(product.tag)
setPrice(product.price)
}
return (
<WrapperIngredients>
<h2>Ingredients</h2>
<View id="row">
<View style={{ width: '100%' }}>
{IngredientList}
<InputIngredientName onFocus={() => setFocused(true)} onBlur={() => { setTimeout(() => { setFocused(false) }, 100) }}
type="text"
value={ingredientName}
onChange={(text) => setIngredientName(text.target.value)}
placeholder="Name ingredient" />
{focused &&
<Dropdown array={products.map(product => product.productName)} text={ingredientName} setElement={handleDropdownPress} />}
<View id="row">
<View>
<Input
onFocus={() => setFocusedTag(true)} onBlur={() => { setTimeout(() => { setFocusedTag(false) }, 100) }}
style={{ marginLeft: 0 }}
type="text"
value={tag}
onChange={(text) => setTag(text.target.value)}
placeholder="tag" />
{focusedTag &&
<Dropdown array={tags.map(t => t.tagName)} text={tag} setElement={setTag} />}
</View>
<Input
type="number"
value={price}
onChange={(text) => setPrice(text.target.value)}
placeholder="Price" />
</View>
<View id="row">
<InputAmount
type="number"
value={amount}
onChange={(text) => setAmount(text.target.value)}
placeholder="amt." />
<InputPortion
type="text"
value={portion}
onChange={(text) => setPortion(text.target.value)}
placeholder="Portion (1/2 teaspoon, ...)" />
<Button onClick={submitIngredient} >{index === -1 ? 'Add' : 'Edit'}</Button>
</View>
</View>
</View>
</WrapperIngredients>
)
}

View File

@@ -0,0 +1,62 @@
import { View } from "react-native"
import styled, {css} from 'styled-components'
import { Button } from '../../../../styles/componentBlueprints'
import { FiPlus, FiSettings } from 'react-icons/fi'
import { GoTrashcan } from 'react-icons/go'
import { BiEditAlt } from 'react-icons/bi'
export const WrapperAddRecipe = styled(Button)`
position:absolute;
background-color: ${({ theme }) => theme.colors.primary};
bottom:20px;
right:20px;
@media ${({ theme }) => theme.mediaQueries.below768}{
bottom:30px;
right:10px;
}
`
export const IconPlus = styled(FiPlus)`
color: ${({ theme }) => theme.colors.textB2};
font-size: 60px;
@media ${({ theme }) => theme.mediaQueries.below768}{
font-size: 60px;
}
`
export const WrapperOptions = styled(View)`
position: absolute;
top: 0px;
right: 0px;
`
export const WrapperOptionButtons = styled(View)`
display: flex;
flex-direction: column;
position: absolute;
align-items: center;
top: 60px;
width: 55px;
right: 0px;
height: ${props => props.toggled ? css`100px` : css`0px` };
background-color: ${({theme}) => theme.colors.dp00 +'bb'};
border-bottom-left-radius: 8px;
overflow: hidden;
`
export const IconOptions = styled(FiSettings)`
margin-right: 5px;
margin-top: 10px;
color: ${({ theme }) => theme.colors.textW2};
transform: rotate(${props => props.toggled ? css`135deg` : css`0deg` });
font-size: 40px;
`
export const IconRemove = styled(GoTrashcan)`
position: absolute;
font-size: 40px;
color: ${({ theme }) => theme.colors.error};
`
export const IconEdit = styled(BiEditAlt)`
position: absolute;
top: 50px;
color: ${({ theme }) => theme.colors.textW2};
font-size: 40px;
`

View File

@@ -0,0 +1,61 @@
import { Text, View } from "react-native"
import styled from 'styled-components'
import { GoTrashcan } from 'react-icons/go'
import { BiEditAlt } from 'react-icons/bi'
export const WrapperIngredient = styled(View)`
margin: 5px 0px;
display: flex;
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
#left{
display: flex;
flex-direction: row;
align-items: center;
width: 75%;
}
`
export const TagCircle = styled(View)`
background-color: ${(props) => props.tagColor};
width: 7px;
height: 7px;
border-radius: 4px;
align-self: center;
margin: 0px 5px;
`
export const IngredientName = styled(Text)`
margin-left: 10px;
width: 60%;
font-size: ${({ theme }) => theme.fontSize.fontS};
overflow-wrap: break-word;
color: ${props => props.theme.colors.textW4};
`
export const IngredientPortion = styled(Text)`
width: 35%;
font-size: ${({ theme }) => theme.fontSize.fontS};
font-weight: 900;
word-break: break-all;
overflow-wrap: break-word;
color: ${props => props.theme.colors.textW5};
`
export const IngredientAmount = styled(Text)`
max-width: 12%;
margin-right: 48px;
font-size: ${({theme}) => theme.fontSize.fontS};
overflow-wrap: break-word;
color: ${props => props.theme.colors.textW5};
`
export const IconRemove = styled(GoTrashcan)`
position:absolute;
right: 0px;
color: ${({ theme }) => theme.colors.error + 'aa'};
font-size: ${({theme}) => theme.fontSize.fontS};
`
export const IconEdit = styled(BiEditAlt)`
position:absolute;
right: 20px;
color: ${({ theme }) => theme.colors.textW5};
font-size: ${({theme}) => theme.fontSize.fontS};
`

View File

@@ -0,0 +1,43 @@
import { Text, TextInput, View } from "react-native"
import styled from "styled-components"
export const WrapperIngredients = styled(View)`
margin-top: 10px;
width:100%;
border-left: solid ${props => props.theme.colors.primary + '55'} 1px;
padding-left: 5px;
/* #row{
position: relative;
display: flex;
flex-direction: row;
flex:1;
width:100%;
} */
`
export const InputIngredientName = styled(TextInput)`
margin-top: 10px;
font-size: ${({theme}) => theme.fontSize.fontS};
border-bottom-color: ${props => props.theme.colors.primary + 'aa'};
border-bottom-width: 1px;
width:100%;
`
export const InputAmount = styled(TextInput)`
font-size: ${({theme}) => theme.fontSize.fontS};
border-bottom-color: ${props => props.theme.colors.primary + 'aa'};
border-bottom-width: 1px;
width: 50px;
`
export const InputPortion = styled(TextInput)`
font-size: ${({theme}) => theme.fontSize.fontS};
border-bottom-color: ${props => props.theme.colors.primary + 'aa'};
border-bottom-width: 1px;
width: 60%;
margin-left: 10px;
`
export const Button = styled(Text)`
position: absolute;
right: 10px;
bottom: -8px;
color: ${props => props.theme.colors.primary + 'bb'};
font-weight: bold;
font-size: ${({theme}) => theme.fontSize.fontS};
`

View File

@@ -0,0 +1,39 @@
import { Image, Text, View } from 'react-native'
import styled from 'styled-components'
export const WrapperRecipeCard = styled(View)`
box-shadow: ${({theme})=> theme.colors.shadow};
display: flex;
flex:1;
flex-direction: column;
align-items: center;
overflow: hidden;
position: relative;
margin: 5px;
height: 130px;
min-width: 130px;
max-width: 200px;
background-color: ${props => props.theme.colors.dp01 + 'dd'};
border-radius: 20px;
`
export const RecipeName = styled(Text)`
position: absolute;
bottom: 0px;
width: 100%;
flex:1;
max-height: 70px;
padding: 5px;
font-size: 18px;
color: ${props => props.theme.colors.textW4};
background-color: ${props => props.theme.colors.dp00 + 'dd'};
`
export const ImageRecipe = styled(Image)`
object-fit: cover;
width: 100%;
height: 100%;
border-radius: 20px;
`

View File

@@ -0,0 +1,8 @@
import { View } from 'react-native'
import styled from 'styled-components'
export const WrapperRecipeList = styled(View)`
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
`

View File

@@ -0,0 +1,31 @@
import { View } from "react-native"
import styled from 'styled-components'
export const WrapperHeader = styled(View)`
z-index: 2;
position:absolute;
display: flex;
width: 100%;
flex-direction: row;
align-items: center;
padding: 7px 0px 7px 5px;
background-color: ${({ theme }) => theme.colors.dp00 + 'bb'};
/* h1{
width: 80%;
word-wrap: break-word;
margin: 0px;
margin-left: 20px;
font-size: 2.2rem;
color: ${props => props.theme.colors.textW1};
} */
`
// export const TextPeers = styled(Text)`
// position: absolute;
// right: 10px;
// top: 5px;
// background-color: #0002;
// color: ${props => props.theme.colors.textW5};
// padding: 0px 7px;
// border-radius: 8px;
// font-size: 1rem;
// `

Binary file not shown.

Binary file not shown.

Binary file not shown.

30
src/functions.js Normal file
View File

@@ -0,0 +1,30 @@
function LightenDarkenColor(col, nAmt) {
let amt = nAmt *2.55;
var usePound = false;
if (col[0] === "#") {
col = col.slice(1);
usePound = true;
}
var num = parseInt(col,16);
var r = (num >> 16) + amt;
if (r > 255) r = 255;
else if (r < 0) r = 0;
var b = ((num >> 8) & 0x00FF) + amt;
if (b > 255) b = 255;
else if (b < 0) b = 0;
var g = (num & 0x0000FF) + amt;
if (g > 255) g = 255;
else if (g < 0) g = 0;
return (usePound?"#":"") + (g | (b << 8) | (r << 16)).toString(16);
}
export default LightenDarkenColor

32
src/pages/HomePage.js Normal file
View File

@@ -0,0 +1,32 @@
import React from 'react'
import { View } from 'react-native'
import HeaderPadding from '../components/Header'
//components
import Header from '../components/Header'
import GroceryCard from '../components/Home/GroceryCard'
import RecipeCard from '../components/Home/RecipeCard'
//styling
import { Wrapper, WrapperBoard, WrapperRight, WrapperLeft } from './styles/HomePage'
const HomePage = () => {
return (
<View style={{flex: 1}}>
<HeaderPadding/>
<Wrapper>
<WrapperBoard>
<WrapperLeft>
<GroceryCard />
</WrapperLeft>
<WrapperRight>
<RecipeCard />
</WrapperRight>
</WrapperBoard>
</Wrapper>
</View>
)
}
export default HomePage

View File

@@ -0,0 +1,20 @@
import React, { useState } from 'react'
//components
import TabMenu from '../../components/GroceryList/TabMenu'
import Groceries from './storageManagement/Groceries'
import Products from './storageManagement/Products'
//styling
import { WrapperGroceryPage } from './styles/GroceryListPage'
const GroceryListPage = () => {
const [currentTab, setCurrentTab] = useState("Groceries")
return (
<WrapperGroceryPage>
{
currentTab === "Groceries" ? <Groceries /> : <Products />
}
<TabMenu currentTab={currentTab} setCurrentTab={setCurrentTab} />
</WrapperGroceryPage>
)
}
export default GroceryListPage

View File

@@ -0,0 +1,26 @@
import { Text, View } from 'react-native'
import React from 'react'
//components
import Header from '../../../components/Header'
import GroceryList from '../../../components/GroceryList/groceries/GroceryList'
import { LoadAnimation } from '../../../styles/componentBlueprints'
import { ContainerButtons, AddItemButton } from '../../../components/GroceryList/groceries/GroceryButtons'
//styling
import { ArrowBack } from '../../styles/page'
//redux
import { useSelector } from 'react-redux'
const Groceries = () => {
const statusItems = useSelector(state => state.items.status)
return (
<View>
{statusItems === 'loading' ? <LoadAnimation/>
: <GroceryList />}
<ContainerButtons />
<AddItemButton />
</View>
)
}
export default Groceries

View File

@@ -0,0 +1,28 @@
import { Text, View } from 'react-native'
import React from 'react'
//components
import Header from '../../../components/Header'
import ProductsList from '../../../components/GroceryList/products/ProductsList'
import { AddProductButton, ContainerButtons } from '../../../components/GroceryList/products/ProductButtons'
import { LoadAnimation } from '../../../styles/componentBlueprints'
//styling
import { ArrowBack } from '../../styles/page'
//redux
import { useSelector } from 'react-redux'
const Products = () => {
const statusProducts = useSelector(state => state.products.status)
return (
<View>
{statusProducts === 'loading' ? <LoadAnimation />
: <ProductsList />}
<AddProductButton />
<ContainerButtons />
</View>
)
}
export default Products

View File

@@ -0,0 +1,6 @@
import { View } from "react-native"
import styled from 'styled-components'
export const WrapperGroceryPage = styled(View)`
width:100%;
`

View File

@@ -0,0 +1,113 @@
import { Text, View } from 'react-native'
//components
import Header from "../../components/Header"
import { ArrowBack } from "../styles/page";
import { Ingredients } from "../../components/recipes/addRecipe/Inputs";
//styling
import { Wrapper, WrapperRecipe, Input, InputInstructions, IconRecipe, IconPot, IconMeal, IconImage, IconCheck } from "./styles/addRecipe"
import { useState } from "react";
import { addRecipe, findRecipeById, updateRecipe } from "../../redux/slices/recipesSlice";
import { useDispatch, useSelector } from "react-redux";
import { StyledImage } from "./styles/recipe";
import HeaderPadding from '../../components/Header';
const AddRecipe = () => {
let { id } = useParams()
const foundRecipe = useSelector(state => findRecipeById(state, id))
let recipe = foundRecipe ? foundRecipe : { name: '', prepTime: '', servings: '', image: '', ingredients: [], instructions: '' }
const dispatch = useDispatch()
const [name, setName] = useState(recipe.name)
const [prepTime, setPrepTime] = useState(recipe.prepTime)
const [servings, setServings] = useState(recipe.servings)
const [image, setImage] = useState(recipe.image)
const [ingredients, setIngredients] = useState(recipe.ingredients)
const [instructions, setInstructions] = useState(recipe.instructions)
const handleImageInput = (input) => {
if (input.target.files && input.target.files[0] && input.target.files[0].size < 1000000) {
let reader = new FileReader()
reader.onload = function (e) {
setImage(e.target.result)
};
reader.readAsDataURL(input.target.files[0])
}
else {
alert("That file is too big! Choose an image with a size lower than 1MB")
}
}
const submitRecipe = () => {
if (name && ingredients && instructions) {
foundRecipe ? dispatch(updateRecipe({ _id: foundRecipe._id, name, prepTime: Number(prepTime), servings: Number(servings), image, ingredients, instructions }))
: dispatch(addRecipe({ name, prepTime: Number(prepTime), servings: Number(servings), image, ingredients, instructions }))
console.log(typeof (image))
}
else {
alert("You should add a name, at least one ingredient and the instructions")
}
}
const handleInput = (event) => {
let lastChar = event.target.value.substr(-1).charCodeAt(0);
let penultimateChar = event.target.value.substr(-2).charCodeAt(0);
if (instructions === '') {
setInstructions("- " + event.target.value)
return
}
else if (penultimateChar === 10 && lastChar === 45) {
setInstructions(instructions.slice(0, instructions.length - 3))
return
}
else if (lastChar === 10) {
setInstructions(event.target.value + "- ")
return
}
setInstructions(event.target.value)
}
return (
<>
<HeaderPadding/>
<Wrapper >
{image && <StyledImage src={image} alt="Recipe" />}
<WrapperRecipe>
<View id="row">
<IconRecipe />
<Input
type="text"
value={name}
onChange={(text) => setName(text.target.value)}
placeholder="Recipe name" />
</View>
<View id="row">
<IconPot />
<Input
type="number"
value={prepTime}
onChange={(text) => setPrepTime(text.target.value)}
placeholder="Prep time (min)" />
<IconMeal />
<Input
type="number"
value={servings}
onChange={(text) => setServings(text.target.value)}
placeholder="Servings" />
</View>
<View id="row">
<IconImage />
<Input
style={{ borderBottom: 'none' }}
type="file" accept="image/*"
onChange={input => handleImageInput(input)} />
</View>
<Ingredients ingredients={ingredients} setIngredients={setIngredients} />
<InputInstructions
type="text"
value={instructions}
onChange={(text) => handleInput(text)}
placeholder='Instructions' />
</WrapperRecipe>
</Wrapper>
</>
)
}
export default AddRecipe

View File

@@ -0,0 +1,72 @@
import { useSelector } from "react-redux"
import { useParams } from "react-router"
//redux
import { findRecipeById } from "../../redux/slices/recipesSlice"
import Header from "../../components/Header"
import { ArrowBack } from "../styles/page"
import { Wrapper, WrapperRecipe } from "./styles/addRecipe"
import { Ingredient } from "../../components/recipes/addRecipe/Ingredient"
import { Title, InstructionWrapper, InstructionNumber, StyledImage, TextPrep, TextTime, TextMinutes, WrapperServings, TextServings, IconPotLarge, IconMeal, Hr } from "./styles/recipe"
import { OptionsButtonRecipe, AddIngredientsButton } from "../../components/recipes/addRecipe/Buttons"
import { Text, View } from "react-native"
import HeaderPadding from "../../components/Header"
const Recipe = () => {
let { id } = useParams()
const recipe = useSelector(state => findRecipeById(state, id))
const IngredientList = recipe.ingredients.map((ingredient, index) => {
return (<Ingredient ingredient={ingredient} index={index} key={index} />)
}
)
let strArr = recipe.instructions.split("- ")
const InstructionList = strArr.map((instruction, index) => {
return index !== 0 && (<InstructionWrapper key={index} >
<View><InstructionNumber>{index}</InstructionNumber></View>
<Text id="instruction">{instruction}</Text>
</InstructionWrapper>)
})
return (
<>
<HeaderPadding/>
<Wrapper >
{recipe.image && <StyledImage src={recipe.image} />}
<WrapperRecipe>
<Text>{recipe.name}</Text>
<Hr />
{recipe.prepTime !== 0 && recipe.prepTime && <>
<View id="row" style={{ position: 'relative', marginTop: 0 }} >
<IconPotLarge />
<TextPrep>READY IN:</TextPrep>
<View id="column" >
<TextTime>{recipe.prepTime}</TextTime>
<TextMinutes>minutes</TextMinutes>
</View>
{recipe.servings !== 0 && recipe.servings && <>
<WrapperServings>
<IconMeal />
<View>
<TextTime>{recipe.servings}</TextTime>
<TextServings>servings</TextServings>
</View>
</WrapperServings>
</>}
</View>
<Hr />
</>}
<View style={{ width: '100%' }}>
<Title>Ingredients</Title>
{IngredientList}
<Hr />
<Title>Instructions</Title>
{InstructionList}
</View>
</WrapperRecipe>
</Wrapper>
<AddIngredientsButton id={id} />
</>
)
}
export default Recipe

View File

@@ -0,0 +1,22 @@
//components
import RecipeList from "../../components/recipes/RecipeList"
import Header from '../../components/Header'
import { ArrowBack } from '../styles/page'
import { AddRecipeButton } from "../../components/recipes/addRecipe/Buttons";
import { LoadAnimation } from "../../styles/componentBlueprints";
//redux
import { useSelector } from "react-redux";
import { Text } from "react-native";
import HeaderPadding from "../../components/Header";
const RecipesPage = () => {
const statusRecipes = useSelector(state => state.recipes.status)
return (<>
<HeaderPadding/>
{statusRecipes === 'loading' ? <LoadAnimation/>
: <RecipeList />}
<AddRecipeButton />
</>
)
}
export default RecipesPage

View File

@@ -0,0 +1,95 @@
import { Image, Text, TextInput, View } from "react-native"
import styled from 'styled-components'
import { GiHotMeal, GiMeal, GiCookingPot } from 'react-icons/gi'
import { HiCheck } from 'react-icons/hi'
import { BiImageAdd } from 'react-icons/bi'
export const Wrapper = styled(View)`
display: flex;
flex-direction: column;
align-items: center;
@media ${({ theme }) => theme.mediaQueries.below768}{
align-items: flex-start;
}
`
export const WrapperRecipe = styled(View)`
display: flex;
flex-direction: column;
align-items: flex-start;
width: 500px;
padding: 10px;
#row{
margin-top: 5px;
display: flex;
flex-direction: row;
width: 100%;
align-items: flex-end;
}
#column{
margin-left: 5px;
margin-bottom: -3px;
display: flex;
flex-direction: column;
width: 100%;
justify-content: flex-end;
}
@media ${({ theme }) => theme.mediaQueries.below768}{
width: 100%;
}
`
export const Input = styled(TextInput)`
flex:1;
margin-left: 5px;
color: ${props => props.theme.colors.textW1};
border-bottom-color: ${props => props.theme.colors.primary + 'aa'};
border-bottom-width: 1px;
font-size: ${({theme}) => theme.fontSize.fontS};
width: 100%;
`
export const InputInstructions = styled(Text)`
margin-top: 30px;
padding: 5px;
color: ${props => props.theme.colors.textW1};
border-top: solid ${props => props.theme.colors.primary + 'aa'} 1px;
border-bottom-color: ${props => props.theme.colors.primary + 'aa'};
border-bottom-width: 1px;
font-size: ${({theme}) => theme.fontSize.fontS};
min-height: 600px;
width: 100%;
`
export const StyledImage = styled(Image)`
object-fit: cover;
width: 100px;
height: 100px;
`
export const IconRecipe = styled(GiHotMeal)`
color: ${({ theme }) => theme.colors.primaryVar};
font-size: ${({theme}) => theme.fontSize.fontM};
`
export const IconPot = styled(GiCookingPot)`
color: ${({ theme }) => theme.colors.primaryVar};
font-size: ${({theme}) => theme.fontSize.fontM};
`
export const IconMeal = styled(GiMeal)`
margin-left: 5px;
color: ${({ theme }) => theme.colors.primaryVar};
font-size: ${({theme}) => theme.fontSize.fontM};
`
export const IconImage = styled(BiImageAdd)`
color: ${({ theme }) => theme.colors.primaryVar};
font-size: ${({theme}) => theme.fontSize.fontM};
`
export const IconCheck = styled(HiCheck)`
margin-right: 10px;
color: ${({ theme }) => theme.colors.textB3};
background-color: ${({ theme }) => theme.colors.primary + 'cc'};
border-radius: 30px;
min-width: 40px;
height: 40px;
font-size: ${({theme}) => theme.fontSize.fontL};
`

View File

@@ -0,0 +1,93 @@
import { Image, Text, View } from "react-native"
import styled from 'styled-components'
import { GiCookingPot, GiMeal } from 'react-icons/gi'
export const Title = styled(Text)`
margin-top: 10px;
`
export const InstructionWrapper = styled(View)`
display: flex;
flex-direction: row;
width: 100%;
padding: 5px 0px;
align-items: center;
border-bottom-color: ${({ theme }) => theme.colors.dp02};
border-bottom-width: 3px;
#instruction{
margin-left: 7px;
color: ${({theme}) => theme.colors.textW4};
}
`
export const InstructionNumber = styled(Text)`
display: flex;
align-items: center;
justify-content: center;
background-color: ${({theme}) => theme.colors.primary + 'dd'};
color: ${({theme}) => theme.colors.textB1};
font-size: 25px;
font-weight: bold;
height: 34px;
width: 34px;
border-radius: 17px;
`
export const StyledImage = styled(Image)`
margin-top: -70px;
object-fit: cover;
height: 200px;
width: 100%;
@media ${({theme}) => theme.mediaQueries.above768}{
width: 500px;
}
`
export const TextPrep = styled(Text)`
text-align: right;
width: 70px;
font-weight: bold;
font-size: ${({theme}) => theme.fontSize.fontS};
line-height: 1.2;
`
export const TextTime = styled(Text)`
margin-bottom: -3px;
width: 70px;
font-size: ${({theme}) => theme.fontSize.fontS};
font-weight: bold;
line-height: 1.2;
color: ${({ theme }) => theme.colors.primary};
`
export const TextMinutes = styled(Text)`
width: 70px;
font-size: ${({theme}) => theme.fontSize.fontS};
font-weight: normal;
color: ${({ theme }) => theme.colors.primary};
`
export const WrapperServings = styled(View)`
display: flex;
position: absolute;
right: 2%;
top: 2px;
`
export const TextServings = styled(Text)`
margin-top: -5px;
font-weight: bold;
`
export const IconPotLarge = styled(GiCookingPot)`
margin: -20px 0px -16px 0px;
color: ${({ theme }) => theme.colors.primary};
font-size: ${({theme}) => theme.fontSize.fontL};
`
export const IconMeal = styled(GiMeal)`
margin: -20px 0px -16px 0px;
color: ${({ theme }) => theme.colors.primary};
font-size: ${({theme}) => theme.fontSize.fontL};
`
export const Hr = styled(View)`
width: 100%;
margin: 4px 0px;
height: 2px;
border-radius: 1px;
background-color: ${({ theme }) => theme.colors.dp08};
`

View File

@@ -0,0 +1,30 @@
import { View } from "react-native"
import styled from 'styled-components'
import { FiMenu } from 'react-icons/fi'
export const Wrapper = styled(View)`
display: flex;
flex-direction: column;
width: 100%;
align-items: center;
`
export const WrapperBoard = styled(View)`
display: flex;
justify-content: center;
flex-direction: row;
width: 100%;
flex: 1;
align-items: center;
`
export const WrapperLeft = styled(View)`
width: 50%;
padding: 0px 4px 8px 8px;
`
export const WrapperRight = styled(View)`
width: 50%;
padding: 0px 8px 8px 4px;
`
// export const Menu = styled(FiMenu)`
// color: ${props=>props.theme.colors.textW2};
// font-size: 45px;
// `

9
src/pages/styles/page.js Normal file
View File

@@ -0,0 +1,9 @@
import styled from 'styled-components'
import { BiArrowBack } from 'react-icons/bi'
export const ArrowBack = styled(BiArrowBack).attrs(props=>({
// onClick:() => history.goBack()
}))`
color: ${({theme}) => theme.colors.textW2};
font-size: 45px;
`

View File

@@ -0,0 +1,198 @@
import { createSlice, createAsyncThunk, createSelector } from '@reduxjs/toolkit';
export const fetchItems = createAsyncThunk('items/fetchItems', async () => {
let items = []
await fetch('https://ohomeapi.familymoyaers.com/groceries', {
headers: {
'Accept': 'application/json'
}
})
.then((res) => res.json())
.then((data) => { items = data })
items.forEach((item) => { item.checked = false; item.modalVisible = false; })
return items
})
export const itemAdded = createAsyncThunk('items/itemAdded',
async (payload, thunkAPI) => {
let newItem = {
...payload,
amount: payload.amount,
price: payload.price && Number(payload.price),
modalVisible: false,
checked: false,
}
// let existingItem = thunkAPI.getState().items.entities.slice().find(item => item.productName === newItem.productName)
// if (existingItem) {
// alert("This item was already in " + thunkAPI.getState().lists.entities.find((list) => list._id === existingItem.listId).listName.toString())
// existingItem = {
// ...existingItem,
// person: existingItem.person + ', ' + newItem.person,
// details: existingItem.details + ', ' + newItem.details,
// amount: existingItem.amount + newItem.amount
// }
// const requestOptions = {
// method: 'PUT',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(existingItem)
// };
// await fetch('https://ohomeapi.familymoyaers.com/grocery', requestOptions)
// return { existed: true, newItem: existingItem }
// }
// else {
// const requestOptions = {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(newItem)
// };
// await fetch('https://ohomeapi.familymoyaers.com/grocery', requestOptions)
// return { existed: false, newItem: newItem }
// }
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newItem)
};
await fetch('https://ohomeapi.familymoyaers.com/grocery', requestOptions)
return { existed: false, newItem: newItem }
}
)
export const itemsRemoved = createAsyncThunk('items/itemsRemoved', async (payload, thunkAPI) => {
let productIds = []
await thunkAPI.getState().items.entities
.forEach(item =>
item.checked === true && productIds.push(item._id)
)
const requestOptions = {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productIds: productIds })
};
await fetch('https://ohomeapi.familymoyaers.com/groceries', requestOptions)
})
export const itemsRemovedByList = createAsyncThunk('items/itemsRemovedByList', async (payload) => {
const requestOptions = {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ listId: payload })
};
console.log(payload)
await fetch('https://ohomeapi.familymoyaers.com/groceriesByList', requestOptions)
return payload
})
export const itemRemoved = createAsyncThunk('items/itemRemoved', async (payload, thunkAPI) => {
const requestOptions = {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId: payload })
};
await fetch('https://ohomeapi.familymoyaers.com/grocery', requestOptions)
return payload
})
const itemsSlice = createSlice({
name: 'items',
initialState: {
status: 'loading',
entities: [
]
},
reducers: {
checkToggle: {
reducer(state, action) {
let item = state.entities.find((item) => item._id === action.payload._id)
item.checked = !item.checked
},
prepare(_id) {
return {
payload: {
_id: _id
}
}
}
},
checkAll(state, action) {
let listItems = state.entities.filter(item => item.listId === action.payload)
if (listItems.slice().filter((item) => item.checked === false).length === 0) {
listItems.forEach(item => item.checked = false)
}
else {
listItems.forEach(item => item.checked = true)
}
},
modalToggle: {
reducer(state, action) {
let item = state.entities.find((item) => item._id === action.payload._id)
if (!state.entities.find((i) => i.modalVisible === true) || item.modalVisible) {
item.modalVisible = !item.modalVisible;
}
},
prepare(_id) {
return {
payload: {
_id: _id
}
}
}
},
},
extraReducers: builder => {
builder
.addCase(fetchItems.fulfilled, (state, action) => {
state.entities = action.payload;
// state.entities.unshift(...stockProducts);
state.status = 'idle'
console.log('items fetched')
})
.addCase(itemAdded.fulfilled, (state, action) => {
let newItem = action.payload.newItem
state.entities.push(newItem)
console.log('sent the new item')
// if (action.payload.existed === false) {
// state.entities.push(newItem)
// console.log('sent the new item')
// }
// else {
// let prevItem = state.entities.find(item => item._id === newItem._id)
// Object.assign(prevItem, newItem)
// console.log('edited the item')
})
.addCase(itemsRemoved.fulfilled, (state, action) => {
state.entities = state.entities.filter(item => item.checked === false)
console.log('items removed')
})
.addCase(itemsRemovedByList.fulfilled, (state, action) => {
state.entities = state.entities.filter(item => item.listId === action.payload)
})
.addCase(itemRemoved.fulfilled, (state, action) => {
state.entities.splice(state.entities.findIndex((item) => item._id === action.payload), 1)
console.log('item removed')
})
}
});
//selectors
//all items
export const selectAllSortedItems = createSelector(
state => state.items.entities,
state => state.products.entities,
(items, products) => {
return items.slice().sort((a, b) => {
let productA = products.find(product => product._id === a._id)
let tagA = productA ? productA.tag : a.tag ? a.tag : 'uncategorized'
let productB = products.find(product => product._id === b._id)
let tagB = productB ? productB.tag : b.tag ? b.tag : 'uncategorized'
return tagA.localeCompare(tagB)
});
})
//product by id
export const selectItemById = (state, _id) => state.items.entities.find(item => item._id === _id)
//item by modal visible
export const selectItemModalVisible = createSelector(
state => state.items.entities,
items => items.find(item => item.modalVisible === true)
)
export const { checkToggle, checkAll, modalToggle } = itemsSlice.actions;
export default itemsSlice.reducer;

View File

@@ -0,0 +1,97 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchLists = createAsyncThunk('items/fetchLists', async () => {
let lists = []
await fetch('https://ohomeapi.familymoyaers.com/groceryLists', {
headers: {
'Accept': 'application/json'
}
})
.then((res) => res.json())
.then((data) => { lists = data })
console.log(lists)
lists.forEach((list) => { list.open = false })
return lists
})
export const listAdded = createAsyncThunk('items/listAdded',
async (payload) => {
let list = {
...payload,
open: false
}
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(list)
};
await fetch('https://ohomeapi.familymoyaers.com/groceryList', requestOptions)
.then((res) => res.json())
.then((data) => { list._id = data })
return list
}
)
export const listRemoved = createAsyncThunk('items/listRemoved', async (payload, thunkAPI) => {
const requestOptions = {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ listId: payload })
};
await fetch('https://ohomeapi.familymoyaers.com/groceryList', requestOptions)
return payload
})
export const listEdited = createAsyncThunk('items/listEdited', async (payload) => {
const requestOptions = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
};
await fetch('https://ohomeapi.familymoyaers.com/groceryList', requestOptions)
return payload
})
const listsSlice = createSlice({
name: 'lists',
initialState: {
status: 'loading',
entities: [
]
},
reducers: {
toggleOpen(state, action) {
state.entities.forEach(list => list._id !== action.payload && (list.open = false))
let list = state.entities.find((list) => list._id === action.payload)
list && (list.open = !list.open)
},
},
extraReducers: builder => {
builder
.addCase(fetchLists.fulfilled, (state, action) => {
state.entities = action.payload;
// state.entities.unshift(...stockProducts);
state.status = 'idle'
// console.log('items fetched')
})
.addCase(listAdded.fulfilled, (state, action) => {
state.entities.push(action.payload)
// console.log('sent the new item')
})
.addCase(listRemoved.fulfilled, (state, action) => {
state.entities.splice(state.entities.findIndex((list) => list._id === action.payload), 1)
// console.log('item removed')
})
.addCase(listEdited.fulfilled, (state, action) => {
let list = state.entities.find(list=> list._id === action.payload._id)
list.listName = action.payload.listName
list.listDescription = action.payload.listDescription
// console.log('item removed')
})
}
});
export const selectOpenListId = (state) => {
let list = state.lists.entities.find(list => list.open === true)
return list && list._id }
// export const { checkToggle, checkAll, modalToggle } = itemsSlice.actions;
export const { toggleOpen } = listsSlice.actions;
export default listsSlice.reducer;

View File

@@ -0,0 +1,171 @@
import { createSlice, createAsyncThunk, createSelector } from '@reduxjs/toolkit';
export const fetchProducts = createAsyncThunk('products/fetchProducts', async () => {
let products = []
await fetch('https://ohomeapi.familymoyaers.com/products', {
headers: {
'Accept': 'application/json'
}
})
.then((res) => res.json())
.then((data) => { products = data })
products.forEach((product) => { product.checked = false; product.modalVisible = false; })
return products
})
export const productAdded = createAsyncThunk('products/productAdded',
async (payload) => {
let newProduct = {
...payload,
type: 'product',
price: Number(payload.price),
modalVisible: false,
checked: false,
}
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newProduct)
};
await fetch('https://ohomeapi.familymoyaers.com/product', requestOptions)
.then((res) => res.json())
.then((data) => { newProduct._id = data })
return newProduct
}
)
export const productsRemoved = createAsyncThunk('products/productsRemoved', async (payload, thunkAPI) => {
let productIds = []
await thunkAPI.getState().products.entities
.forEach(product =>
product.checked === true && productIds.push(product._id)
)
const requestOptions = {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productIds: productIds })
};
await fetch('https://ohomeapi.familymoyaers.com/products', requestOptions)
})
export const productRemoved = createAsyncThunk('products/productRemoved', async (payload) => {
const requestOptions = {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId: payload })
};
await fetch('https://ohomeapi.familymoyaers.com/product', requestOptions)
return payload
})
export const productsUploaded = createAsyncThunk('products/productsUploaded', async (payload) => {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
};
fetch('https://ohomeapi.familymoyaers.com/products', requestOptions)
return payload
})
const productsSlice = createSlice({
name: 'products',
initialState: {
status: 'loading',
entities: [
]
},
reducers: {
checkToggle: {
reducer(state, action) {
let product = state.entities.find((product) => product._id === action.payload._id)
product.checked = !product.checked;
},
prepare(_id) {
return {
payload: {
_id: _id
}
}
}
},
modalToggle: {
reducer(state, action) {
let product = state.entities.find((product) => product._id === action.payload._id)
if (!state.entities.find((p) => p.modalVisible === true) || product.modalVisible) {
product.modalVisible = !product.modalVisible;
}
},
prepare(_id) {
return {
payload: {
_id: _id
}
}
}
},
},
extraReducers: builder => {
builder
.addCase(fetchProducts.fulfilled, (state, action) => {
state.entities = action.payload
state.status = 'idle'
console.log('products fetched')
})
.addCase(productAdded.fulfilled, (state, action) => {
state.entities.push(action.payload);
console.log('sended the item')
})
.addCase(productsRemoved.fulfilled, (state, action) => {
state.entities = state.entities.filter(product => product.checked === false)
console.log('products removed')
})
.addCase(productRemoved.fulfilled, (state, action) => {
state.entities.splice(state.entities.findIndex((product) => product._id === action.payload), 1)
console.log('product removed')
})
.addCase(productsUploaded.fulfilled, (state, action) => {
action.payload.forEach(product => state.entities.push(product))
})
}
});
//selectors
//all products
export const selectAllProducts = state => state.products.entities
export const selectAllSortedProducts = state => state.products.entities.slice().sort((a, b) => a.tag.localeCompare(b.tag))
//product by id
export const selectProductsByTag = (state, tag) => state.products.entities.filter(product => product.tag === tag)
export const selectProductById = (state, _id) => {
let product = state.products.entities.find(product => product._id === _id)
if (product) {
return product
}
else {
return {
productName: _id,
tag: 'uncategorized',
}
}
}
export const selectProductByItem = (state, item) => {
let product = state.products.entities.find(product => product._id === item._id)
if (product) {
return product
}
else {
return {
productName: item._id,
tag: item.tag ? item.tag : 'uncategorized',
price: item.price && item.price,
}
}
}
export const selectProductModalVisible = createSelector(
state => state.products.entities,
products => products.find(product => product.modalVisible === true)
)
export const { checkToggle, modalToggle } = productsSlice.actions;
export default productsSlice.reducer;

View File

@@ -0,0 +1,49 @@
import { createSlice} from '@reduxjs/toolkit';
const initialState = {
status: 'idle',
entities: [
{ tagName: 'uncategorized', color: '#92a69b' },
{ tagName: 'Vegetables', color: '#BDD684' },
{ tagName: 'Grains', color: '#FA9600' },
{ tagName: 'Meal', color: '#ffe2a3' },
{ tagName: 'Meat', color: '#ef5350' },
{ tagName: 'Dairy', color: '#E8E8E8' },
{ tagName: 'Preservable', color: '#8ac8e6' },
{ tagName: 'Spices', color: '#d8eb34' },
{ tagName: 'Drinks', color: '#00e0b7' },
{ tagName: 'Snack', color: '#9575CD' },
{ tagName: 'Fruit', color: '#8fff40' },
{ tagName: 'Sauce', color: '#FCCABB' },
{ tagName: 'Baking', color: '#edb600' },
{ tagName: 'Cleaning', color: '#26EDEA' },
{ tagName: 'Hygiene', color: '#FC92F9' },
{ tagName: 'Pets', color: '#a0fc92' },
{ tagName: 'Equipment', color: '#C8C8E6' },
]
}
const tagsSlice = createSlice({
name: 'tags',
initialState,
reducers: {
tagAdded: {
reducer(state, action) {
state.push(action.payload)
},
prepare(tagName, color) {
return {
payload: {
tagName,
color
}
}
}
}
}
})
export const findTag = (state, tagName) => state.tags.entities.find(tag => tag.tagName === tagName)
export const selectAllTags = (state) => state.tags.entities
export const { tagAdded } = tagsSlice.actions
export default tagsSlice.reducer

View File

@@ -0,0 +1,26 @@
import { createSlice} from '@reduxjs/toolkit';
const initialState = {
peers: 0,
}
const informationSlice = createSlice({
name: 'information',
initialState,
reducers: {
peersChanged: {
reducer(state, action) {
state.peers += action.payload.amount
},
prepare(amount) {
return {
payload: {
amount
}
}
}
}
}
})
export const { peersChanged } = informationSlice.actions
export default informationSlice.reducer

View File

@@ -0,0 +1,85 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
export const fetchRecipes = createAsyncThunk('recipes/fetchRecipes', async () => {
let recipes = []
await fetch('https://ohomeapi.familymoyaers.com/recipes', {
headers: {
'Accept': 'application/json'
}
})
.then((res) => res.json())
.then((data) => { recipes = data })
recipes.forEach(recipe => recipe.ingredients.forEach(ingredient => ingredient.checked = false))
return recipes
})
export const addRecipe = createAsyncThunk('recipes/addRecipe', async (payload) => {
const size = new TextEncoder().encode(JSON.stringify(payload)).length
console.log(size)
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
};
await fetch('https://ohomeapi.familymoyaers.com/recipe', requestOptions)
return payload
})
export const updateRecipe = createAsyncThunk('recipes/updateRecipe', async (payload) => {
const requestOptions = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
};
await fetch('https://ohomeapi.familymoyaers.com/recipe', requestOptions)
return payload
})
export const removeRecipe = createAsyncThunk('recipes/removeRecipe', async (payload) => {
const requestOptions = {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: payload })
};
await fetch('https://ohomeapi.familymoyaers.com/recipe', requestOptions)
return payload
})
const recipesSlice = createSlice({
name: 'recipes',
initialState: {
status: 'loading',
entities: []
},
reducers: {
ingredientCheckToggle(state, action) {
let ingredient = state.entities.find(recipe => recipe._id === action.payload.id).ingredients.find(ingredient => ingredient.productName === action.payload.productName)
ingredient.checked = !ingredient.checked
}
},
extraReducers: builder => {
builder
.addCase(fetchRecipes.fulfilled, (state, action) => {
state.entities = action.payload
state.status = 'idle'
console.log('RECIPES fetched')
})
.addCase(addRecipe.fulfilled, (state, action) => {
state.entities.push(action.payload)
console.log('RECIPE added')
})
.addCase(updateRecipe.fulfilled, (state, action) => {
let index = state.entities.findIndex(recipe => recipe._id === action.payload._id)
state.entities.splice(index, 1, action.payload)
})
.addCase(removeRecipe.fulfilled, (state, action) => {
state.entities.splice(state.entities.findIndex(recipe => recipe._id === action.payload), 1)
console.log('RECIPE removed')
})
}
})
export const findRecipeById = (state, id) => state.recipes.entities.find(recipe => recipe._id === id)
export const { ingredientCheckToggle } = recipesSlice.actions
export default recipesSlice.reducer

23
src/redux/store.js Normal file
View File

@@ -0,0 +1,23 @@
import { configureStore } from '@reduxjs/toolkit';
import itemsReducer from './slices/groceryList/itemsSlice';
import productsReducer from './slices/groceryList/productsSlice';
import lists from './slices/groceryList/listsSlice';
import tagsReducer from './slices/groceryList/tagsSlice';
// import informationReducer from './slices/informationSlice';
import recipes from './slices/recipesSlice';
export default configureStore({
reducer: {
lists: lists,
items: itemsReducer,
products: productsReducer,
recipes: recipes,
tags: tagsReducer,
},
})

View File

@@ -0,0 +1,53 @@
import { View } from "react-native"
import React from 'react'
import styled, { css } from 'styled-components'
//deps
import { HiCheck } from 'react-icons/hi'
//anims
import LightenDarken from '../functions'
//standard button layout
export const Button = styled(View)`
box-shadow: ${({ theme }) => theme.colors.shadow};
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height:80px;
border-radius: 50px;
@media ${({ theme }) => theme.mediaQueries.below768}{
height: 65px;
width: 65px;
}
`
const CheckButtonWrapper = styled(View)`
display:flex;
justify-content: center;
align-items: center;
height: 30px;
min-width: 30px;
border: 1px solid ${props => props.checked ? props.theme.colors.selected : LightenDarken(props.theme.colors.dp24, -5)};
border-radius: 15px;
margin: 0px 5px ;
background-color: ${props => props.checked ? props.theme.colors.selected : props.theme.colors.buttonGrey};
&:hover{
background-color: ${props => props.checked ? props.theme.colors.selected : LightenDarken(props.theme.colors.buttonGrey, 5)}
}
`
const IconCheck = styled(HiCheck)`
font-size: ${({ theme }) => theme.fontSize.fontL};
color: ${({ theme }) => theme.colors.dp00};
`
export const CheckButton = (props) => {
return (
<CheckButtonWrapper checked={props.checked} >
<IconCheck checked={props.checked} />
</CheckButtonWrapper>
)
}
//load anim
export const LoadAnimation = () => {
return (<View style={{ backgroundColor: "red", width: 100, height: 100 }} />)
}

View File

@@ -0,0 +1,73 @@
// import { createGlobalStyle } from 'styled-components'
// export default createGlobalStyle`
// * {
// padding: 0px;
// margin: 0px;
// font-size: 1rem;
// font-family: sansationRegular;
// }
// a {
// text-decoration:none;
// }
// p{
// padding: 0px;
// margin:0px;
// font-size: 1.2rem;
// color: ${({theme}) => theme.colors.textW5};
// }
// h1{
// margin: 0px;
// padding: 0px;
// font-size: 1.8rem;
// color: ${({theme}) => theme.colors.textW1};
// font-weight: bold;
// }
// h2{
// margin: 0px;
// padding: 0px;
// font-size: 1.5rem;
// color: ${({theme}) => theme.colors.textW3};
// font-weight: bold;
// }
// button{
// padding: 0px;
// border: none;
// background: none;
// }
// input{
// color: ${props => props.theme.colors.textW1};
// background-color: #0000;
// resize: none;
// outline: none;
// border: none;
// }
// textarea{
// background-color: #0000;
// resize: none;
// outline: none;
// border: none;
// }
// /* width */
// ::-webkit-scrollbar {
// width: 5px;
// }
// /* Track */
// ::-webkit-scrollbar-track {
// opacity: 0;
// }
// /* Handle */
// ::-webkit-scrollbar-thumb {
// background: #555;
// }
// ::-moz-selection { /* Code for Firefox */
// color: white;
// background: #7bc97eaa;
// }
// ::selection {
// color: white;
// background: #7bc97eaa;
// }
// `

106
src/styles/theme.js Normal file
View File

@@ -0,0 +1,106 @@
import LightenDarken from '../functions'
let themeStyle = 'light'
const dp00 = themeStyle === 'dark' ? '#0C0F12' : '#f9f6ef'
const textW0 = themeStyle === 'dark' ? '#ffffff': '#524131'
const textB0 = themeStyle === 'dark' ? '#000000': '#ffffff'
const theme = {
colors: themeStyle === 'dark' ? {
//buttons
buttonGrey: LightenDarken(dp00, 7), //30%,
//standard
primary: '#98f8b6', //primary (light: #3e897c) #005c51 52c7b8 b6fcec #98f8b6
primaryVar: '#aaf898',
secondary: '#F2A8DB', //secondary
error: '#ef5350',
selected: '#3e8362',
itemSelected: '#253f34',
iconGrey: '#8a8a8a',
//background
dp00: dp00, //0%
dp01: LightenDarken(dp00, 5), //5%
dp02: LightenDarken(dp00, 7), //7%
dp03: LightenDarken(dp00, 8), //8%
dp04: LightenDarken(dp00, 9), //9%
dp06: LightenDarken(dp00, 11), //11%
dp08: LightenDarken(dp00, 12), //12%
dp12: LightenDarken(dp00, 14), //14%
dp16: LightenDarken(dp00, 15), //15%
dp24: LightenDarken(dp00, 16), //16%
//text
textW0: textW0,
textW1: LightenDarken(textW0, -5),
textW2: LightenDarken(textW0, -10),
textW3: LightenDarken(textW0, -15),
textW4: LightenDarken(textW0, -20),
textW5: LightenDarken(textW0, -25),
textB0: textB0,
textB1: LightenDarken(textB0, 5),
textB2: LightenDarken(textB0, 10),
textB3: LightenDarken(textB0, 15),
textB4: LightenDarken(textB0, 20),
textB5: LightenDarken(textB0, 25),
} :
{
//buttons
buttonGrey: dp00, //30%,
//standard
primary: '#f28006', //primary (light: #3e897c) #005c51 52c7b8 b6fcec #98f8b6
primaryVar: '#da5d3f',
secondary: '#F2A8DB', //secondary
error: '#ef5350',
selected: '#E09A4F',
itemSelected: '#ECC194',
// iconGrey: '#8a8a8a',
iconGrey: '#da5d3f',
shadow: '2px 2px 5px #0001',
//background
dp00: dp00, //0%
dp01: LightenDarken(dp00, -5 ), //5%
dp02: LightenDarken(dp00, -7 ), //7%
dp03: LightenDarken(dp00, -8 ), //8%
dp04: LightenDarken(dp00, -9 ), //9%
dp06: LightenDarken(dp00, -11), //11%
dp08: LightenDarken(dp00, -12), //12%
dp12: LightenDarken(dp00, -14), //14%
dp16: LightenDarken(dp00, -15), //15%
dp24: LightenDarken(dp00, -16), //16%
//text
textW0: textW0,
textW1: LightenDarken(textW0, 5),
textW2: LightenDarken(textW0, 10),
textW3: LightenDarken(textW0, 15),
textW4: LightenDarken(textW0, 20),
textW5: LightenDarken(textW0, 25),
textB0: textB0,
textB1: LightenDarken(textB0, -5),
textB2: LightenDarken(textB0, -8),
textB3: LightenDarken(textB0, -12),
textB4: LightenDarken(textB0, -15),
textB5: LightenDarken(textB0, -18),
},
fontSizes: {
fontS: '16px',
fontM: '24px',
fontL: '32px',
fontXL: '64px',
},
mediaQueries: {
"below768": "only screen and (max-width: 768px)",
"above768": "only screen and (min-width: 768px)"
}
}
export default theme

131
src/utils.js Normal file
View File

@@ -0,0 +1,131 @@
// redux
import { fetchProducts } from './redux/slices/groceryList/productsSlice';
import { fetchItems } from './redux/slices/groceryList/itemsSlice';
import { fetchLists } from './redux/slices/groceryList/listsSlice';
import store from './redux/store';
import { fetchRecipes } from './redux/slices/recipesSlice';
const updateProducts = async () => {
let APIProductIds = ''
await fetch('https://ohomeapi.familymoyaers.com/productNames')
.then(res => res.json())
.then((data) => { APIProductIds = JSON.stringify(data) })
let productIds = JSON.stringify(store.getState().products.entities.slice().map((product) => product._id))
return APIProductIds !== productIds
}
const updateItems = async () => {
let APIItemIds = ''
await fetch('https://ohomeapi.familymoyaers.com/groceryNames')
.then(res => res.json())
.then((data) => { APIItemIds = JSON.stringify(data) })
let ItemIds = JSON.stringify(store.getState().items.entities.slice().map((item) => item._id))
return APIItemIds !== ItemIds
}
// const updateLists = async () => {
// let APILists = ''
// await fetch('https://ohomeapi.familymoyaers.com/groceryLists')
// .then(res => res.json())
// .then((data) => { APILists = JSON.stringify(data) })
// let lists = JSON.stringify(store.getState().lists.entities)
// return APILists !== lists
// }
export const fetchAll = () => {
store.dispatch(fetchProducts())
store.dispatch(fetchItems())
store.dispatch(fetchLists())
store.dispatch(fetchRecipes())
setInterval(function () {
updateProducts()
.then((result) => result && store.dispatch(fetchProducts()))
updateItems()
.then((result) => result && store.dispatch(fetchItems()))
// updateLists()
// .then((result) => result && store.dispatch(fetchLists()))
}, 5000);
}
// let ipfs = undefined;
// let orbitdb = undefined;
// export let db = undefined;
// var ipfsConfig = {
// preload: { enabled: false },
// config: {
// Addresses: {
// Swarm: [
// // Use IPFS dev signal server
// // Websocket:
// // '/dns4/ws-star-signal-1.servep2p.com/tcp/443/wss/p2p-websocket-star',
// // '/dns4/ws-star-signal-2.servep2p.com/tcp/443/wss/p2p-websocket-star',
// // '/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star',
// // WebRTC:
// // '/dns4/star-signal.cloud.ipfs.team/wss/p2p-webrtc-star',
// // '/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star/',
// // '/dns4/wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star/',
// // '/dns4/webrtc-star.discovery.libp2p.io/tcp/443/wss/p2p-webrtc-star/',
// // Use local signal server
// // '/ip4/0.0.0.0/tcp/9090/wss/p2p-webrtc-star',
// '/dns4/wrtc-star.familymoyaers.com/tcp/443/wss/p2p-webrtc-star/',
// ]
// }
// },
// // Discovery: {
// // MDNS: {
// // Interval: 1
// // }
// // },
// EXPERIMENTAL: {
// pubsub: true
// }
// }
// export async function main(store) {
// // Create IPFS instance
// try {
// ipfs = await IPFS.create(ipfsConfig)
// orbitdb = await OrbitDB.createInstance(ipfs)
// const options = {
// // Give write access to ourselves
// accessController: {
// // type: 'orbitdb', //OrbitDBAccessController
// write: ['*']
// }
// }
// db = await orbitdb.keyvalue('37th-database', options)
// console.log('db.load ')
// await db.load()
// await fetchAllValues(store)
// db.events.on('replicated', () => {
// fetchAllValues(store)
// })
// db.events.on('peer', () => {
// store.dispatch(peersChanged(1))
// })
// let maxTotal = 0, loaded = 0
// db.events.on('load.progress', (address, hash, entry, progress, total) => {
// maxTotal = Math.max.apply(null, [maxTotal, progress, 0])
// total = Math.max.apply(null, [progress, maxTotal, total, entry.clock.time, 0])
// console.log(maxTotal / total)
// })
// }
// catch (err) {
// throw Error(err)
// }
// }
// const fetchAllValues = async (store) => {
// console.log('Loaded...')
// const dbValues = await Object.values(db.all)
// const products = dbValues.filter(value => value.type === 'product')
// const items = dbValues.filter(value => value.type === 'item')
// console.log('Fetched...')
// }

1831
yarn.lock

File diff suppressed because it is too large Load Diff