refactor layout

* ui polishing
* frontend and backend fixes
This commit is contained in:
Philipinho
2024-05-31 21:51:44 +01:00
parent 046dd6d150
commit 06d854a7d2
95 changed files with 1548 additions and 821 deletions

View File

@ -2,10 +2,6 @@ import { ActionIcon, rem } from "@mantine/core";
import React from "react";
import { IconUsersGroup } from "@tabler/icons-react";
interface IconPeopleCircleProps extends React.ComponentPropsWithoutRef<"svg"> {
size?: number | string;
}
export function IconGroupCircle() {
return (
<ActionIcon variant="light" size="lg" color="gray" radius="xl">

View File

@ -1,6 +0,0 @@
.breadcrumb {
a {
color: var(--mantine-color-default-color);
text-overflow: ellipsis;
}
}

View File

@ -1,133 +0,0 @@
import { useAtomValue } from "jotai";
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
import React, { useEffect, useState } from "react";
import { findBreadcrumbPath } from "@/features/page/tree/utils";
import {
Button,
Anchor,
Popover,
Breadcrumbs,
ActionIcon,
Text,
} from "@mantine/core";
import { IconDots } from "@tabler/icons-react";
import { Link, useParams } from "react-router-dom";
import classes from "./breadcrumb.module.css";
import { SpaceTreeNode } from "@/features/page/tree/types.ts";
import { buildPageSlug } from "@/features/page/page.utils.ts";
import { usePageQuery } from "@/features/page/queries/page-query.ts";
function getTitle(name: string, icon: string) {
if (icon) {
return `${icon} ${name}`;
}
return name;
}
export default function Breadcrumb() {
const treeData = useAtomValue(treeDataAtom);
const [breadcrumbNodes, setBreadcrumbNodes] = useState<
SpaceTreeNode[] | null
>(null);
const { slugId } = useParams();
const { data: currentPage } = usePageQuery(slugId);
useEffect(() => {
if (treeData?.length > 0 && currentPage) {
const breadcrumb = findBreadcrumbPath(treeData, currentPage.id);
if (breadcrumb) {
setBreadcrumbNodes(breadcrumb);
}
}
}, [currentPage?.id, treeData]);
const HiddenNodesTooltipContent = () =>
breadcrumbNodes?.slice(1, -2).map((node) => (
<Button.Group orientation="vertical" key={node.id}>
<Button
justify="start"
component={Link}
to={buildPageSlug(node.slugId, node.name)}
variant="default"
style={{ border: "none" }}
>
<Text truncate="end">{getTitle(node.name, node.icon)}</Text>
</Button>
</Button.Group>
));
const getLastNthNode = (n: number) =>
breadcrumbNodes && breadcrumbNodes[breadcrumbNodes.length - n];
const getBreadcrumbItems = () => {
if (breadcrumbNodes?.length > 3) {
return [
<Anchor
component={Link}
to={buildPageSlug(breadcrumbNodes[0].slugId, breadcrumbNodes[0].name)}
underline="never"
key={breadcrumbNodes[0].slugId}
>
{getTitle(breadcrumbNodes[0].name, breadcrumbNodes[0].icon)}
</Anchor>,
<Popover
width={250}
position="bottom"
withArrow
shadow="xl"
key="hidden-nodes"
>
<Popover.Target>
<ActionIcon c="gray" variant="transparent">
<IconDots size={20} stroke={2} />
</ActionIcon>
</Popover.Target>
<Popover.Dropdown>
<HiddenNodesTooltipContent />
</Popover.Dropdown>
</Popover>,
<Anchor
component={Link}
to={buildPageSlug(getLastNthNode(2)?.slugId, getLastNthNode(2)?.name)}
underline="never"
key={getLastNthNode(2)?.slugId}
>
{getTitle(getLastNthNode(2)?.name, getLastNthNode(2)?.icon)}
</Anchor>,
<Anchor
component={Link}
to={buildPageSlug(getLastNthNode(1)?.slugId, getLastNthNode(1)?.name)}
underline="never"
key={getLastNthNode(1)?.slugId}
>
{getTitle(getLastNthNode(1)?.name, getLastNthNode(1)?.icon)}
</Anchor>,
];
}
if (breadcrumbNodes) {
return breadcrumbNodes.map((node) => (
<Anchor
component={Link}
to={buildPageSlug(node.slugId, node.name)}
underline="never"
key={node.id}
>
{getTitle(node.name, node.icon)}
</Anchor>
));
}
return [];
};
return (
<div className={classes.breadcrumb}>
{breadcrumbNodes ? (
<Breadcrumbs>{getBreadcrumbItems()}</Breadcrumbs>
) : (
<></>
)}
</div>
);
}

View File

@ -1,17 +0,0 @@
import { UserProvider } from "@/features/user/user-provider.tsx";
import Shell from "./shell.tsx";
import { Outlet } from "react-router-dom";
import { Helmet } from "react-helmet-async";
export default function DashboardLayout() {
return (
<UserProvider>
<Shell>
<Helmet>
<title>Home</title>
</Helmet>
<Outlet />
</Shell>
</UserProvider>
);
}

View File

@ -1,100 +0,0 @@
import { ActionIcon, Menu, Tooltip } from "@mantine/core";
import {
IconDots,
IconHistory,
IconLink,
IconMessage,
} from "@tabler/icons-react";
import React from "react";
import useToggleAside from "@/hooks/use-toggle-aside.tsx";
import { useAtom } from "jotai";
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
import { useClipboard } from "@mantine/hooks";
import { useParams } from "react-router-dom";
import { usePageQuery } from "@/features/page/queries/page-query.ts";
import { buildPageSlug } from "@/features/page/page.utils.ts";
import { notifications } from "@mantine/notifications";
export default function Header() {
const toggleAside = useToggleAside();
return (
<>
{/*
<Button variant="default" style={{ border: "none" }} size="compact-sm">
Share
</Button>
*/}
<Tooltip label="Comments" openDelay={250} withArrow>
<ActionIcon
variant="default"
style={{ border: "none" }}
onClick={() => toggleAside("comments")}
>
<IconMessage size={20} stroke={2} />
</ActionIcon>
</Tooltip>
<PageActionMenu />
</>
);
}
function PageActionMenu() {
const [, setHistoryModalOpen] = useAtom(historyAtoms);
const clipboard = useClipboard({ timeout: 500 });
const { slugId } = useParams();
const { data: page, isLoading, isError } = usePageQuery(slugId);
const handleCopyLink = () => {
const pageLink =
window.location.host + buildPageSlug(page.slugId, page.title);
clipboard.copy(pageLink);
notifications.show({ message: "Link copied" });
};
const openHistoryModal = () => {
setHistoryModalOpen(true);
};
return (
<Menu
shadow="xl"
position="bottom-end"
offset={20}
width={200}
withArrow
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="default" style={{ border: "none" }}>
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconLink size={16} stroke={2} />}
onClick={handleCopyLink}
>
Copy link
</Menu.Item>
<Menu.Divider />
<Menu.Item
leftSection={<IconHistory size={16} stroke={2} />}
onClick={openHistoryModal}
>
Page history
</Menu.Item>
{/*
<Menu.Divider />
<Menu.Item leftSection={<IconTrash size={16} stroke={2} />}>
Delete
</Menu.Item>
*/}
</Menu.Dropdown>
</Menu>
);
}

View File

@ -1,38 +0,0 @@
.header,
.footer {
@media (max-width: 992px) {
[data-layout="alt"] & {
--_section-right: var(--app-shell-aside-offset, 0px);
}
}
}
.aside {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
@media (min-width: 993px) {
[data-layout="alt"] & {
--_section-top: var(--_section-top, var(--app-shell-header-offset, 0px));
--_section-height: var(
--_section-height,
calc(
100dvh - var(--app-shell-header-offset, 0px) -
var(--app-shell-footer-offset, 0px)
)
);
}
}
}
@media (max-width: 48em) {
.aside {
width: 350px;
}
}
@media (max-width: 48em) {
.navbar {
width: 300px;
}
}

View File

@ -1,88 +0,0 @@
import {
asideStateAtom,
desktopSidebarAtom,
} from "@/components/navbar/atoms/sidebar-atom.ts";
import { useToggleSidebar } from "@/components/navbar/hooks/use-toggle-sidebar.ts";
import { Navbar } from "@/components/navbar/navbar.tsx";
import { AppShell, Burger, Group } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useAtom } from "jotai";
import classes from "./shell.module.css";
import Header from "@/components/layouts/dashboard/header.tsx";
import Breadcrumb from "@/components/layouts/components/breadcrumb.tsx";
import Aside from "@/components/layouts/dashboard/aside.tsx";
import { useMatchPath } from "@/hooks/use-match-path.tsx";
import React from "react";
export default function Shell({ children }: { children: React.ReactNode }) {
const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] =
useDisclosure();
const [desktopOpened] = useAtom(desktopSidebarAtom);
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
const matchPath = useMatchPath();
const isPageRoute = matchPath("/p/:pageId");
const [{ isAsideOpen }] = useAtom(asideStateAtom);
return (
<AppShell
layout="alt"
header={{ height: 45 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
aside={{
width: 350,
breakpoint: "sm",
collapsed: { mobile: !isAsideOpen, desktop: !isAsideOpen },
}}
padding="md"
>
<AppShell.Header className={classes.header} withBorder={false}>
<Group justify="space-between" h="100%" px="md" wrap="nowrap">
<Group
h="100%"
maw="60%"
px="md"
wrap="nowrap"
style={{ overflow: "hidden" }}
>
<Burger
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Burger
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
{isPageRoute && <Breadcrumb />}
</Group>
{isPageRoute && (
<Group justify="flex-end" h="100%" px="md" wrap="nowrap">
<Header />
</Group>
)}
</Group>
</AppShell.Header>
<AppShell.Navbar className={classes.navbar} withBorder={false}>
<Navbar />
</AppShell.Navbar>
<AppShell.Main>{children}</AppShell.Main>
{isPageRoute && (
<AppShell.Aside className={classes.aside} withBorder={false}>
<Aside />
</AppShell.Aside>
)}
</AppShell>
);
}

View File

@ -0,0 +1,23 @@
.header {
height: 100%;
margin-bottom: rem(120px);
background-color: var(--mantine-color-body);
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
padding-left: var(--mantine-spacing-md);
padding-right: var(--mantine-spacing-md);
}
.link {
display: block;
line-height: 1;
padding: rem(8px) rem(12px);
border-radius: var(--mantine-radius-sm);
text-decoration: none;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
font-size: var(--mantine-font-size-sm);
font-weight: 500;
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}
}

View File

@ -0,0 +1,70 @@
import { Group } from "@mantine/core";
import { IconSquareLetterDFilled } from "@tabler/icons-react";
import classes from "./app-header.module.css";
import React from "react";
import TopMenu from "@/components/layouts/global/top-menu.tsx";
import { Link } from "react-router-dom";
import APP_ROUTE from "@/lib/app-route.ts";
import { useAtom } from "jotai/index";
import {
desktopSidebarAtom,
mobileSidebarAtom,
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import SidebarToggle from "@/components/ui/sidebar-toggle-button.tsx";
const links = [{ link: APP_ROUTE.HOME, label: "Home" }];
export function AppHeader() {
const [mobileOpened] = useAtom(mobileSidebarAtom);
const toggleMobile = useToggleSidebar(mobileSidebarAtom);
const [desktopOpened] = useAtom(desktopSidebarAtom);
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
const isHomeRoute = location.pathname.startsWith("/home");
const items = links.map((link) => (
<Link key={link.label} to={link.link} className={classes.link}>
{link.label}
</Link>
));
return (
<>
<Group h="100%" px="md" justify="space-between" wrap={"nowrap"}>
<Group>
<IconSquareLetterDFilled size={30} />
{!isHomeRoute && (
<>
<SidebarToggle
aria-label="sidebar toggle"
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<SidebarToggle
aria-label="sidebar toggle"
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
</>
)}
<Group ml={50} gap={5} className={classes.links} visibleFrom="sm">
{items}
</Group>
</Group>
<Group px={"xl"}>
<TopMenu />
</Group>
</Group>
</>
);
}

View File

@ -0,0 +1,16 @@
.header, .navbar, .aside {
background-color: light-dark(#f6f7f9, var(--mantine-color-dark-8));
}
.navbar, .aside {
@media (max-width: $mantine-breakpoint-sm) {
width: 350px;
}
}
.aside {
@media (max-width: $mantine-breakpoint-sm) {
margin-top: 45px;
}
}

View File

@ -1,7 +1,7 @@
import { Box, ScrollArea, Text } from "@mantine/core";
import CommentList from "@/features/comment/components/comment-list.tsx";
import { useAtom } from "jotai";
import { asideStateAtom } from "@/components/navbar/atoms/sidebar-atom.ts";
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import React, { ReactNode } from "react";
export default function Aside() {

View File

@ -0,0 +1,77 @@
import { AppShell, Container } from "@mantine/core";
import React from "react";
import { useLocation } from "react-router-dom";
import SettingsSidebar from "@/components/settings/settings-sidebar.tsx";
import { useAtom } from "jotai";
import {
asideStateAtom,
desktopSidebarAtom,
mobileSidebarAtom,
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
import Aside from "@/components/layouts/global/aside.tsx";
import classes from "./app-shell.module.css";
export default function GlobalAppShell({
children,
}: {
children: React.ReactNode;
}) {
const [mobileOpened] = useAtom(mobileSidebarAtom);
const [desktopOpened] = useAtom(desktopSidebarAtom);
const [{ isAsideOpen }] = useAtom(asideStateAtom);
const location = useLocation();
const isSettingsRoute = location.pathname.startsWith("/settings");
const isSpaceRoute = location.pathname.startsWith("/s/");
const isHomeRoute = location.pathname.startsWith("/home");
const isPageRoute = location.pathname.includes("/p/");
return (
<AppShell
header={{ height: 45 }}
navbar={
!isHomeRoute && {
width: 300,
breakpoint: "sm",
collapsed: {
mobile: !mobileOpened,
desktop: !desktopOpened,
},
}
}
aside={
isPageRoute && {
width: 350,
breakpoint: "sm",
collapsed: { mobile: !isAsideOpen, desktop: !isAsideOpen },
}
}
padding="md"
>
<AppShell.Header px="md" className={classes.header}>
<AppHeader />
</AppShell.Header>
{!isHomeRoute && (
<AppShell.Navbar className={classes.navbar} withBorder={false}>
{isSpaceRoute && <SpaceSidebar />}
{isSettingsRoute && <SettingsSidebar />}
</AppShell.Navbar>
)}
<AppShell.Main>
{isSettingsRoute ? (
<Container size={800}>{children}</Container>
) : (
children
)}
</AppShell.Main>
{isPageRoute && (
<AppShell.Aside className={classes.aside} p="md" withBorder={false}>
<Aside />
</AppShell.Aside>
)}
</AppShell>
);
}

View File

@ -0,0 +1,21 @@
import { atomWithWebStorage } from "@/lib/jotai-helper.ts";
import { atom } from "jotai";
export const mobileSidebarAtom = atom<boolean>(false);
export const desktopSidebarAtom = atomWithWebStorage<boolean>(
"showSidebar",
true,
);
export const desktopAsideAtom = atom<boolean>(false);
type AsideStateType = {
tab: string;
isAsideOpen: boolean;
};
export const asideStateAtom = atom<AsideStateType>({
tab: "",
isAsideOpen: false,
});

View File

@ -0,0 +1,13 @@
import { UserProvider } from "@/features/user/user-provider.tsx";
import { Outlet } from "react-router-dom";
import GlobalAppShell from "@/components/layouts/global/global-app-shell.tsx";
export default function Layout() {
return (
<UserProvider>
<GlobalAppShell>
<Outlet />
</GlobalAppShell>
</UserProvider>
);
}

View File

@ -0,0 +1,124 @@
import { Avatar, Group, Menu, rem, UnstyledButton, Text } from "@mantine/core";
import {
IconChevronDown,
IconLogout,
IconSettings,
IconUserCircle,
IconUsers,
} from "@tabler/icons-react";
import { useAtom } from "jotai";
import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts";
import { Link } from "react-router-dom";
import APP_ROUTE from "@/lib/app-route.ts";
import useAuth from "@/features/auth/hooks/use-auth.ts";
import { UserAvatar } from "@/components/ui/user-avatar.tsx";
export default function TopMenu() {
const [currentUser] = useAtom(currentUserAtom);
const { logout } = useAuth();
const user = currentUser?.user;
const workspace = currentUser?.workspace;
return (
<Menu width={250} position="bottom-end" withArrow shadow={"lg"}>
<Menu.Target>
<UnstyledButton>
<Group gap={7} wrap={"nowrap"}>
<Avatar
src={workspace.logo}
alt={workspace.name}
radius="xl"
size={20}
/>
<Text fw={500} size="sm" lh={1} mr={3}>
{workspace.name}
</Text>
<IconChevronDown
style={{ width: rem(12), height: rem(12) }}
stroke={1.5}
/>
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Workspace</Menu.Label>
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
leftSection={
<IconSettings
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
Workspace settings
</Menu.Item>
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
leftSection={
<IconUsers
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
Manage members
</Menu.Item>
<Menu.Divider />
<Menu.Label>Account</Menu.Label>
<Menu.Item component={Link} to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}>
<Group wrap={"nowrap"}>
<UserAvatar
radius="xl"
size={"sm"}
avatarUrl={user.avatarUrl}
name={user.name}
/>
<div>
<Text size="sm" fw={500} lineClamp={1}>
{user.name}
</Text>
<Text size="xs" c="dimmed">
{user.email}
</Text>
</div>
</Group>
</Menu.Item>
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
leftSection={
<IconUserCircle
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
My profile
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={logout}
leftSection={
<IconLogout
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
Logout
</Menu.Item>
</Menu.Dropdown>
</Menu>
);
}

View File

@ -1,17 +0,0 @@
import { UserProvider } from "@/features/user/user-provider.tsx";
import { Outlet } from "react-router-dom";
import SettingsShell from "@/components/layouts/settings/settings-shell.tsx";
import { Helmet } from "react-helmet-async";
export default function SettingsLayout() {
return (
<UserProvider>
<SettingsShell>
<Helmet>
<title>Settings</title>
</Helmet>
<Outlet />
</SettingsShell>
</UserProvider>
);
}

View File

@ -1,36 +0,0 @@
import { desktopSidebarAtom } from "@/components/navbar/atoms/sidebar-atom.ts";
import { AppShell, Container } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useAtom } from "jotai";
import React from "react";
import SettingsSidebar from "@/components/layouts/settings/settings-sidebar.tsx";
export default function SettingsShell({
children,
}: {
children: React.ReactNode;
}) {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
const [desktopOpened] = useAtom(desktopSidebarAtom);
return (
<AppShell
layout="alt"
header={{ height: 45 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
padding="md"
>
<AppShell.Navbar>
<SettingsSidebar />
</AppShell.Navbar>
<AppShell.Main>
<Container size={800}>{children}</Container>
</AppShell.Main>
</AppShell>
);
}

View File

@ -1,16 +0,0 @@
import { atomWithWebStorage } from '@/lib/jotai-helper';
import { atom } from 'jotai';
export const desktopSidebarAtom = atomWithWebStorage('showSidebar', true);
export const desktopAsideAtom = atom(false);
type AsideStateType = {
tab: string,
isAsideOpen: boolean,
}
export const asideStateAtom = atom<AsideStateType>({
tab: '',
isAsideOpen: false,
});

View File

@ -1,92 +0,0 @@
.navbar {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
height: 100%;
width: 100%;
padding: var(--mantine-spacing-md);
padding-top: 0;
display: flex;
flex-direction: column;
}
.section {
margin-left: calc(var(--mantine-spacing-md) * -1);
margin-right: calc(var(--mantine-spacing-md) * -1);
margin-bottom: var(--mantine-spacing-md);
&:not(:last-of-type) {
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
}
.searchCode {
font-weight: 700;
font-size: rem(10px);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-7));
}
.menuItems {
padding-left: calc(var(--mantine-spacing-md) - var(--mantine-spacing-xs));
padding-right: calc(var(--mantine-spacing-md) - var(--mantine-spacing-xs));
padding-bottom: var(--mantine-spacing-md);
}
.menu {
display: flex;
align-items: center;
width: 100%;
font-size: var(--mantine-font-size-sm);
padding: rem(4px) var(--mantine-spacing-xs);
border-radius: var(--mantine-radius-sm);
font-weight: 500;
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
&:hover {
background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
}
}
.menuItemInner {
display: flex;
align-items: center;
flex: 1;
}
.menuItemIcon {
margin-right: var(--mantine-spacing-sm);
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}
.pages {
padding-left: calc(var(--mantine-spacing-md) - rem(6px));
padding-right: calc(var(--mantine-spacing-md) - rem(6px));
padding-bottom: var(--mantine-spacing-md);
}
.pagesHeader {
padding-left: calc(var(--mantine-spacing-md) + rem(2px));
padding-right: var(--mantine-spacing-md);
margin-bottom: rem(5px);
}
.pageLink {
display: block;
padding: rem(8px) var(--mantine-spacing-xs);
text-decoration: none;
border-radius: var(--mantine-radius-sm);
font-size: var(--mantine-font-size-xs);
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
line-height: 1;
font-weight: 500;
&:hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
}
}
.activeButton {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
}

View File

@ -1,122 +0,0 @@
import {
UnstyledButton,
Text,
Group,
ActionIcon,
Tooltip,
rem,
} from "@mantine/core";
import { spotlight } from "@mantine/spotlight";
import {
IconSearch,
IconPlus,
IconSettings,
IconHome,
} from "@tabler/icons-react";
import classes from "./navbar.module.css";
import { UserButton } from "./user-button";
import React from "react";
import { useAtom } from "jotai";
import { SearchSpotlight } from "@/features/search/search-spotlight";
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom";
import { useLocation, useNavigate } from "react-router-dom";
import SpaceContent from "@/features/page/tree/components/space-content.tsx";
import clsx from "clsx";
import APP_ROUTE from "@/lib/app-route.ts";
interface PrimaryMenuItem {
icon: React.ElementType;
label: string;
path?: string;
onClick?: () => void;
}
const primaryMenu: PrimaryMenuItem[] = [
{ icon: IconHome, label: "Home", path: "/home" },
{ icon: IconSearch, label: "Search" },
{ icon: IconSettings, label: "Settings" },
];
export function Navbar() {
const [tree] = useAtom(treeApiAtom);
const navigate = useNavigate();
const location = useLocation();
const handleMenuItemClick = (label: string) => {
if (label === "Home") {
navigate("/home");
}
if (label === "Search") {
spotlight.open();
}
if (label === "Settings") {
navigate("/settings/workspace");
}
};
function handleCreatePage() {
tree?.create({ parentId: null, type: "internal", index: 0 });
}
const primaryMenuItems = primaryMenu.map((menuItem) => (
<UnstyledButton
key={menuItem.label}
className={clsx(
classes.menu,
location.pathname.toLowerCase() === menuItem?.path
? classes.activeButton
: "",
)}
onClick={() => handleMenuItemClick(menuItem.label)}
>
<div className={classes.menuItemInner}>
<menuItem.icon size={18} className={classes.menuItemIcon} stroke={2} />
<span>{menuItem.label}</span>
</div>
</UnstyledButton>
));
return (
<>
<nav className={classes.navbar}>
<div className={classes.section}>
<UserButton />
</div>
<div className={classes.section}>
<div className={classes.menuItems}>{primaryMenuItems}</div>
</div>
<div className={classes.section}>
<Group className={classes.pagesHeader} justify="space-between">
<Text size="xs" fw={500} c="dimmed">
Pages
</Text>
<Tooltip label="Create page" withArrow position="right">
<ActionIcon
variant="default"
size={18}
onClick={handleCreatePage}
>
<IconPlus
style={{ width: rem(12), height: rem(12) }}
stroke={1.5}
/>
</ActionIcon>
</Tooltip>
</Group>
<div className={classes.pages}>
<SpaceContent />
</div>
</div>
</nav>
<SearchSpotlight />
</>
);
}

View File

@ -1,10 +0,0 @@
.user {
display: block;
width: 100%;
padding: var(--mantine-spacing-md);
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
@mixin hover {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
}
}

View File

@ -1,32 +0,0 @@
import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core';
import { IconChevronRight } from '@tabler/icons-react';
import classes from './user-button.module.css';
import { useAtom } from 'jotai/index';
import { currentUserAtom } from '@/features/user/atoms/current-user-atom';
export function UserButton() {
const [currentUser] = useAtom(currentUserAtom);
return (
<UnstyledButton className={classes.user}>
<Group>
<Avatar
src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-9.png"
radius="xl"
/>
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>
{currentUser?.user.name}
</Text>
<Text c="dimmed" size="xs">
{currentUser?.user.email}
</Text>
</div>
<IconChevronRight style={{ width: rem(14), height: rem(14) }} stroke={1.5} />
</Group>
</UnstyledButton>
);
}

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { Group, Text, ScrollArea, ActionIcon, rem } from "@mantine/core";
import {
IconUser,
@ -9,7 +9,7 @@ import {
IconSpaces,
IconBrush,
} from "@tabler/icons-react";
import { Link, useLocation } from "react-router-dom";
import { Link, useLocation, useNavigate } from "react-router-dom";
import classes from "./settings.module.css";
interface DataItem {
@ -51,8 +51,13 @@ const groupedData: DataGroup[] = [
];
export default function SettingsSidebar() {
const pathname = useLocation().pathname;
const [active, setActive] = useState(pathname);
const location = useLocation();
const [active, setActive] = useState(location.pathname);
const navigate = useNavigate();
useEffect(() => {
setActive(location.pathname);
}, [location.pathname]);
const menuItems = groupedData.map((group) => (
<div key={group.heading}>
@ -65,9 +70,6 @@ export default function SettingsSidebar() {
data-active={active.startsWith(item.path) || undefined}
key={item.label}
to={item.path}
onClick={() => {
setActive(item.path);
}}
>
<item.icon className={classes.linkIcon} stroke={2} />
<span>{item.label}</span>
@ -77,32 +79,20 @@ export default function SettingsSidebar() {
));
return (
<nav className={classes.navbar}>
<div>
<Group className={classes.header} justify="flex-start">
<ActionIcon
component={Link}
to="/home"
variant="transparent"
c="gray"
aria-label="Home"
>
<IconArrowLeft stroke={2} />
</ActionIcon>
<Text fw={500}>Settings</Text>
</Group>
<div className={classes.navbar}>
<Group className={classes.title} justify="flex-start">
<ActionIcon
onClick={() => navigate(-1)}
variant="transparent"
c="gray"
aria-label="Back"
>
<IconArrowLeft stroke={2} />
</ActionIcon>
<Text fw={500}>Settings</Text>
</Group>
<ScrollArea h="80vh" w="100%">
{menuItems}
</ScrollArea>
</div>
<div className={classes.footer}>
<Link to="/home" className={classes.link}>
<IconArrowLeft className={classes.linkIcon} stroke={1.5} />
<span>Return to the app</span>
</Link>
</div>
</nav>
<ScrollArea w="100%">{menuItems}</ScrollArea>
</div>
);
}

View File

@ -1,29 +1,16 @@
.navbar {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
height: 100%;
width: 100%;
padding: var(--mantine-spacing-md);
display: flex;
flex-direction: column;
/*border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));*/
}
.navbarMain {
flex: 1;
}
.header {
padding-bottom: var(--mantine-spacing-md);
margin-bottom: calc(var(--mantine-spacing-md) * 1.5);
.title {
padding-bottom: var(--mantine-spacing-sm);
border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.footer {
padding-top: var(--mantine-spacing-md);
margin-top: var(--mantine-spacing-md);
border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}
.link {
cursor: pointer;
display: flex;

View File

@ -0,0 +1,21 @@
import { Skeleton } from '@mantine/core';
export default function PageListSkeleton() {
return (
<>
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
<Skeleton height={25} my="xs" radius="xs" />
</>
);
}

View File

@ -0,0 +1,34 @@
import {
IconLayoutSidebarRightCollapse,
IconLayoutSidebarRightExpand,
} from "@tabler/icons-react";
import {
ActionIcon,
BoxProps,
ElementProps,
MantineColor,
MantineSize,
} from "@mantine/core";
import React from "react";
export interface SidebarToggleProps extends BoxProps, ElementProps<"button"> {
size?: MantineSize | `compact-${MantineSize}` | (string & {});
color?: MantineColor;
opened?: boolean;
}
export default function SidebarToggle({
opened,
size = "sm",
...others
}: SidebarToggleProps) {
return (
<ActionIcon size={size} {...others} variant="subtle" color="gray">
{opened ? (
<IconLayoutSidebarRightExpand />
) : (
<IconLayoutSidebarRightCollapse />
)}
</ActionIcon>
);
}