mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-15 01:01:09 +10:00
refactor layout
* ui polishing * frontend and backend fixes
This commit is contained in:
@ -33,7 +33,7 @@ export default function SpaceSettingsModal({
|
||||
<Modal.Overlay />
|
||||
<Modal.Content style={{ overflow: "hidden" }}>
|
||||
<Modal.Header py={0}>
|
||||
<Modal.Title fw={500}>{space?.name} space </Modal.Title>
|
||||
<Modal.Title fw={500}>{space?.name}</Modal.Title>
|
||||
<Modal.CloseButton />
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
.spaceName {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-sm);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { UnstyledButton, Group, Avatar, Text, rem } from "@mantine/core";
|
||||
import classes from "./space-name.module.css";
|
||||
|
||||
interface SpaceNameProps {
|
||||
spaceName: string;
|
||||
}
|
||||
export function SpaceName({ spaceName }: SpaceNameProps) {
|
||||
return (
|
||||
<UnstyledButton className={classes.spaceName}>
|
||||
<Group>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="md" fw={500}>
|
||||
{spaceName}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
.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));
|
||||
}
|
||||
}
|
||||
|
||||
.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-2), var(--mantine-color-dark-6));
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
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 "./space-sidebar.module.css";
|
||||
import React from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { SearchSpotlight } from "@/features/search/search-spotlight.tsx";
|
||||
import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts";
|
||||
import { Link, useLocation, useParams } from "react-router-dom";
|
||||
import clsx from "clsx";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import SpaceSettingsModal from "@/features/space/components/settings-modal.tsx";
|
||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { SpaceName } from "@/features/space/components/sidebar/space-name.tsx";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import SpaceTree from "@/features/page/tree/components/space-tree.tsx";
|
||||
|
||||
export function SpaceSidebar() {
|
||||
const [tree] = useAtom(treeApiAtom);
|
||||
const location = useLocation();
|
||||
const [opened, { open: openSettings, close: closeSettings }] =
|
||||
useDisclosure(false);
|
||||
const { spaceSlug } = useParams();
|
||||
const { data: space, isLoading, isError } = useGetSpaceBySlugQuery(spaceSlug);
|
||||
|
||||
function handleCreatePage() {
|
||||
tree?.create({ parentId: null, type: "internal", index: 0 });
|
||||
}
|
||||
|
||||
if (!space) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.navbar}>
|
||||
<div
|
||||
className={classes.section}
|
||||
style={{
|
||||
border: "none",
|
||||
paddingTop: "8px",
|
||||
marginBottom: "0",
|
||||
}}
|
||||
>
|
||||
<SpaceName spaceName={space?.name} />
|
||||
</div>
|
||||
|
||||
<div className={classes.section}>
|
||||
<div className={classes.menuItems}>
|
||||
<UnstyledButton
|
||||
component={Link}
|
||||
to={getSpaceUrl(spaceSlug)}
|
||||
className={clsx(
|
||||
classes.menu,
|
||||
location.pathname.toLowerCase() === getSpaceUrl(spaceSlug)
|
||||
? classes.activeButton
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<div className={classes.menuItemInner}>
|
||||
<IconHome
|
||||
size={18}
|
||||
className={classes.menuItemIcon}
|
||||
stroke={2}
|
||||
/>
|
||||
<span>Overview</span>
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
|
||||
<UnstyledButton className={classes.menu} onClick={spotlight.open}>
|
||||
<div className={classes.menuItemInner}>
|
||||
<IconSearch
|
||||
size={18}
|
||||
className={classes.menuItemIcon}
|
||||
stroke={2}
|
||||
/>
|
||||
<span>Search</span>
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
|
||||
<UnstyledButton className={classes.menu} onClick={openSettings}>
|
||||
<div className={classes.menuItemInner}>
|
||||
<IconSettings
|
||||
size={18}
|
||||
className={classes.menuItemIcon}
|
||||
stroke={2}
|
||||
/>
|
||||
<span>Space settings</span>
|
||||
</div>
|
||||
</UnstyledButton>
|
||||
</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}>
|
||||
<SpaceTree spaceId={space.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SpaceSettingsModal
|
||||
opened={opened}
|
||||
onClose={closeSettings}
|
||||
spaceId={space?.slug}
|
||||
/>
|
||||
|
||||
<SearchSpotlight spaceId={space.id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
.card {
|
||||
background-color: var(--mantine-color-body);
|
||||
@mixin hover {
|
||||
box-shadow: var(--mantine-shadow-xs);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.cardSection {
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: rem(5px);
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-2));
|
||||
}
|
||||
46
apps/client/src/features/space/components/space-grid.tsx
Normal file
46
apps/client/src/features/space/components/space-grid.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { Text, Avatar, SimpleGrid, Card, rem } from "@mantine/core";
|
||||
import React from "react";
|
||||
import { useGetSpacesQuery } from "@/features/space/queries/space-query.ts";
|
||||
import { getSpaceUrl } from "@/lib/config.ts";
|
||||
import { Link } from "react-router-dom";
|
||||
import classes from "./space-grid.module.css";
|
||||
import { formatMemberCount } from "@/lib";
|
||||
|
||||
export default function SpaceGrid() {
|
||||
const { data, isLoading } = useGetSpacesQuery();
|
||||
|
||||
const cards = data?.items.map((space, index) => (
|
||||
<Card
|
||||
key={space.id}
|
||||
p="xs"
|
||||
radius="md"
|
||||
component={Link}
|
||||
to={getSpaceUrl(space.slug)}
|
||||
className={classes.card}
|
||||
withBorder
|
||||
>
|
||||
<Card.Section className={classes.cardSection} h={40}></Card.Section>
|
||||
<Avatar variant="filled" size="md" mt={rem(-20)}>
|
||||
{space.name.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
|
||||
<Text fz="md" fw={500} mt="xs" className={classes.title}>
|
||||
{space.name}
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs" fw={700} mt="md">
|
||||
{formatMemberCount(space.memberCount)}
|
||||
</Text>
|
||||
</Card>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text fz="sm" fw={500} mb={"md"}>
|
||||
Spaces you belong to
|
||||
</Text>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>{cards}</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { Text, Tabs, Space } from "@mantine/core";
|
||||
import { IconClockHour3 } from "@tabler/icons-react";
|
||||
import SpaceRecentChanges from "@/features/space/components/space-recent-changes.tsx";
|
||||
|
||||
export default function SpaceHomeTabs() {
|
||||
return (
|
||||
<Tabs defaultValue="recent">
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="recent" leftSection={<IconClockHour3 size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
Recent changes
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Space my="md" />
|
||||
|
||||
<Tabs.Panel value="recent">
|
||||
<SpaceRecentChanges />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
.page {
|
||||
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-1), var(--mantine-color-dark-8));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { Text, Group, Stack, UnstyledButton, Divider } from "@mantine/core";
|
||||
import classes from "./space-home.module.css";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||
import { formattedDate } from "@/lib/time.ts";
|
||||
import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
|
||||
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
||||
|
||||
function SpaceRecentChanges() {
|
||||
const { spaceSlug } = useParams();
|
||||
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
|
||||
const { data, isLoading, isError } = useRecentChangesQuery(space?.id);
|
||||
|
||||
if (isLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Text>Failed to fetch recent pages</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
data && (
|
||||
<div>
|
||||
{data.items.map((page) => (
|
||||
<div key={page.id}>
|
||||
<UnstyledButton
|
||||
component={Link}
|
||||
to={buildPageUrl(space.slug, page.slugId, page.title)}
|
||||
className={classes.page}
|
||||
p="xs"
|
||||
>
|
||||
<Group wrap="nowrap">
|
||||
<Stack gap="xs" style={{ flex: 1 }}>
|
||||
<Text fw={500} size="sm" lineClamp={1}>
|
||||
{page.title || "Untitled"}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Text c="dimmed" size="xs" fw={500}>
|
||||
{formattedDate(page.updatedAt)}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
<Divider />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default SpaceRecentChanges;
|
||||
Reference in New Issue
Block a user