mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
feat: more usable ui for mobile and tablet
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
53
apps/web/src/hooks/use-slider.js
Normal file
53
apps/web/src/hooks/use-slider.js
Normal 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];
|
||||
}
|
||||
@@ -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": () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user