mirror of
https://github.com/Athou/commafeed.git
synced 2026-03-21 21:37:29 +00:00
resizeable tree (#1084)
This commit is contained in:
10
commafeed-client/package-lock.json
generated
10
commafeed-client/package-lock.json
generated
@@ -25,6 +25,7 @@
|
|||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"interweave": "^13.1.0",
|
"interweave": "^13.1.0",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-async-hook": "^4.0.0",
|
"react-async-hook": "^4.0.0",
|
||||||
"react-contexify": "^6.0.0",
|
"react-contexify": "^6.0.0",
|
||||||
@@ -10035,6 +10036,15 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"interweave": "^13.1.0",
|
"interweave": "^13.1.0",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-async-hook": "^4.0.0",
|
"react-async-hook": "^4.0.0",
|
||||||
"react-contexify": "^6.0.0",
|
"react-contexify": "^6.0.0",
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ function Providers(props: { children: React.ReactNode }) {
|
|||||||
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage"))
|
const ApiDocumentationPage = React.lazy(() => import("pages/app/ApiDocumentationPage"))
|
||||||
|
|
||||||
function AppRoutes() {
|
function AppRoutes() {
|
||||||
|
const sidebarWidth = useAppSelector(state => state.tree.sidebarWidth)
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to={`/app/category/${Constants.categories.all.id}`} replace />} />
|
<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="register" element={<RegistrationPage />} />
|
||||||
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
|
<Route path="passwordRecovery" element={<PasswordRecoveryPage />} />
|
||||||
<Route path="api" element={<ApiDocumentationPage />} />
|
<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="category">
|
||||||
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
|
<Route path=":id" element={<FeedEntriesPage sourceType="category" />} />
|
||||||
<Route path=":id/details" element={<CategoryDetailsPage />} />
|
<Route path=":id/details" element={<CategoryDetailsPage />} />
|
||||||
@@ -135,8 +136,11 @@ function FaviconHandler() {
|
|||||||
const root = useAppSelector(state => state.tree.rootCategory)
|
const root = useAppSelector(state => state.tree.rootCategory)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unreadCount = categoryUnreadCount(root)
|
const unreadCount = categoryUnreadCount(root)
|
||||||
if (unreadCount === 0) Tinycon.reset()
|
if (unreadCount === 0) {
|
||||||
else Tinycon.setBubble(unreadCount)
|
Tinycon.reset()
|
||||||
|
} else {
|
||||||
|
Tinycon.setBubble(unreadCount)
|
||||||
|
}
|
||||||
}, [root])
|
}, [root])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ export const Constants = {
|
|||||||
layout: {
|
layout: {
|
||||||
mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
|
mobileBreakpoint: DEFAULT_THEME.breakpoints.md,
|
||||||
headerHeight: 60,
|
headerHeight: 60,
|
||||||
sidebarWidth: 350,
|
|
||||||
entryMaxWidth: 650,
|
entryMaxWidth: 650,
|
||||||
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
|
isTopVisible: (div: HTMLElement) => div.getBoundingClientRect().top >= Constants.layout.headerHeight,
|
||||||
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
|
isBottomVisible: (div: HTMLElement) => div.getBoundingClientRect().bottom <= window.innerHeight,
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import { redirectTo } from "./redirect"
|
|||||||
interface TreeState {
|
interface TreeState {
|
||||||
rootCategory?: Category
|
rootCategory?: Category
|
||||||
mobileMenuOpen: boolean
|
mobileMenuOpen: boolean
|
||||||
|
sidebarWidth: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: TreeState = {
|
const initialState: TreeState = {
|
||||||
mobileMenuOpen: false,
|
mobileMenuOpen: false,
|
||||||
|
sidebarWidth: 350,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reloadTree = createAsyncThunk("tree/reload", () => client.category.getRoot().then(r => r.data))
|
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>) => {
|
setMobileMenuOpen: (state, action: PayloadAction<boolean>) => {
|
||||||
state.mobileMenuOpen = action.payload
|
state.mobileMenuOpen = action.payload
|
||||||
},
|
},
|
||||||
|
setSidebarWidth: (state, action: PayloadAction<number>) => {
|
||||||
|
state.sidebarWidth = action.payload
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: builder => {
|
extraReducers: builder => {
|
||||||
builder.addCase(reloadTree.fulfilled, (state, action) => {
|
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
|
export default treeSlice.reducer
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { useViewportSize } from "@mantine/hooks"
|
import { useViewportSize } from "@mantine/hooks"
|
||||||
import { Constants } from "app/constants"
|
import { Constants } from "app/constants"
|
||||||
import { redirectToAdd, redirectToRootCategory } from "app/slices/redirect"
|
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 { reloadProfile, reloadSettings, reloadTags } from "app/slices/user"
|
||||||
import { useAppDispatch, useAppSelector } from "app/store"
|
import { useAppDispatch, useAppSelector } from "app/store"
|
||||||
import { Loader } from "components/Loader"
|
import { Loader } from "components/Loader"
|
||||||
@@ -26,28 +26,34 @@ import { OnMobile } from "components/responsive/OnMobile"
|
|||||||
import { useAppLoading } from "hooks/useAppLoading"
|
import { useAppLoading } from "hooks/useAppLoading"
|
||||||
import { useWebSocket } from "hooks/useWebSocket"
|
import { useWebSocket } from "hooks/useWebSocket"
|
||||||
import { LoadingPage } from "pages/LoadingPage"
|
import { LoadingPage } from "pages/LoadingPage"
|
||||||
|
import { Resizable } from "re-resizable"
|
||||||
import { ReactNode, Suspense, useEffect } from "react"
|
import { ReactNode, Suspense, useEffect } from "react"
|
||||||
import { TbPlus } from "react-icons/tb"
|
import { TbPlus } from "react-icons/tb"
|
||||||
import { Outlet } from "react-router-dom"
|
import { Outlet } from "react-router-dom"
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
sidebar: ReactNode
|
sidebar: ReactNode
|
||||||
|
sidebarWidth: number
|
||||||
header: ReactNode
|
header: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarPadding = DEFAULT_THEME.spacing.xs
|
const sidebarPadding = DEFAULT_THEME.spacing.xs
|
||||||
const sidebarRightBorderWidth = "1px"
|
const sidebarRightBorderWidth = "1px"
|
||||||
|
|
||||||
const useStyles = createStyles(theme => ({
|
const useStyles = createStyles((theme, props: LayoutProps) => ({
|
||||||
|
sidebarContentResizeWrapper: {
|
||||||
|
padding: sidebarPadding,
|
||||||
|
minHeight: "100vh",
|
||||||
|
},
|
||||||
sidebarContent: {
|
sidebarContent: {
|
||||||
maxWidth: `calc(${Constants.layout.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
|
maxWidth: `calc(${props.sidebarWidth}px - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
|
||||||
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
||||||
maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
|
maxWidth: `calc(100vw - ${sidebarPadding} * 2 - ${sidebarRightBorderWidth})`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mainContentWrapper: {
|
mainContentWrapper: {
|
||||||
paddingTop: Constants.layout.headerHeight,
|
paddingTop: Constants.layout.headerHeight,
|
||||||
paddingLeft: Constants.layout.sidebarWidth,
|
paddingLeft: props.sidebarWidth,
|
||||||
paddingRight: 0,
|
paddingRight: 0,
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
||||||
@@ -55,7 +61,7 @@ const useStyles = createStyles(theme => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mainContent: {
|
mainContent: {
|
||||||
maxWidth: `calc(100vw - ${Constants.layout.sidebarWidth}px)`,
|
maxWidth: `calc(100vw - ${props.sidebarWidth}px)`,
|
||||||
padding: theme.spacing.md,
|
padding: theme.spacing.md,
|
||||||
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
[theme.fn.smallerThan(Constants.layout.mobileBreakpoint)]: {
|
||||||
maxWidth: "100vw",
|
maxWidth: "100vw",
|
||||||
@@ -76,8 +82,8 @@ function LogoAndTitle() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout({ sidebar, header }: LayoutProps) {
|
export default function Layout(props: LayoutProps) {
|
||||||
const { classes } = useStyles()
|
const { classes } = useStyles(props)
|
||||||
const theme = useMantineTheme()
|
const theme = useMantineTheme()
|
||||||
const viewport = useViewportSize()
|
const viewport = useViewportSize()
|
||||||
const { loading } = useAppLoading()
|
const { loading } = useAppLoading()
|
||||||
@@ -85,6 +91,8 @@ export default function Layout({ sidebar, header }: LayoutProps) {
|
|||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
useWebSocket()
|
useWebSocket()
|
||||||
|
|
||||||
|
const handleResize = (element: HTMLElement) => dispatch(setSidebarWidth(element.offsetWidth))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(reloadSettings())
|
dispatch(reloadSettings())
|
||||||
dispatch(reloadProfile())
|
dispatch(reloadProfile())
|
||||||
@@ -122,14 +130,30 @@ export default function Layout({ sidebar, header }: LayoutProps) {
|
|||||||
navbar={
|
navbar={
|
||||||
<Navbar
|
<Navbar
|
||||||
id="sidebar"
|
id="sidebar"
|
||||||
p={sidebarPadding}
|
|
||||||
hiddenBreakpoint={Constants.layout.mobileBreakpoint}
|
hiddenBreakpoint={Constants.layout.mobileBreakpoint}
|
||||||
hidden={!mobileMenuOpen}
|
hidden={!mobileMenuOpen}
|
||||||
width={{ md: Constants.layout.sidebarWidth }}
|
width={{ md: props.sidebarWidth }}
|
||||||
>
|
>
|
||||||
<Navbar.Section grow component={ScrollArea} mx="-xs" px="xs">
|
<Resizable
|
||||||
<Box className={classes.sidebarContent}>{sidebar}</Box>
|
enable={{
|
||||||
</Navbar.Section>
|
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>
|
</Navbar>
|
||||||
}
|
}
|
||||||
header={
|
header={
|
||||||
@@ -147,19 +171,19 @@ export default function Layout({ sidebar, header }: LayoutProps) {
|
|||||||
{!mobileMenuOpen && (
|
{!mobileMenuOpen && (
|
||||||
<Group>
|
<Group>
|
||||||
<Box mr="sm">{burger}</Box>
|
<Box mr="sm">{burger}</Box>
|
||||||
<Box sx={{ flexGrow: 1 }}>{header}</Box>
|
<Box sx={{ flexGrow: 1 }}>{props.header}</Box>
|
||||||
</Group>
|
</Group>
|
||||||
)}
|
)}
|
||||||
</OnMobile>
|
</OnMobile>
|
||||||
<OnDesktop>
|
<OnDesktop>
|
||||||
<Group>
|
<Group>
|
||||||
<Group position="apart" sx={{ width: Constants.layout.sidebarWidth - 16 }}>
|
<Group position="apart" sx={{ width: props.sidebarWidth - 16 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<LogoAndTitle />
|
<LogoAndTitle />
|
||||||
</Box>
|
</Box>
|
||||||
<Box>{addButton}</Box>
|
<Box>{addButton}</Box>
|
||||||
</Group>
|
</Group>
|
||||||
<Box sx={{ flexGrow: 1 }}>{header}</Box>
|
<Box sx={{ flexGrow: 1 }}>{props.header}</Box>
|
||||||
</Group>
|
</Group>
|
||||||
</OnDesktop>
|
</OnDesktop>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
Reference in New Issue
Block a user