diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..f77001e --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,26 @@ +import CommonLayout from "../components/layout/CommonLayout"; +import { UserSettingsStoreContextProvider } from "../stores/UserSettingsStore"; +import "antd/dist/reset.css"; + +export const metadata = { + title: "0+x Harvest", + description: "Time track app for use at 0+x", + viewport: "initial-scale=1, maximum-scale=1, shrink-to-fit=no", + themeColor: "#001529", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + ); +} diff --git a/app/settings-v2/page.tsx b/app/settings-v2/page.tsx new file mode 100644 index 0000000..4c2633f --- /dev/null +++ b/app/settings-v2/page.tsx @@ -0,0 +1,129 @@ +"use client"; + +import moment from "moment/moment"; +import React from "react"; +import { + DEFAULT_VACATION_ALLOWANCE, + getVacationAllowance, + usePrimaryTask, + VACATION_ALLOWANCE_KEY, +} from "../../utils"; +import { Card, Col, Form, InputNumber, Row, Select } from "antd"; +import { observer } from "mobx-react-lite"; +import { specialTasks, useProjectAssignments } from "../../lib/api"; +import { useUserSettingState } from "../../stores/UserSettingsStore"; +import { useRedirectIfNotInRole } from "../../lib/utils"; +import { Route } from "../../lib/routes"; +import { AccessRole } from "../../types"; + +export default function Page() { + useRedirectIfNotInRole(Route.ADMIN, [AccessRole.MEMBER]); + return ( + + + + + + + + + ); +} + +const VacationAllowanceCard = () => { + const thisYear = moment().year(); + const years = [thisYear - 2, thisYear - 1, thisYear, thisYear + 1]; + + const [vacationAllowance, setVacationAllowance] = React.useState(() => { + let values = getVacationAllowance(); + return cleanupValues(values, years); + }); + + return ( + + +
+ {years.map((year) => ( + + { + let newValue = { ...vacationAllowance }; + if (typeof val === "number") { + newValue[year] = val; + } else { + delete newValue[year]; + } + newValue = cleanupValues(newValue, years); + localStorage.setItem( + VACATION_ALLOWANCE_KEY, + JSON.stringify(newValue) + ); + setVacationAllowance(newValue); + }} + /> + + ))} +
+
+ ); +}; + +const PrimaryTaskCard = observer(() => { + const { data: projectAssignments } = useProjectAssignments(); + const userSettingsState = useUserSettingState(); + const primaryTask = usePrimaryTask(); + return ( + + + + + ); +}); + +const cleanupValues = ( + allowance: Record, + validYears: number[] +) => { + const copy = { ...allowance }; + for (const y of Object.keys(copy)) { + const yearInt = parseInt(y); + if (!validYears.includes(yearInt)) { + // cleaning up old/invalid values + delete copy[yearInt]; + } + } + + return copy; +}; diff --git a/components/layout/CommonLayout.tsx b/components/layout/CommonLayout.tsx index 74160e0..cd272cb 100644 --- a/components/layout/CommonLayout.tsx +++ b/components/layout/CommonLayout.tsx @@ -1,3 +1,4 @@ +"use client"; import React from "react"; import { Col, Layout, Row } from "antd"; import { GithubOutlined } from "@ant-design/icons"; diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index c828642..6daa56b 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -3,16 +3,15 @@ import { Avatar, Layout, Menu } from "antd"; import Link from "next/link"; import Image from "next/image"; import { isUserInRole, logout, useUser } from "../../lib/api"; -import { useRouter } from "next/dist/client/router"; +import { ItemType } from "antd/lib/menu/hooks/useItems"; +import { usePathname } from "next/navigation"; import { AccessRole } from "../../types"; -import { ItemType } from "antd/es/menu/hooks/useItems"; import { Route } from "../../lib/routes"; const LoginButton = () => { - const router = useRouter(); const { data, isLoading } = useUser(); - - if (router.pathname.startsWith("/login")) { + const pathname = usePathname(); + if (pathname?.startsWith("/login")) { return Log In; } @@ -36,6 +35,13 @@ const LoginButton = () => { const pages: (ItemType & { requiredRoles?: AccessRole[]; })[] = [ + { + key: "default", + label: ( + Logo + ), + requiredRoles: [AccessRole.MEMBER], + }, { key: "home", label: Hours, @@ -60,7 +66,7 @@ const pages: (ItemType & { ]; export default function Header() { - const router = useRouter(); + const pathname = usePathname(); const { data: user, isLoading } = useUser(); const filteredPages = pages.filter( (p) => @@ -70,13 +76,10 @@ export default function Header() { return ( -
- Logo -
diff --git a/lib/utils.ts b/lib/utils.ts index 99f4276..76a0228 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,4 +1,4 @@ -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import { isUserInRole, useUser } from "./api"; import React from "react"; import { AccessRole } from "../types"; @@ -24,9 +24,7 @@ export const useRedirectIfNotInRole = ( React.useEffect(() => { if (user && !isUserInRole(user, requiredRoles)) { - router.push({ - pathname: redirectTo, - }); + router.push(redirectTo); } }, [user, router, requiredRoles, redirectTo]); }; diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03..fd36f94 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/package.json b/package.json index 03b0c67..8071ce8 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,10 @@ "test:e2e": "playwright test" }, "dependencies": { - "@ant-design/icons": "^4.8.0", + "@ant-design/cssinjs": "^1.9.1", + "@ant-design/icons": "^5.0.1", "@sentry/nextjs": "^7.51.0", - "antd": "^5.0.2", + "antd": "^5.4.6", "axios": "^0.27.2", "classnames": "^2.3.1", "mobx": "^6.6.1", @@ -24,7 +25,7 @@ "nookies": "^2.5.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "swr": "^1.3.0" + "swr": "^2.1.5" }, "devDependencies": { "@playwright/test": "^1.27.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index a684ce0..e51e438 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -3,27 +3,24 @@ import Head from "next/head"; import type { AppProps } from "next/app"; import "antd/dist/reset.css"; import React from "react"; +import { usePathname, useRouter } from "next/navigation"; import CommonLayout from "../components/layout/CommonLayout"; import { UserSettingsStore, UserSettingsStoreContext, } from "../stores/UserSettingsStore"; import { useUser } from "../lib/api"; -import { useRouter } from "next/router"; const publicPaths = ["/login", "/not-us"]; function App({ Component, pageProps }: AppProps) { const { isError } = useUser(); const router = useRouter(); - + const pathname = usePathname(); React.useEffect(() => { if (isError) { - const path = router.asPath.split("?")[0]; - if (!publicPaths.includes(path)) { - router.push({ - pathname: "/login", - }); + if (pathname && !publicPaths.includes(pathname)) { + router.push("/login"); } } }, [isError, router]); diff --git a/pages/_document.tsx b/pages/_document.tsx new file mode 100644 index 0000000..fc09edd --- /dev/null +++ b/pages/_document.tsx @@ -0,0 +1,51 @@ +import Document, { Html, Head, Main, NextScript } from "next/document"; +import { StyleProvider, createCache, extractStyle } from "@ant-design/cssinjs"; + +type MyDocumentProps = { + styles: React.ReactNode; +}; + +export class MyDocument extends Document { + render() { + return ( + + {this.props.styles} + +
+ + + + ); + } +} + +MyDocument.getInitialProps = async (ctx) => { + const originalRenderPage = ctx.renderPage; + const cache = createCache(); + + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: (App) => + function EnhanceApp(props) { + return ( + + + + ); + }, + }); + + const initialProps = await Document.getInitialProps(ctx); + + return { + ...initialProps, + styles: ( + <> + {initialProps.styles} +