feat: add root route app ui
This commit is contained in:
parent
13017299cf
commit
3946d0f05c
487
app/root.tsx
487
app/root.tsx
|
|
@ -1,5 +1,5 @@
|
||||||
import type { MetaFunction, LoaderArgs, LinksFunction } from '@remix-run/node'
|
import { MetaFunction, LoaderArgs, LinksFunction } from '@remix-run/node';
|
||||||
import { json } from '@remix-run/node'
|
import { json } from '@remix-run/node';
|
||||||
import {
|
import {
|
||||||
Links,
|
Links,
|
||||||
LiveReload,
|
LiveReload,
|
||||||
|
|
@ -7,38 +7,82 @@ import {
|
||||||
Outlet,
|
Outlet,
|
||||||
Scripts,
|
Scripts,
|
||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
|
Link,
|
||||||
useCatch,
|
useCatch,
|
||||||
useMatches,
|
useMatches,
|
||||||
useTransition
|
useTransition,
|
||||||
} from '@remix-run/react'
|
Form,
|
||||||
import { useEffect } from 'react'
|
useLocation
|
||||||
import NProgress from 'nprogress'
|
} from '@remix-run/react';
|
||||||
import nProgressStylesUrl from 'nprogress/nprogress.css'
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
import { getUser } from './session.server'
|
MantineProvider,
|
||||||
|
createEmotionCache,
|
||||||
|
useMantineTheme,
|
||||||
|
ColorScheme,
|
||||||
|
ColorSchemeProvider,
|
||||||
|
AppShell,
|
||||||
|
Navbar,
|
||||||
|
Header,
|
||||||
|
Text,
|
||||||
|
MediaQuery,
|
||||||
|
Burger,
|
||||||
|
ActionIcon,
|
||||||
|
useMantineColorScheme,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Group,
|
||||||
|
UnstyledButton,
|
||||||
|
ThemeIcon
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useColorScheme, useLocalStorage } from '@mantine/hooks';
|
||||||
|
import { StylesPlaceholder } from '@mantine/remix';
|
||||||
|
import {
|
||||||
|
NavigationProgress,
|
||||||
|
startNavigationProgress,
|
||||||
|
completeNavigationProgress
|
||||||
|
} from '@mantine/nprogress';
|
||||||
|
import { getUser } from './session.server';
|
||||||
|
import {
|
||||||
|
Sun,
|
||||||
|
Moon,
|
||||||
|
Clock,
|
||||||
|
LogOut,
|
||||||
|
LogIn,
|
||||||
|
Home,
|
||||||
|
Briefcase,
|
||||||
|
BarChart2,
|
||||||
|
FileText,
|
||||||
|
Upload
|
||||||
|
} from 'react-feather';
|
||||||
|
import { NotificationsProvider } from '@mantine/notifications';
|
||||||
|
|
||||||
export const meta: MetaFunction = () => ({
|
export const meta: MetaFunction = () => ({
|
||||||
charset: 'utf-8',
|
charset: 'utf-8',
|
||||||
title: 'New Remix App',
|
title: 'WorkTimer',
|
||||||
|
description:
|
||||||
|
'WorkTimer is a time tracking app. Helps you track your time spent on projects.',
|
||||||
viewport: 'width=device-width,initial-scale=1'
|
viewport: 'width=device-width,initial-scale=1'
|
||||||
})
|
});
|
||||||
|
|
||||||
export let links: LinksFunction = () => {
|
export let links: LinksFunction = () => {
|
||||||
return [{ rel: 'stylesheet', href: nProgressStylesUrl }]
|
return [];
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function loader({ request }: LoaderArgs) {
|
export async function loader({ request }: LoaderArgs) {
|
||||||
return json({
|
return json({
|
||||||
user: await getUser(request)
|
user: await getUser(request)
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEmotionCache({ key: 'mantine' });
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
let transition = useTransition()
|
let transition = useTransition();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (transition.state === 'idle') NProgress.done()
|
if (transition.state === 'idle') completeNavigationProgress();
|
||||||
else NProgress.start()
|
else startNavigationProgress();
|
||||||
}, [transition.state])
|
}, [transition.state]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document>
|
<Document>
|
||||||
|
|
@ -46,44 +90,79 @@ export default function App() {
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Layout>
|
</Layout>
|
||||||
</Document>
|
</Document>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Document({ children, title }: { children: React.ReactNode; title?: string }) {
|
function Document({
|
||||||
|
children,
|
||||||
|
title
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
title?: string;
|
||||||
|
}) {
|
||||||
|
const preferredColorScheme = useColorScheme();
|
||||||
|
const toggleColorScheme = (value?: ColorScheme) => {
|
||||||
|
console.log(value);
|
||||||
|
setColorScheme(value || (colorScheme === 'dark' ? 'light' : 'dark'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>({
|
||||||
|
key: 'mantine-color-scheme',
|
||||||
|
defaultValue: preferredColorScheme,
|
||||||
|
getInitialValueInEffect: true
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ColorSchemeProvider
|
||||||
|
colorScheme={colorScheme}
|
||||||
|
toggleColorScheme={toggleColorScheme}
|
||||||
|
>
|
||||||
|
<MantineProvider
|
||||||
|
theme={{ colorScheme }}
|
||||||
|
withGlobalStyles
|
||||||
|
withNormalizeCSS
|
||||||
|
>
|
||||||
|
<NotificationsProvider>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
{title ? <title>{title}</title> : null}
|
{title ? <title>{title}</title> : null}
|
||||||
|
<StylesPlaceholder />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<NavigationProgress />
|
||||||
{children}
|
{children}
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
{process.env.NODE_ENV === 'development' && <LiveReload />}
|
{process.env.NODE_ENV === 'development' && <LiveReload />}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
</NotificationsProvider>
|
||||||
|
</MantineProvider>
|
||||||
|
</ColorSchemeProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CatchBoundary() {
|
export function CatchBoundary() {
|
||||||
let caught = useCatch()
|
let caught = useCatch();
|
||||||
|
|
||||||
let message
|
let message;
|
||||||
switch (caught.status) {
|
switch (caught.status) {
|
||||||
case 401:
|
case 401:
|
||||||
message = 'Oops! Looks like you tried to visit a page that you do not have access to.'
|
message =
|
||||||
|
'Oops! Looks like you tried to visit a page that you do not have access to.';
|
||||||
|
|
||||||
break
|
break;
|
||||||
case 404:
|
case 404:
|
||||||
message = 'Oops! Looks like you tried to visit a page that does not exist.'
|
message =
|
||||||
|
'Oops! Looks like you tried to visit a page that does not exist.';
|
||||||
|
|
||||||
break
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(caught.data || caught.statusText)
|
throw new Error(caught.data || caught.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -97,23 +176,354 @@ export function CatchBoundary() {
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Document>
|
</Document>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Layout({ children }: React.PropsWithChildren<{}>) {
|
function Layout({ children }: React.PropsWithChildren<{}>) {
|
||||||
let version = useMatches().find((m) => m.id === 'root')?.data?.version
|
let user = useMatches().find((m) => m.id === 'root')?.data?.user;
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
const location = useLocation();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpened(false);
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<AppShell
|
||||||
<header className="sticky top-0 bg-white sm:px-10 p-5 border-b">Timer</header>
|
padding="md"
|
||||||
<main className="flex-grow">{children}</main>
|
navbarOffsetBreakpoint="sm"
|
||||||
<footer className="sm:px-10 p-5">Version {version}</footer>
|
asideOffsetBreakpoint="sm"
|
||||||
|
navbar={
|
||||||
|
user && (
|
||||||
|
<Navbar
|
||||||
|
p="xs"
|
||||||
|
hiddenBreakpoint="sm"
|
||||||
|
hidden={!opened}
|
||||||
|
width={{ sm: 200, lg: 250 }}
|
||||||
|
>
|
||||||
|
<MediaQuery largerThan="sm" styles={{ display: 'none' }}>
|
||||||
|
<Navbar.Section>
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
|
paddingLeft: theme.spacing.xs,
|
||||||
|
paddingRight: theme.spacing.xs,
|
||||||
|
paddingBottom: theme.spacing.lg,
|
||||||
|
borderBottom: `1px solid ${
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[4]
|
||||||
|
: theme.colors.gray[2]
|
||||||
|
}`
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Group position="apart" align="center">
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
onClick={() => toggleColorScheme()}
|
||||||
|
size={30}
|
||||||
|
title="Toggle color scheme"
|
||||||
|
>
|
||||||
|
{colorScheme === 'dark' ? (
|
||||||
|
<Sun size={16} />
|
||||||
|
) : (
|
||||||
|
<Moon size={16} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
</Box>
|
||||||
|
</Navbar.Section>
|
||||||
|
</MediaQuery>
|
||||||
|
{user && (
|
||||||
|
<Navbar.Section grow mt="md">
|
||||||
|
<UnstyledButton
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
sx={(theme) => ({
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing.xs,
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
color:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[0]
|
||||||
|
: theme.black,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[6]
|
||||||
|
: theme.colors.gray[0]
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon variant="light">
|
||||||
|
<Home size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<Text size="sm">Home</Text>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
<UnstyledButton
|
||||||
|
component={Link}
|
||||||
|
to="/time-entries"
|
||||||
|
sx={(theme) => ({
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing.xs,
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
color:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[0]
|
||||||
|
: theme.black,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[6]
|
||||||
|
: theme.colors.gray[0]
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon variant="light">
|
||||||
|
<Clock size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<Text size="sm">Time entries</Text>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
<UnstyledButton
|
||||||
|
component={Link}
|
||||||
|
to="/projects"
|
||||||
|
sx={(theme) => ({
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing.xs,
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
color:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[0]
|
||||||
|
: theme.black,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[6]
|
||||||
|
: theme.colors.gray[0]
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon variant="light">
|
||||||
|
<Briefcase size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<Text size="sm">Projects</Text>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
<UnstyledButton
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
sx={(theme) => ({
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing.xs,
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
color:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[0]
|
||||||
|
: theme.black,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[6]
|
||||||
|
: theme.colors.gray[0]
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon variant="light">
|
||||||
|
<FileText size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<Text size="sm">Report</Text>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
<UnstyledButton
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
sx={(theme) => ({
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing.xs,
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
color:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[0]
|
||||||
|
: theme.black,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[6]
|
||||||
|
: theme.colors.gray[0]
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon variant="light">
|
||||||
|
<Upload size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<Text size="sm">Import</Text>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
<UnstyledButton
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
sx={(theme) => ({
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
padding: theme.spacing.xs,
|
||||||
|
borderRadius: theme.radius.sm,
|
||||||
|
color:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[0]
|
||||||
|
: theme.black,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[6]
|
||||||
|
: theme.colors.gray[0]
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon variant="light">
|
||||||
|
<BarChart2 size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
|
||||||
|
<Text size="sm">Statistics</Text>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
</Navbar.Section>
|
||||||
|
)}
|
||||||
|
{user && (
|
||||||
|
<Navbar.Section>
|
||||||
|
<Form action="/logout" method="post">
|
||||||
|
<UnstyledButton type="submit" ml="sm" title="Logout">
|
||||||
|
<Group>
|
||||||
|
<ThemeIcon variant="light">
|
||||||
|
<LogOut size={16} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<div>
|
||||||
|
<Text>{user.email.split('@')[0]}</Text>
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
Click to logout
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
</Group>
|
||||||
|
</UnstyledButton>
|
||||||
|
</Form>
|
||||||
|
</Navbar.Section>
|
||||||
|
)}
|
||||||
|
</Navbar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
header={
|
||||||
|
<Header height={{ base: 50, md: 70 }} p="md">
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', alignItems: 'center', height: '100%' }}
|
||||||
|
>
|
||||||
|
<MediaQuery largerThan="sm" styles={{ display: 'none' }}>
|
||||||
|
<Burger
|
||||||
|
opened={opened}
|
||||||
|
onClick={() => setOpened((o) => !o)}
|
||||||
|
size="sm"
|
||||||
|
color={theme.colors.gray[6]}
|
||||||
|
mr="xl"
|
||||||
|
/>
|
||||||
|
</MediaQuery>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
component={Link}
|
||||||
|
to="/"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeIcon
|
||||||
|
variant="light"
|
||||||
|
size={24}
|
||||||
|
style={{ marginRight: theme.spacing.sm }}
|
||||||
|
>
|
||||||
|
<Clock />
|
||||||
|
</ThemeIcon>
|
||||||
|
<span>WorkTimer</span>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 'auto'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MediaQuery
|
||||||
|
smallerThan="sm"
|
||||||
|
styles={user ? { display: 'none' } : {}}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
onClick={() =>
|
||||||
|
toggleColorScheme(colorScheme === 'dark' ? 'light' : 'dark')
|
||||||
|
}
|
||||||
|
title="Toggle color scheme"
|
||||||
|
>
|
||||||
|
{colorScheme === 'dark' ? (
|
||||||
|
<Sun size={18} />
|
||||||
|
) : (
|
||||||
|
<Moon size={18} />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
</MediaQuery>
|
||||||
|
{!user && (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
ml="sm"
|
||||||
|
leftIcon={<LogIn />}
|
||||||
|
component={Link}
|
||||||
|
to="/login"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Header>
|
||||||
|
}
|
||||||
|
styles={(theme) => ({
|
||||||
|
main: {
|
||||||
|
backgroundColor:
|
||||||
|
theme.colorScheme === 'dark'
|
||||||
|
? theme.colors.dark[8]
|
||||||
|
: theme.colors.gray[0]
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: { error: Error }) {
|
export function ErrorBoundary({ error }: { error: Error }) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
return (
|
return (
|
||||||
<Document title="Error!">
|
<Document title="Error!">
|
||||||
<Layout>
|
<Layout>
|
||||||
|
|
@ -121,9 +531,12 @@ export function ErrorBoundary({ error }: { error: Error }) {
|
||||||
<h1>There was an error</h1>
|
<h1>There was an error</h1>
|
||||||
<p>{error.message}</p>
|
<p>{error.message}</p>
|
||||||
<hr />
|
<hr />
|
||||||
<p>Hey, developer, you should replace this with what you want your users to see.</p>
|
<p>
|
||||||
|
Hey, developer, you should replace this with what you want your
|
||||||
|
users to see.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Document>
|
</Document>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue