resizeable tree (#1084)

This commit is contained in:
Athou
2023-06-16 21:24:34 +02:00
parent 6944d4dc0b
commit d1ddcb6ace
6 changed files with 63 additions and 20 deletions

View File

@@ -25,6 +25,7 @@
"dayjs": "^1.11.7",
"interweave": "^13.1.0",
"mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0",
@@ -10035,6 +10036,15 @@
"node": ">=0.10.0"
}
},
"node_modules/re-resizable": {
"version": "6.9.9",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.9.tgz",
"integrity": "sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA==",
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",

View File

@@ -31,6 +31,7 @@
"dayjs": "^1.11.7",
"interweave": "^13.1.0",
"mousetrap": "^1.6.5",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-async-hook": "^4.0.0",
"react-contexify": "^6.0.0",

View File

@@ -66,6 +66,7 @@ function Providers(props: { children: React.ReactNode }) {
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage"))
function AppRoutes() {
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)
return (
<Routes>
<Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
@@ -74,7 +75,7 @@ function AppRoutes() {
<Route path="register" element={<RegistrationPage />} />
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
<Route path="api" element={<ApiDocumentationPage />} />
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} />}>
<Route path="app" element={<Layout header={<Header />} sidebar={<Tree />} sidebarWidth={sidebarWidth} />}>
<Route path="category">
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
<Route path=":id/details" element={<CategoryDetailsPage />} />
@@ -135,8 +136,11 @@ function FaviconHandler() {
const root = useAppSelector(state => state.tree.rootCategory)
useEffect(() => {
const unreadCount = categoryUnreadCount(root)
if (unreadCount === 0) Tinycon.reset()
else Tinycon.setBubble(unreadCount)
if (unreadCount === 0) {
Tinycon.reset()
} else {
Tinycon.setBubble(unreadCount)
}
}, [root])
return null

View File

@@ -88,7 +88,6 @@ export const Constants = {
layout: {
mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
headerHeight: 60,
sidebarWidth: 350,
entryMaxWidth: 650,
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,

View File

@@ -9,10 +9,12 @@ import { redirectTo } from "./redirect"
interface TreeState {
rootCategory?: Category
mobileMenuOpen: boolean
sidebarWidth: number
}
const initialState: TreeState = {
mobileMenuOpen: false,
sidebarWidth: 350,
}
export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
@@ -27,6 +29,9 @@ export const treeSlice = createSlice({
setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
state.mobileMenuOpen = action.payload
},
setSidebarWidth: (state, action: PayloadAction<number>) => {
state.sidebarWidth = action.payload
},
},
extraReducers: builder => {
builder.addCase(reloadTree.fulfilled, (state, action) => {
@@ -54,5 +59,5 @@ export const treeSlice = createSlice({
},
})
export const { setMobileMenuOpen } = treeSlice.actions
export const { setMobileMenuOpen, setSidebarWidth } = treeSlice.actions
export default treeSlice.reducer

View File

@@ -16,7 +16,7 @@ import {
import { useViewportSize } from "@mantine/hooks"
import { Constants } from "app/constants"
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect"
import { reloadTree, setMobileMenuOpen } from "app/slices/tree"
import { reloadTree, setMobileMenuOpen, setSidebarWidth } from "app/slices/tree"
import { reloadProfile, reloadSettings, reloadTags } from "app/slices/user"
import { useAppDispatch, useAppSelector } from "app/store"
import { Loader } from "components/Loader"
@@ -26,28 +26,34 @@ import { OnMobile } from "components/responsive/OnMobile"
import { useAppLoading } from "hooks/useAppLoading"
import { useWebSocket } from "hooks/useWebSocket"
import { LoadingPage } from "pages/LoadingPage"
import { Resizable } from "re-resizable"
import { ReactNode, Suspense, useEffect } from "react"
import { TbPlus } from "react-icons/tb"
import { Outlet } from "react-router-dom"
interface LayoutProps {
sidebar: ReactNode
sidebarWidth: number
header: ReactNode
}
const sidebarPadding = DEFAULT_THEME.spacing.xs
const sidebarRightBorderWidth = "1px"
const useStyles = createStyles(theme => ({
const useStyles = createStyles((theme, props: LayoutProps) => ({
sidebarContentResizeWrapper: {
padding: sidebarPadding,
minHeight: "100vh",
},
sidebarContent: {
maxWidth: `calc(${Constants.layout.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
maxWidth: `calc(${props.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
},
},
mainContentWrapper: {
paddingTop: Constants.layout.headerHeight,
paddingLeft: Constants.layout.sidebarWidth,
paddingLeft: props.sidebarWidth,
paddingRight: 0,
paddingBottom: 0,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
@@ -55,7 +61,7 @@ const useStyles = createStyles(theme => ({
},
},
mainContent: {
maxWidth: `calc(100vw - ${Constants.layout.sidebarWidth}px)`,
maxWidth: `calc(100vw - ${props.sidebarWidth}px)`,
padding: theme.spacing.md,
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
maxWidth: "100vw",
@@ -76,8 +82,8 @@ function LogoAndTitle() {
)
}
export default function Layout({ sidebar, header }: LayoutProps) {
const { classes } = useStyles()
export default function Layout(props: LayoutProps) {
const { classes } = useStyles(props)
const theme = useMantineTheme()
const viewport = useViewportSize()
const { loading } = useAppLoading()
@@ -85,6 +91,8 @@ export default function Layout({ sidebar, header }: LayoutProps) {
const dispatch = useAppDispatch()
useWebSocket()
const handleResize = (element: HTMLElement) => dispatch(setSidebarWidth(element.offsetWidth))
useEffect(() => {
dispatch(reloadSettings())
dispatch(reloadProfile())
@@ -122,14 +130,30 @@ export default function Layout({ sidebar, header }: LayoutProps) {
navbar={
<Navbar
id="sidebar"
p={sidebarPadding}
hiddenBreakpoint={Constants.layout.mobileBreakpoint}
hidden={!mobileMenuOpen}
width={{ md: Constants.layout.sidebarWidth }}
width={{ md: props.sidebarWidth }}
>
<Navbar.Section grow component={ScrollArea} mx="-xs" px="xs">
<Box className={classes.sidebarContent}>{sidebar}</Box>
</Navbar.Section>
<Resizable
enable={{
top: false,
right: true,
bottom: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
}}
onResize={(e, dir, el) => handleResize(el)}
minWidth={120}
>
<Box className={classes.sidebarContentResizeWrapper}>
<Navbar.Section grow component={ScrollArea} mx="-xs" px="xs">
<Box className={classes.sidebarContent}>{props.sidebar}</Box>
</Navbar.Section>
</Box>
</Resizable>
</Navbar>
}
header={
@@ -147,19 +171,19 @@ export default function Layout({ sidebar, header }: LayoutProps) {
{!mobileMenuOpen && (
<Group>
<Box mr="sm">{burger}</Box>
<Box sx={{ flexGrow: 1 }}>{header}</Box>
<Box sx={{ flexGrow: 1 }}>{props.header}</Box>
</Group>
)}
</OnMobile>
<OnDesktop>
<Group>
<Group position="apart" sx={{ width: Constants.layout.sidebarWidth - 16 }}>
<Group position="apart" sx={{ width: props.sidebarWidth - 16 }}>
<Box>
<LogoAndTitle />
</Box>
<Box>{addButton}</Box>
</Group>
<Box sx={{ flexGrow: 1 }}>{header}</Box>
<Box sx={{ flexGrow: 1 }}>{props.header}</Box>
</Group>
</OnDesktop>
</Header>