feat: more usable ui for mobile and tablet

This commit is contained in:
thecodrr
2021-07-17 08:50:18 +05:00
parent 36da5fddff
commit 9fa9373fa8
6 changed files with 223 additions and 127 deletions

View File

@@ -10,13 +10,14 @@ import { CHECK_IDS, EV, EVENTS } from "notes-core/common";
import { registerKeyMap } from "./common/key-map";
import { isUserPremium } from "./hooks/use-is-user-premium";
function AppEffects({ isMobile, isTablet, setShow }) {
function AppEffects({ isMobile, isTablet, setShow, slideToIndex }) {
const refreshColors = useStore((store) => store.refreshColors);
const refreshMenuPins = useStore((store) => store.refreshMenuPins);
const updateLastSynced = useStore((store) => store.updateLastSynced);
const isFocusMode = useStore((store) => store.isFocusMode);
const isEditorOpen = useStore((store) => store.isEditorOpen);
const toggleSideMenu = useStore((store) => store.toggleSideMenu);
const isSideMenuOpen = useStore((store) => store.isSideMenuOpen);
const addReminder = useStore((store) => store.addReminder);
const initUser = useUserStore((store) => store.init);
const initNotes = useNotesStore((store) => store.init);
@@ -76,21 +77,10 @@ function AppEffects({ isMobile, isTablet, setShow }) {
useEffect(() => {
if (isFocusMode) {
setShow(false);
} else {
if (!isTablet) setShow(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFocusMode]);
useEffect(() => {
if (!isMobile && !isTablet) return;
setShow(!isEditorOpen);
//setIsEditorOpen(!show);
// if (isTablet) toggleSideMenu(!isEditorOpen);
// if (!isEditorOpen && !isTablet && !isMobile) toggleSideMenu(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isEditorOpen, isMobile, isTablet]);
useEffect(() => {
introduceFeatures();
return () => {
@@ -98,6 +88,16 @@ function AppEffects({ isMobile, isTablet, setShow }) {
};
}, []);
useEffect(() => {
if (!isMobile) return;
slideToIndex(isSideMenuOpen ? 0 : 1);
}, [isMobile, slideToIndex, isSideMenuOpen]);
useEffect(() => {
if (!isMobile) return;
slideToIndex(isEditorOpen ? 2 : 1);
}, [isMobile, slideToIndex, isEditorOpen]);
useEffect(() => {
toggleSideMenu(!isMobile);
if (!isMobile && !isTablet && !isFocusMode) setShow(true);

View File

@@ -1,21 +1,21 @@
import React, { useState, useEffect } from "react";
import "./app.css";
import { Flex } from "rebass";
import { Box, Flex } from "rebass";
import { MotionConfig, AnimationFeature, GesturesFeature } from "framer-motion";
import ThemeProvider from "./components/theme-provider";
import Banner from "./components/banner";
import StatusBar from "./components/statusbar";
import Animated from "./components/animated";
import NavigationMenu from "./components/navigationmenu";
import GlobalMenuWrapper from "./components/globalmenuwrapper";
import { getCurrentPath, NavigationEvents } from "./navigation";
import rootroutes from "./navigation/rootroutes";
import { useStore as useEditorStore } from "./stores/editor-store";
import { useStore } from "./stores/app-store";
import { Suspense } from "react";
import useMobile from "./utils/use-mobile";
import useTablet from "./utils/use-tablet";
import HashRouter from "./components/hashrouter";
import ThemeTransition from "./components/themeprovider/themetransition";
import useSlider from "./hooks/use-slider";
const AppEffects = React.lazy(() => import("./app-effects"));
const CachedRouter = React.lazy(() => import("./components/cached-router"));
@@ -25,7 +25,28 @@ function App() {
const isMobile = useMobile();
const isTablet = useTablet();
const [isAppLoaded, setIsAppLoaded] = useState(false);
const clearSession = useEditorStore((store) => store.clearSession);
const toggleSideMenu = useStore((store) => store.toggleSideMenu);
const setIsEditorOpen = useStore((store) => store.setIsEditorOpen);
const [sliderRef, slideToIndex] = useSlider({
onSliding: (e, { lastSlide, position, lastPosition }) => {
if (!isMobile) return;
const offset = 70;
const width = 180;
const percent = offset - (position / width) * offset;
if (percent >= 0) {
const overlay = document.getElementById("overlay");
overlay.style.opacity = `${percent}%`;
overlay.style.pointerEvents =
Math.round(percent) === offset ? "all" : "none";
}
},
onChange: (e, { slide, lastSlide }) => {
if (!lastSlide || !isMobile) return;
toggleSideMenu(slide?.index === 0 ? true : false);
setIsEditorOpen(slide?.index === 2 ? true : false);
},
});
useEffect(() => {
function onNavigate() {
@@ -41,6 +62,7 @@ function App() {
{isAppLoaded && (
<Suspense fallback={<div style={{ display: "none" }} />}>
<AppEffects
slideToIndex={slideToIndex}
setShow={setShow}
isMobile={isMobile}
isTablet={isTablet}
@@ -56,42 +78,91 @@ function App() {
height="100%"
sx={{ overflow: "hidden" }}
>
<Flex flex={1} sx={{ overflow: "hidden" }}>
<NavigationMenu
toggleNavigationContainer={(state) => {
if (isMobile || isTablet) {
clearSession();
} else setShow(state || !show);
<Flex
ref={sliderRef}
variant="rowFill"
overflowX={["auto", "hidden"]}
sx={{
scrollSnapType: "x mandatory",
scrollBehavior: "smooth",
WebkitOverflowScrolling: "touch",
}}
>
<Flex
flexShrink={0}
sx={{
scrollSnapAlign: "start",
}}
/>
<Flex variant="rowFill">
<Animated.Flex
className="listMenu"
variant="columnFill"
initial={{ width: "30%", opacity: 1, x: 0 }}
animate={{
width: show ? "30%" : "0%",
x: show ? 0 : "-30%",
opacity: show ? 1 : 0,
flexDirection="column"
>
<NavigationMenu
toggleNavigationContainer={(state) => {
if (!isMobile) setShow(state || !show);
}}
transition={{ duration: 0.2, ease: "easeOut" }}
sx={{
borderRight: "1px solid",
borderColor: "border",
borderRightWidth: show ? 1 : 0,
}}
>
{isMobile && <Banner />}
<Suspense fallback={<div />}>
<CachedRouter />
</Suspense>
</Animated.Flex>
<Flex
width={[show ? 0 : "100%", show ? 0 : "100%", "100%"]}
flexDirection="column"
>
<HashRouter />
</Flex>
/>
</Flex>
<Animated.Flex
className="listMenu"
variant="columnFill"
initial={{
width: isMobile ? "100vw" : isTablet ? "40%" : "25%",
opacity: 1,
x: 0,
}}
animate={{
width: show
? isMobile
? "100vw"
: isTablet
? "40%"
: "25%"
: "0%",
x: show ? 0 : isTablet ? "-40%" : "-25%",
opacity: show ? 1 : 0,
}}
transition={{ duration: 0.2, ease: "easeOut" }}
sx={{
borderRight: "1px solid",
borderColor: "border",
borderRightWidth: show ? 1 : 0,
position: "relative",
scrollSnapAlign: "start",
}}
flexShrink={0}
>
<Suspense fallback={<div />}>
<CachedRouter />
</Suspense>
{isMobile && (
<Box
id="overlay"
sx={{
position: "absolute",
width: "100%",
height: "100%",
top: 0,
left: 0,
zIndex: 999,
opacity: 0,
visibility: "visible",
pointerEvents: "none",
}}
bg="black"
onClick={() => {
toggleSideMenu(false);
}}
/>
)}
</Animated.Flex>
<Flex
width={["100vw", "100%"]}
flexShrink={[0, 1]}
sx={{
scrollSnapAlign: "start",
}}
flexDirection="column"
>
<HashRouter />
</Flex>
</Flex>
<StatusBar />

View File

@@ -1,6 +1,5 @@
import React from "react";
import ReactDOM from "react-dom";
import { Flex } from "rebass";
import { Box, Flex } from "rebass";
import { useStore as useAppStore } from "../../stores/app-store";
import * as Icon from "../icons";
import { useStore as useUserStore } from "../../stores/user-store";
@@ -12,7 +11,6 @@ import { toTitleCase } from "../../utils/string";
import { COLORS } from "../../common";
import { db } from "../../common/db";
import useMobile from "../../utils/use-mobile";
import useTablet from "../../utils/use-tablet";
import { useLocation } from "wouter";
function shouldSelectNavItem(route, pin) {
@@ -56,6 +54,7 @@ const bottomRoutes = [
];
const NAVIGATION_MENU_WIDTH = "10em";
const NAVIGATION_MENU_TABLET_WIDTH = "4em";
function NavigationMenu(props) {
const { toggleNavigationContainer } = props;
@@ -63,68 +62,48 @@ function NavigationMenu(props) {
const isFocusMode = useAppStore((store) => store.isFocusMode);
const colors = useAppStore((store) => store.colors);
const pins = useAppStore((store) => store.menuPins);
const isSideMenuOpen = useAppStore((store) => store.isSideMenuOpen);
const refreshMenuPins = useAppStore((store) => store.refreshMenuPins);
const toggleSideMenu = useAppStore((store) => store.toggleSideMenu);
const isSyncing = useAppStore((store) => store.isSyncing);
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
//const logout = useUserStore((store) => store.logout);
const sync = useAppStore((store) => store.sync);
const theme = useThemeStore((store) => store.theme);
const toggleNightMode = useThemeStore((store) => store.toggleNightMode);
const isMobile = useMobile();
const isTablet = useTablet();
return (
<Animated.Flex
id="navigationmenu"
flexDirection="column"
justifyContent="space-between"
flex={1}
initial={{
opacity: 1,
x: isMobile ? -500 : 0,
}}
animate={{
opacity: isFocusMode ? 0 : 1,
visibility: isFocusMode ? "collapse" : "visible",
x: isMobile ? (isSideMenuOpen ? 0 : -500) : 0,
}}
transition={{ duration: 0.3, ease: "easeOut" }}
sx={{
borderRight: "1px solid",
borderRightColor: "border",
minWidth: [
"85%",
isSideMenuOpen && !isFocusMode ? NAVIGATION_MENU_WIDTH : 0,
NAVIGATION_MENU_WIDTH,
isFocusMode ? 0 : NAVIGATION_MENU_TABLET_WIDTH,
isFocusMode ? 0 : NAVIGATION_MENU_WIDTH,
],
maxWidth: [
"85%",
isSideMenuOpen && !isFocusMode ? NAVIGATION_MENU_WIDTH : 0,
NAVIGATION_MENU_WIDTH,
isFocusMode ? 0 : NAVIGATION_MENU_TABLET_WIDTH,
isFocusMode ? 0 : NAVIGATION_MENU_WIDTH,
],
zIndex: !isSideMenuOpen ? -1 : isMobile ? 999 : isTablet ? 1 : 1,
height: ["100%", "auto", "auto"],
position: ["absolute", "relative", "relative"],
zIndex: 1,
height: "auto",
position: "relative",
}}
bg={"bgSecondary"}
px={0}
>
{isMobile &&
isSideMenuOpen &&
ReactDOM.createPortal(
<Flex
sx={{
position: "absolute",
height: "100%",
width: "100%",
zIndex: 998,
}}
bg="overlay"
onClick={() => toggleSideMenu()}
/>,
document.getElementById("app")
)}
<Flex
flexDirection="column"
sx={{
@@ -146,7 +125,7 @@ function NavigationMenu(props) {
: location.startsWith(item.path)
}
onClick={() => {
if (!isMobile && !isTablet && location === item.path)
if (!isMobile && location === item.path)
return toggleNavigationContainer();
toggleNavigationContainer(true);
navigate(item.path);
@@ -165,48 +144,44 @@ function NavigationMenu(props) {
}}
/>
))}
<Flex
flexDirection="column"
sx={{ borderTop: "1px solid", borderTopColor: "border" }}
>
{pins.map((pin) => (
<NavigationItem
key={pin.id}
title={pin.title}
menu={{
items: [
{
key: "removeshortcut",
title: () => "Remove shortcut",
onClick: async ({ pin }) => {
await db.settings.unpin(pin.id);
refreshMenuPins();
},
<Box width="85%" height="0.8px" bg="border" alignSelf="center" my={1} />
{pins.map((pin) => (
<NavigationItem
key={pin.id}
title={pin.title}
menu={{
items: [
{
key: "removeshortcut",
title: () => "Remove shortcut",
onClick: async ({ pin }) => {
await db.settings.unpin(pin.id);
refreshMenuPins();
},
],
extraData: { pin },
}}
icon={
pin.type === "notebook"
? Icon.Notebook2
: pin.type === "tag"
? Icon.Tag2
: Icon.Topic
},
],
extraData: { pin },
}}
icon={
pin.type === "notebook"
? Icon.Notebook2
: pin.type === "tag"
? Icon.Tag2
: Icon.Topic
}
isShortcut
selected={shouldSelectNavItem(location, pin)}
onClick={() => {
if (pin.type === "notebook") {
navigate(`/notebooks/${pin.id}`);
} else if (pin.type === "topic") {
navigate(`/notebooks/${pin.notebookId}/${pin.id}`);
} else if (pin.type === "tag") {
navigate(`/tags/${pin.id}`);
}
isShortcut
selected={shouldSelectNavItem(location, pin)}
onClick={() => {
if (pin.type === "notebook") {
navigate(`/notebooks/${pin.id}`);
} else if (pin.type === "topic") {
navigate(`/notebooks/${pin.notebookId}/${pin.id}`);
} else if (pin.type === "tag") {
navigate(`/tags/${pin.id}`);
}
}}
/>
))}
</Flex>
}}
/>
))}
</Flex>
<Flex flexDirection="column">
{theme === "light" ? (

View File

@@ -0,0 +1,53 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
export default function useSlider({ initialIndex, onSliding, onChange }) {
const ref = useRef();
const slides = useMemo(() => [], []);
useEffect(() => {
if (!ref.current) return;
const slider = ref.current;
let lastSlide = null;
let lastPosition = 0;
let last = 0;
if (!slides.length) {
for (let node of slider.childNodes) {
slides.push({
index: slides.length,
node,
offset: last,
width: node.scrollWidth,
});
last += node.scrollWidth;
}
}
function onScroll(e) {
const position = e.target.scrollLeft;
if (onSliding) onSliding(e, { lastSlide, lastPosition, position });
const slide = slides.find(
(slide) => slide.offset === Math.round(position)
);
if (onChange && slide && lastSlide !== slide) {
onChange(e, { position, slide, lastSlide });
lastSlide = slide;
}
lastPosition = position;
}
slider.onscroll = onScroll;
return () => {
slider.onscroll = null;
};
}, [ref, slides, initialIndex, onSliding, onChange]);
const slideToIndex = useCallback(
(index) => {
if (!ref.current || index >= slides.length) return;
slides[index].node.scrollIntoView({ behavior: "smooth" });
},
[ref, slides]
);
return [ref, slideToIndex];
}

View File

@@ -16,7 +16,7 @@ import DiffViewer from "../components/diff-viewer";
import Unlock from "../components/unlock";
import { store as appStore } from "../stores/app-store";
import { store as editorStore } from "../stores/editor-store";
import { isMobile, isTablet } from "../utils/dimensions";
import { isMobile } from "../utils/dimensions";
import {
showEditTopicDialog,
showTopicDialog,
@@ -30,8 +30,6 @@ const Editor = React.lazy(() => import("../components/editor"));
const hashroutes = {
"/": () => {
closeOpenedDialog();
if (isMobile() || isTablet()) editorStore.clearSession(false);
return !editorStore.get().session.state && <EditorPlaceholder />;
},
"/email/verify": () => {

View File

@@ -5,13 +5,12 @@ import { store as notebookStore } from "./notebook-store";
import { store as trashStore } from "./trash-store";
import { store as tagStore } from "./tag-store";
import BaseStore from "./index";
import { isMobile } from "../utils/dimensions";
import { showToast } from "../utils/toast";
import { resetReminders } from "../common/reminders";
class AppStore extends BaseStore {
// default state
isSideMenuOpen = !isMobile();
isSideMenuOpen = false;
isSyncing = false;
isFocusMode = false;
isEditorOpen = false;