Refactor state and make list detail a page
This commit is contained in:
@@ -24,6 +24,7 @@ There are currently snippets for the following common mobile components:
|
||||
- [x] Content
|
||||
- [x] Tabs
|
||||
- [ ] Nav (in progress)
|
||||
- [ ] Next.js router integration
|
||||
- [x] Icon
|
||||
- [x] Menu
|
||||
- [x] Modal
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import Store from '../store';
|
||||
import * as actions from '../store/actions';
|
||||
import * as selectors from '../store/selectors';
|
||||
|
||||
const MenuItem = ({ children, ...props }) => (
|
||||
<li {...props}>
|
||||
@@ -12,14 +14,11 @@ const MenuItem = ({ children, ...props }) => (
|
||||
);
|
||||
|
||||
const MenuContent = () => {
|
||||
const pages = Store.useState(s => s.pages);
|
||||
const menuLinks = Store.useState(selectors.getMenuLinks);
|
||||
|
||||
const go = page => {
|
||||
Store.update(s => {
|
||||
console.log('Going to', page);
|
||||
s.currentPage = page;
|
||||
s.showMenu = false;
|
||||
});
|
||||
actions.setPage(page);
|
||||
actions.setMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -28,11 +27,15 @@ const MenuContent = () => {
|
||||
<h2 className="text-xl select-none">Menu</h2>
|
||||
</div>
|
||||
<ul>
|
||||
{pages.map(p => (
|
||||
{menuLinks.map(p => {
|
||||
const title = typeof p.title === 'function' ? p.title() : p.title;
|
||||
|
||||
return (
|
||||
<MenuItem key={p.id} onClick={() => go(p)}>
|
||||
{p.title}
|
||||
{title}
|
||||
</MenuItem>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { homeItems } from '../../data';
|
||||
import Store from '../../store';
|
||||
import Card from '../ui/Card';
|
||||
import Content from '../ui/Content';
|
||||
|
||||
import * as selectors from '../../store/selectors';
|
||||
|
||||
const HomeCard = ({ title, type, text, author, image }) => (
|
||||
<Card className="my-4">
|
||||
<div>
|
||||
@@ -17,7 +18,7 @@ const HomeCard = ({ title, type, text, author, image }) => (
|
||||
);
|
||||
|
||||
const Home = ({ selected }) => {
|
||||
const homeItems = Store.useState(s => s.homeItems);
|
||||
const homeItems = Store.useState(selectors.getHomeItems);
|
||||
|
||||
return (
|
||||
<Content visible={selected} className="p-4">
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import Store from '../../store';
|
||||
import * as actions from '../../store/actions';
|
||||
import * as selectors from '../../store/selectors';
|
||||
|
||||
import Content from '../ui/Content';
|
||||
import List from '../ui/List';
|
||||
import VirtualScroll from '../ui/VirtualScroll';
|
||||
|
||||
const ListItems = ({ list, onClose }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="py-2">
|
||||
<a href="#" onClick={onClose}>
|
||||
All Lists
|
||||
</a>
|
||||
</div>
|
||||
<VirtualScroll
|
||||
data={list?.items || []}
|
||||
totalCount={(list?.items || []).length}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
itemContent={(i, item) => <ListItemEntry list={list} item={item} />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItemEntry = ({ list, item }) => (
|
||||
<div className="p-4 border-solid border-b cursor-pointer">
|
||||
<span className="text-md">{item.name}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.done}
|
||||
onChange={() => {
|
||||
actions.setDone(list, item, !item.done);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ListDetail = ({ selected }) => {
|
||||
const selectedList = Store.useState(selectors.getSelectedList);
|
||||
|
||||
return (
|
||||
<Content visible={selected} className="p-4">
|
||||
<List className="h-full w-full">
|
||||
{selected && (
|
||||
<ListItems
|
||||
list={selectedList}
|
||||
onClose={() => {
|
||||
actions.setSelectedList(null);
|
||||
actions.setPageById('lists');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListDetail;
|
||||
@@ -1,11 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import Store from '../../store';
|
||||
import { setDone } from '../../store/actions';
|
||||
import Card from '../ui/Card';
|
||||
import * as actions from '../../store/actions';
|
||||
import * as selectors from '../../store/selectors';
|
||||
|
||||
import Content from '../ui/Content';
|
||||
import List from '../ui/List';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import VirtualScroll from '../ui/VirtualScroll';
|
||||
|
||||
const ListEntry = ({ list, ...props }) => (
|
||||
@@ -15,7 +13,7 @@ const ListEntry = ({ list, ...props }) => (
|
||||
);
|
||||
|
||||
const AllLists = ({ onSelect }) => {
|
||||
const lists = Store.useState(s => s.lists);
|
||||
const lists = Store.useState(selectors.getLists);
|
||||
|
||||
return (
|
||||
<VirtualScroll
|
||||
@@ -29,58 +27,15 @@ const AllLists = ({ onSelect }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ListItems = ({ list, onClose }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="py-2">
|
||||
<a href="#" onClick={onClose}>
|
||||
All Lists
|
||||
</a>
|
||||
</div>
|
||||
<VirtualScroll
|
||||
data={list.items || []}
|
||||
totalCount={(list.items || []).length}
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
itemContent={(i, item) => <ListItemEntry list={list} item={item} />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ListItemEntry = ({ list, item }) => (
|
||||
<div className="p-4 border-solid border-b cursor-pointer">
|
||||
<span className="text-md">{item.name}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.done}
|
||||
onChange={() => {
|
||||
setDone(list, item, !item.done);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Feed = ({ selected }) => {
|
||||
const selectedList = Store.useState(s => s.selectedList);
|
||||
|
||||
const Lists = ({ selected }) => {
|
||||
return (
|
||||
<Content visible={selected} className="p-4">
|
||||
<List className="h-full w-full">
|
||||
{selected && selectedList ? (
|
||||
<ListItems
|
||||
list={selectedList}
|
||||
onClose={() =>
|
||||
Store.update(s => {
|
||||
s.selectedList = null;
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
{selected && (
|
||||
<AllLists
|
||||
onSelect={list => {
|
||||
Store.update(s => {
|
||||
s.selectedList = list;
|
||||
});
|
||||
actions.setSelectedList(list);
|
||||
actions.setPageById('list-detail');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -89,4 +44,4 @@ const Feed = ({ selected }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Feed;
|
||||
export default Lists;
|
||||
|
||||
+9
-15
@@ -1,10 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Plugins } from '@capacitor/core';
|
||||
import Store from '../../store';
|
||||
import * as actions from '../../store/actions';
|
||||
|
||||
const Nav = ({ page }) => {
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||
|
||||
const title = typeof page.title === 'function' ? page.title() : page.title;
|
||||
|
||||
useEffect(() => {
|
||||
Plugins.StatusBar.setStyle({
|
||||
@@ -23,11 +25,7 @@ const Nav = ({ page }) => {
|
||||
<div className="relative flex items-center justify-between h-16">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 flex items-center sm:hidden"
|
||||
onClick={() =>
|
||||
Store.update(s => {
|
||||
s.showMenu = true;
|
||||
})
|
||||
}
|
||||
onClick={() => actions.setMenuOpen(true)}
|
||||
>
|
||||
{/* Mobile menu button*/}
|
||||
<button
|
||||
@@ -81,7 +79,7 @@ const Nav = ({ page }) => {
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<h1 className="text-gray-50">{page.title}</h1>
|
||||
<h1 className="text-gray-50">{title}</h1>
|
||||
</div>
|
||||
<div className="hidden sm:block sm:ml-6">
|
||||
<div className="flex space-x-4">
|
||||
@@ -116,11 +114,7 @@ const Nav = ({ page }) => {
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
<button
|
||||
className="bg-gray-800 p-1 rounded-full text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||
onClick={() =>
|
||||
Store.update(s => {
|
||||
s.showNotifications = true;
|
||||
})
|
||||
}
|
||||
onClick={() => actions.setNotificationsOpen(true)}
|
||||
>
|
||||
<span className="sr-only">View notifications</span>
|
||||
{/* Heroicon name: bell */}
|
||||
@@ -148,7 +142,7 @@ const Nav = ({ page }) => {
|
||||
className="bg-gray-800 flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
|
||||
id="user-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={() => setShowMenu(!showMenu)}
|
||||
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
||||
>
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<img
|
||||
@@ -170,7 +164,7 @@ const Nav = ({ page }) => {
|
||||
*/}
|
||||
<div
|
||||
className={`${
|
||||
showMenu ? '' : 'hidden'
|
||||
showProfileMenu ? '' : 'hidden'
|
||||
} origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5`}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
|
||||
Generated
+6
@@ -4497,6 +4497,12 @@
|
||||
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
|
||||
"integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
|
||||
},
|
||||
"reselect": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
|
||||
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==",
|
||||
"dev": true
|
||||
},
|
||||
"resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@
|
||||
"prettier": "^2.2.1",
|
||||
"pullstate": "^1.20.5",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-use-gesture": "^9.0.0-beta.11"
|
||||
"react-use-gesture": "^9.0.0-beta.11",
|
||||
"reselect": "^4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-31
@@ -1,4 +1,8 @@
|
||||
import { useDrag } from 'react-use-gesture';
|
||||
|
||||
import Store from '../store';
|
||||
import * as actions from '../store/actions';
|
||||
import * as selectors from '../store/selectors';
|
||||
|
||||
import App from '../components/ui/App';
|
||||
import Backdrop from '../components/ui/Backdrop';
|
||||
@@ -9,14 +13,11 @@ import PageStack from '../components/ui/PageStack';
|
||||
import Tab from '../components/ui/Tab';
|
||||
import TabBar from '../components/ui/TabBar';
|
||||
import { SafeAreaProvider } from '../components/ui/SafeArea';
|
||||
|
||||
import { useDrag } from 'react-use-gesture';
|
||||
|
||||
import Notifications from '../components/Notifications';
|
||||
import MenuContent from '../components/MenuContent';
|
||||
|
||||
const CurrentPage = ({ page }) => {
|
||||
const pages = Store.useState(s => s.pages);
|
||||
const pages = Store.useState(selectors.getPages);
|
||||
|
||||
return (
|
||||
<PageStack>
|
||||
@@ -29,37 +30,25 @@ const CurrentPage = ({ page }) => {
|
||||
};
|
||||
|
||||
export default function Index() {
|
||||
const showMenu = Store.useState(s => s.showMenu);
|
||||
const showNotifications = Store.useState(s => s.showNotifications);
|
||||
const currentPage = Store.useState(s => s.currentPage);
|
||||
const pages = Store.useState(s => s.pages);
|
||||
const showMenu = Store.useState(selectors.getMenuOpen);
|
||||
const showNotifications = Store.useState(selectors.getNotificationsOpen);
|
||||
const currentPage = Store.useState(selectors.getCurrentPage);
|
||||
const tabs = Store.useState(selectors.getTabs);
|
||||
|
||||
const closeMenu = () => {
|
||||
Store.update(s => {
|
||||
s.showMenu = false;
|
||||
});
|
||||
};
|
||||
const closeMenu = () => actions.setMenuOpen(false);
|
||||
|
||||
const backdropClose = () => {
|
||||
Store.update(s => {
|
||||
s.showMenu = false;
|
||||
s.showNotifications = false;
|
||||
});
|
||||
actions.setMenuOpen(false);
|
||||
actions.setNotificationsOpen(false);
|
||||
};
|
||||
|
||||
const closeNotifications = () => {
|
||||
Store.update(s => {
|
||||
s.showNotifications = false;
|
||||
});
|
||||
};
|
||||
const closeNotifications = () => actions.setNotificationsOpen(false);
|
||||
|
||||
// To enable edge drag detection to open the side menu
|
||||
const bind = useDrag(
|
||||
({ down, movement: [mx], xy: [x, y], cancel }) => {
|
||||
if (mx > 5 && x < 50 && down) {
|
||||
Store.update(s => {
|
||||
s.showMenu = true;
|
||||
});
|
||||
actions.setMenuOpen(true);
|
||||
cancel();
|
||||
}
|
||||
},
|
||||
@@ -84,15 +73,11 @@ export default function Index() {
|
||||
<Nav page={currentPage} />
|
||||
<CurrentPage page={currentPage} />
|
||||
<TabBar>
|
||||
{pages.map(p => (
|
||||
{tabs.map(p => (
|
||||
<Tab
|
||||
key={p.id}
|
||||
{...p}
|
||||
onClick={() =>
|
||||
Store.update(s => {
|
||||
s.currentPage = p;
|
||||
})
|
||||
}
|
||||
onClick={() => actions.setPage(p)}
|
||||
selected={p.id === currentPage.id}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
import Store from '.';
|
||||
|
||||
export const setPageById = id => {
|
||||
Store.update((s, o) => {
|
||||
s.currentPage = o.pages.find(p => p.id === id);
|
||||
});
|
||||
};
|
||||
|
||||
export const setPage = page => {
|
||||
Store.update((s, o) => {
|
||||
s.currentPage = page;
|
||||
});
|
||||
};
|
||||
|
||||
export const setMenuOpen = open => {
|
||||
Store.update(s => {
|
||||
s.menuOpen = open;
|
||||
});
|
||||
};
|
||||
|
||||
export const setNotificationsOpen = open => {
|
||||
Store.update(s => {
|
||||
s.notificationsOpen = open;
|
||||
});
|
||||
};
|
||||
|
||||
// App-specific actions
|
||||
|
||||
export const setDone = (list, item, done) => {
|
||||
Store.update((s, o) => {
|
||||
const listIndex = o.lists.findIndex(l => l === list);
|
||||
@@ -10,3 +36,9 @@ export const setDone = (list, item, done) => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const setSelectedList = list => {
|
||||
Store.update(s => {
|
||||
s.selectedList = list;
|
||||
});
|
||||
};
|
||||
|
||||
+16
-2
@@ -5,6 +5,7 @@ import { list, listOutline, cog, cogOutline, home, homeOutline } from 'ionicons/
|
||||
import Home from '../components/pages/Home';
|
||||
import Lists from '../components/pages/Lists';
|
||||
import Settings from '../components/pages/Settings';
|
||||
import ListDetail from '../components/pages/ListDetail';
|
||||
|
||||
// The available pages here
|
||||
const pages = [
|
||||
@@ -16,6 +17,13 @@ const pages = [
|
||||
selectedIcon: list,
|
||||
component: Lists,
|
||||
},
|
||||
{
|
||||
id: 'list-detail',
|
||||
title: () => Store.getRawState().selectedList?.name,
|
||||
icon: listOutline,
|
||||
selectedIcon: list,
|
||||
component: ListDetail,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
title: 'Settings',
|
||||
@@ -81,10 +89,16 @@ const lists = [
|
||||
const Store = new PullStateStore({
|
||||
safeAreaTop: 0,
|
||||
safeAreaBottom: 0,
|
||||
showMenu: false,
|
||||
showNotifications: false,
|
||||
menuOpen: false,
|
||||
notificationsOpen: false,
|
||||
currentPage: pages[0],
|
||||
pages,
|
||||
|
||||
// The pages that are linked to tabs
|
||||
tabs: pages.filter(p => ['home', 'lists', 'settings'].indexOf(p.id) >= 0),
|
||||
// The pages that are linked to the menu
|
||||
menuLinks: pages.filter(p => ['home', 'lists', 'settings'].indexOf(p.id) >= 0),
|
||||
|
||||
homeItems,
|
||||
lists,
|
||||
selectedList: null,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const getState = state => state;
|
||||
|
||||
export const getMenuOpen = createSelector(getState, state => state.menuOpen);
|
||||
export const getNotificationsOpen = createSelector(getState, state => state.notificationsOpen);
|
||||
export const getCurrentPage = createSelector(getState, state => state.currentPage);
|
||||
export const getTabs = createSelector(getState, state => state.tabs);
|
||||
export const getMenuLinks = createSelector(getState, state => state.menuLinks);
|
||||
export const getPages = createSelector(getState, state => state.pages);
|
||||
|
||||
// App specific selectors
|
||||
export const getHomeItems = createSelector(getState, state => state.homeItems);
|
||||
export const getLists = createSelector(getState, state => state.lists);
|
||||
export const getSelectedList = createSelector(getState, state => state.selectedList);
|
||||
Reference in New Issue
Block a user