* replace next with vite
* disable strictmode (it interferes with collaboration in dev mode)
This commit is contained in:
Philipinho
2023-10-20 17:12:08 +01:00
parent a86991e3d7
commit 9b682c8af5
96 changed files with 645 additions and 505 deletions

View File

@ -0,0 +1,14 @@
import { UserProvider } from '@/features/user/user-provider';
import Shell from './shell';
import { Outlet } from 'react-router-dom';
export default function DashboardLayout() {
return (
<UserProvider>
<Shell>
<Outlet />
</Shell>
</UserProvider>
);
}

View File

@ -0,0 +1,51 @@
import { desktopSidebarAtom } from '@/components/navbar/atoms/sidebar-atom';
import { useToggleSidebar } from '@/components/navbar/hooks/use-toggle-sidebar';
import { Navbar } from '@/components/navbar/navbar';
import { AppShell, Burger, Group } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useAtom } from 'jotai';
export default function Shell({ children }: { children: React.ReactNode }) {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
const [desktopOpened] = useAtom(desktopSidebarAtom);
const toggleDesktop = useToggleSidebar(desktopSidebarAtom);
return (
<AppShell
layout="alt"
header={{ height: 45 }}
navbar={{
width: 300,
breakpoint: 'sm',
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
padding="md"
>
<AppShell.Header>
<Group h="100%" px="md">
<Burger
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Burger
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
/>
Header
</Group>
</AppShell.Header>
<AppShell.Navbar>
<Navbar />
</AppShell.Navbar>
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
);
}

View File

@ -0,0 +1,3 @@
import { atomWithWebStorage } from "@/lib/jotai-helper";
export const desktopSidebarAtom = atomWithWebStorage('showSidebar',true);

View File

@ -0,0 +1,8 @@
import { useAtom } from "jotai";
export function useToggleSidebar(sidebarAtom: any) {
const [sidebarState, setSidebarState] = useAtom(sidebarAtom);
return () => {
setSidebarState(!sidebarState);
}
}

View File

@ -0,0 +1,88 @@
.navbar {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
height: 100%;
width: 100%;
padding: var(--mantine-spacing-md);
padding-top: 0;
display: flex;
flex-direction: column;
/*border-right: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));*/
}
.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(8px) 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));
}
}

View File

@ -0,0 +1,116 @@
import {
UnstyledButton,
Text,
Group,
ActionIcon,
Tooltip,
rem,
} from '@mantine/core';
import { spotlight } from '@mantine/spotlight';
import {
IconSearch,
IconPlus,
IconSettings,
IconFilePlus,
} from '@tabler/icons-react';
import classes from './navbar.module.css';
import { UserButton } from './user-button';
import React from 'react';
import { useAtom } from 'jotai';
import { settingsModalAtom } from '@/features/settings/modal/atoms/settings-modal-atom';
import SettingsModal from '@/features/settings/modal/settings-modal';
import { SearchSpotlight } from '@/features/search/search-spotlight';
import { treeApiAtom } from '@/features/page/tree/atoms/tree-api-atom';
import PageTree from '@/features/page/tree/page-tree';
interface PrimaryMenuItem {
icon: React.ElementType;
label: string;
onClick?: () => void;
}
const primaryMenu: PrimaryMenuItem[] = [
{ icon: IconSearch, label: 'Search' },
{ icon: IconSettings, label: 'Settings' },
{ icon: IconFilePlus, label: 'New Page' },
];
export function Navbar() {
const [, setSettingsModalOpen] = useAtom(settingsModalAtom);
const [tree] = useAtom(treeApiAtom);
const handleMenuItemClick = (label: string) => {
if (label === 'Search') {
spotlight.open();
}
if (label === 'Settings') {
setSettingsModalOpen(true);
}
};
function handleCreatePage() {
tree?.create({ parentId: null, type: 'internal', index: 0 });
}
const primaryMenuItems = primaryMenu.map((menuItem) => (
<UnstyledButton
key={menuItem.label}
className={classes.menu}
onClick={() => handleMenuItemClick(menuItem.label)}
>
<div className={classes.menuItemInner}>
<menuItem.icon
size={20}
className={classes.menuItemIcon}
stroke={1.5}
/>
<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}>
<PageTree />
</div>
</div>
</nav>
<SearchSpotlight />
<SettingsModal />
</>
);
}

View File

@ -0,0 +1,10 @@
.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

@ -0,0 +1,28 @@
import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core';
import { IconChevronRight } from '@tabler/icons-react';
import classes from './user-button.module.css';
export function UserButton() {
return (
<UnstyledButton className={classes.user}>
<Group>
<Avatar
src="https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=255&q=80"
radius="xl"
/>
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>
Harriette Spoonlicker
</Text>
<Text c="dimmed" size="xs">
hspoonlicker@outlook.com
</Text>
</div>
<IconChevronRight style={{ width: rem(14), height: rem(14) }} stroke={1.5} />
</Group>
</UnstyledButton>
);
}

View File

@ -0,0 +1,19 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnMount: false,
refetchOnWindowFocus: false,
},
},
});
export function TanstackProvider({ children }: React.PropsWithChildren) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}

View File

@ -0,0 +1,13 @@
import { Button, Group, useMantineColorScheme } from '@mantine/core';
export function ThemeToggle() {
const { setColorScheme } = useMantineColorScheme();
return (
<Group justify="center" mt="xl">
<Button onClick={() => setColorScheme('light')}>Light</Button>
<Button onClick={() => setColorScheme('dark')}>Dark</Button>
<Button onClick={() => setColorScheme('auto')}>Auto</Button>
</Group>
);
}

View File

@ -0,0 +1,19 @@
import t, { Toaster, useToasterStore } from "react-hot-toast";
import { useEffect, useState } from "react";
export default function CustomToaster() {
const { toasts } = useToasterStore();
const TOAST_LIMIT = 3;
const [toastLimit, setToastLimit] = useState<number>(TOAST_LIMIT);
useEffect(() => {
toasts
.filter((tt) => tt.visible)
.filter((_, i) => i >= toastLimit)
.forEach((tt) => {
t.dismiss(tt.id);
});
}, [toastLimit, toasts]);
return <Toaster position={"top-right"}/>;
}