diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 3a3a598..0000000 --- a/.eslintrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": ["next", "next/core-web-vitals"], - "rules": { - "react/prop-types": 0, - "react/no-unescaped-entities": 0 - } -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..0a3aeb1 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/eslintrc", + "root": true, + "extends": [ + "next/core-web-vitals", + "prettier", + "plugin:tailwindcss/recommended" + ], + "plugins": ["tailwindcss"], + "rules": { + "tailwindcss/no-custom-classname": "off", + "tailwindcss/classnames-order": "error" + }, + "settings": { + "tailwindcss": { + "callees": ["cn"] + }, + "next": { + "rootDir": ["apps/*/"] + } + } +} \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index b9dc034..ff32919 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,9 @@ .next -.vercel \ No newline at end of file +.vercel +cache +.cache +package.json +package-lock.json +public +CHANGELOG.md +.yarn \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 9c62621..d1de84c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,6 @@ "semi": true, "singleQuote": false, "arrowParens": "avoid", - "trailingComma": "none" + "trailingComma": "none", + "plugins": ["prettier-plugin-tailwindcss"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index f6598f8..4d598ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,26 +1,14 @@ { - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/*.bk": true, - "**/node_modules": true, - "**/target": true - }, "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": true }, - "editor.wordWrapColumn": 100, - "javascript.updateImportsOnFileMove.enabled": "always", - "typescript.updateImportsOnFileMove.enabled": "always", - "javascript.preferences.importModuleSpecifier": "non-relative", + "typescript.tsdk": "node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.suggest.autoImports": true, "typescript.format.enable": true, "[typescriptreact]": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.formatOnSave": true }, - "typescript.tsdk": "node_modules/typescript/lib" + "tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]] } diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx new file mode 100644 index 0000000..1b93301 --- /dev/null +++ b/app/(auth)/layout.tsx @@ -0,0 +1,8 @@ +interface AuthLayoutProps { + children: React.ReactNode + } + + export default function AuthLayout({ children }: AuthLayoutProps) { + return
{children}
+ } + \ No newline at end of file diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 0000000..521d52f --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,26 @@ +import { LoginButton } from "@/components/auth/login-button"; +import { Icons } from "@/components/icons"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Login", + description: "Login to your account" +}; + +export default function LoginPage() { + return ( +
+
+
+ +

Welcome back

+

+ Click to login with your Knowit AD account. +

+
+
+ +
+
+ ); +} diff --git a/app/(dashboard)/dashboard/_components/avatar.tsx b/app/(dashboard)/dashboard/_components/avatar.tsx new file mode 100644 index 0000000..d105f50 --- /dev/null +++ b/app/(dashboard)/dashboard/_components/avatar.tsx @@ -0,0 +1,22 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { UserAvatar } from "@/components/user/user-avatar"; +import { getCurrentUser } from "@/lib/session"; +import { getUserAvatar } from "@/lib/user"; + +export default async function Avatar() { + const user = await getCurrentUser(); + + if (!user) { + return
Not logged in
; + } + + const userAvatar = await getUserAvatar(user?.activeDirectoryId); + + return ; +} + +function AvatarSkeleton() { + return ; +} + +export { AvatarSkeleton }; diff --git a/app/(dashboard)/dashboard/_components/calendar-month-with-salary.tsx b/app/(dashboard)/dashboard/_components/calendar-month-with-salary.tsx new file mode 100644 index 0000000..a74d87b --- /dev/null +++ b/app/(dashboard)/dashboard/_components/calendar-month-with-salary.tsx @@ -0,0 +1,99 @@ +import { MonthSelect } from "@/components/calendar/month-select"; +import { Icons } from "@/components/icons"; +import { SalaryDetailsCard } from "@/components/salary/salary-details-card"; +import { buttonVariants } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Show } from "@/components/ui/show"; +import { UserEditSalaryDetailsDialog } from "@/components/user/user-edit-salary-details-dialog"; +import { UserEarningsDetails, type CalendarMonth as CalendarMonthType } from "@/types"; +import { Session } from "next-auth"; +import Link from "next/link"; +import { CalendarMonth } from "./calendar-month"; + +export default function CalendarMonthWithSalary({ + user, + calendarMonth, + userEarnings +}: { + user: Session["user"]; + calendarMonth: CalendarMonthType; + userEarnings?: UserEarningsDetails; +}) { + return ( +
+
+

+ Salary details for {calendarMonth.month} {calendarMonth.year} +

+
+ + {userEarnings?.activeCalendarMonthStatistics.workDays.length.toString()} + + + {userEarnings?.activeCalendarMonthStatistics.workHours.toString()} + + + {userEarnings?.activeCalendarMonthStatistics.grossFormatted} + + + {userEarnings?.activeCalendarMonthStatistics.netFormatted} + +
+ + + +
+ +
+ + Salary for {calendarMonth.month} paid with{" "} + half tax at{" "} + {userEarnings?.nextMonthStatistics?.payDay} + +
+
+
+ +
+
+
+
+

{calendarMonth.year}

+ +
+
+ {/* Go to previous month if month.MonthNumber is 0, go to previous year and month 11 */} + + Forrige måned + + + {/* Go to current month */} + + I dag + + {/* Go to next month if month.MonthNumber is 11, go to next year and month 0 */} + + Neste måned + + +
+
+ +
+
+ ); +} diff --git a/app/(dashboard)/dashboard/_components/calendar-month.tsx b/app/(dashboard)/dashboard/_components/calendar-month.tsx new file mode 100644 index 0000000..09c8d44 --- /dev/null +++ b/app/(dashboard)/dashboard/_components/calendar-month.tsx @@ -0,0 +1,84 @@ +import { CalendarDay } from "@/components/calendar/calendar-day"; +import { Show } from "@/components/ui/show"; +import { UserEditWorkDayDetailDialog } from "@/components/user/user-edit-work-day-detail-dialog"; +import { getRequestDateNow } from "@/lib/date"; +import { cn } from "@/lib/utils"; +import { CalendarMonth, UserWorkDayDetail } from "@/types"; +import { getCalendarMonthEntries } from "@/utils/calendar-utils"; +import * as React from "react"; + +const CalendarMonth: React.FC<{ + month: CalendarMonth; + big?: boolean; + workDayDetails?: UserWorkDayDetail[]; +}> = ({ month, big = false, workDayDetails = [], ...other }) => { + const currentDate = getRequestDateNow(); + const showWeeks = true && !big; + const holidayInfos = month.days.filter(day => day.holidayInformation); + + const calendarEntries = getCalendarMonthEntries(month, currentDate, showWeeks, workDayDetails); + + return ( +
+
+
+
+ Off work +
+
+
+ Work +
+
+
+ Non commissioned +
+
+
+ {calendarEntries.map((calendarDay, index) => { + switch (calendarDay.type) { + case "day": + return ( + + ); + default: + return ( + + ); + } + })} +
+ 0}> +
+ {holidayInfos.map((day, index) => ( +
+ {`${day.date.getDate()}.${day.date.getMonth() + 1}: ${day.holidayInformation?.name}${ + holidayInfos.length === index + 1 ? "" : "," + }`} +
+ ))} +
+
+
+ ); +}; + +export { CalendarMonth }; diff --git a/app/(dashboard)/dashboard/_components/calendar-year.tsx b/app/(dashboard)/dashboard/_components/calendar-year.tsx new file mode 100644 index 0000000..5f052e9 --- /dev/null +++ b/app/(dashboard)/dashboard/_components/calendar-year.tsx @@ -0,0 +1,72 @@ +import { getCalendarYear } from "@/utils/calendar-utils"; +import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"; +import { getYear } from "date-fns"; +import Link from "next/link"; +import * as React from "react"; + +import { getRequestDateNow } from "@/lib/date"; +import { cn } from "@/lib/utils"; +import { CalendarMonth } from "./calendar-month"; + +const CalendarYear: React.FC<{ date: Date }> = ({ date }) => { + const currentDate = getRequestDateNow(); + const year = date.getFullYear(); + const calendarYear = getCalendarYear(year); + + return ( +
+

+ Norsk kalender med helligdager + - Kalender {getYear(date)} +

+
+

Kalender {getYear(date)}

+
+ {/* Go to previous year */} + + Forrige år + + + {/* Go to current year */} + + I dag + + {/* Go to next year */} + + Neste år + + +
+
+
+ {calendarYear.months.map((month, index) => ( +
+ +

+ {month.month} +

+ + +
+ ))} +
+
+ ); +}; + +export { CalendarYear }; diff --git a/app/(dashboard)/dashboard/_components/company-benefits.tsx b/app/(dashboard)/dashboard/_components/company-benefits.tsx new file mode 100644 index 0000000..24c4cec --- /dev/null +++ b/app/(dashboard)/dashboard/_components/company-benefits.tsx @@ -0,0 +1,43 @@ +import { Icons } from "@/components/icons"; +import { Card } from "@/components/ui/card"; + +const CompanyBenefit = ({ text }: { text: string }) => ( + + +
+ +
+ {text} +
+
+); + +export default function CompanyBenefits() { + return ( +
+
+

Knowit Experience Bergen

+

Company Benefits

+

+ You get paid by commission or guaranteed salary. Knowit covers both employer's + national insurance contributions (14.10%) and holyday payment (12%). This means you can + calculate your next payment by the following formulae{" "} + + work hours in month x hourly rate x commission = your salary. + +

+
+
+ {/* transform all to CompanyBenefit component */} + + + + + + + + +
+
+ ); +} diff --git a/app/(dashboard)/dashboard/_components/footer.tsx b/app/(dashboard)/dashboard/_components/footer.tsx new file mode 100644 index 0000000..0d71838 --- /dev/null +++ b/app/(dashboard)/dashboard/_components/footer.tsx @@ -0,0 +1,72 @@ +import { Icons } from "@/components/icons"; +import { ThemeSelect } from "@/components/theme-select"; +import { Box } from "@/components/ui/box"; +import { Flex } from "@/components/ui/flex"; +import Link from "@/components/ui/link"; + +export default function Footer() { + return ( +
+
+ + +

Resources

+
    +
  • + + Personal handbook + +
  • +
  • + CV Partner +
  • +
  • + Helpit +
  • +
  • + Timekeeping +
  • +
  • + Slack +
  • +
  • + + Shareit + +
  • +
  • + Brand book +
  • +
  • + + Office templates + +
  • +
+
+ +

Site

+
    +
  • + Home +
  • +
  • + Profile +
  • +
  • + Feedback +
  • +
+
+ +

Feedback

+ +
+
+ + + +
+
+ ); +} diff --git a/app/(dashboard)/dashboard/_components/next-paycheck.tsx b/app/(dashboard)/dashboard/_components/next-paycheck.tsx new file mode 100644 index 0000000..cea4647 --- /dev/null +++ b/app/(dashboard)/dashboard/_components/next-paycheck.tsx @@ -0,0 +1,22 @@ +import { getCurrentUser } from "@/lib/session"; +import { getUserEarnings } from "@/lib/user"; + +export default async function NextPaycheck() { + const user = await getCurrentUser(); + + if (!user) { + return
Not logged in
; + } + + const userEarnings = await getUserEarnings(user?.activeDirectoryId); + + return ( +
+
Next paycheck
+
{userEarnings?.nextPayDayStatistics.payDay}
+
+ {userEarnings?.nextPayDayStatistics.netFormatted} +
+
+ ); +} diff --git a/app/(dashboard)/dashboard/_components/salary-form.tsx b/app/(dashboard)/dashboard/_components/salary-form.tsx new file mode 100644 index 0000000..f40ab88 --- /dev/null +++ b/app/(dashboard)/dashboard/_components/salary-form.tsx @@ -0,0 +1,22 @@ +import { UserSalaryDetailsForm } from "@/components/user/user-salary-details-form"; +import { getCurrentUser } from "@/lib/session"; + +export default async function SalaryForm() { + const user = await getCurrentUser(); + + if (!user) { + return
Not logged in
; + } + + return ( + + ); +} diff --git a/app/(dashboard)/dashboard/_components/yearly-economic-overview.tsx b/app/(dashboard)/dashboard/_components/yearly-economic-overview.tsx new file mode 100644 index 0000000..1e607a1 --- /dev/null +++ b/app/(dashboard)/dashboard/_components/yearly-economic-overview.tsx @@ -0,0 +1,52 @@ +import { SalaryDetailsCard } from "@/components/salary/salary-details-card"; +import { getCurrentUser } from "@/lib/session"; +import { getUserEarnings } from "@/lib/user"; + +export default async function YearlyEconomicOverview() { + const user = await getCurrentUser(); + + if (!user) { + return
Not logged in
; + } + + const userEarnings = await getUserEarnings(user?.activeDirectoryId); + + return ( +
+
+

{userEarnings?.yearSalaryStatistics.year} overview

+
+ + {userEarnings?.yearSalaryStatistics.workDays} + + + {userEarnings?.yearSalaryStatistics.workHours} + + + {userEarnings?.yearSalaryStatistics.grossFormatted} + + + {userEarnings?.yearSalaryStatistics.netFormatted} + +
+
+
+

{userEarnings?.nextYearSalaryStatistics.year} overview

+
+ + {userEarnings?.nextYearSalaryStatistics.workDays} + + + {userEarnings?.nextYearSalaryStatistics.workHours} + + + {userEarnings?.nextYearSalaryStatistics.grossFormatted} + + + {userEarnings?.nextYearSalaryStatistics.netFormatted} + +
+
+
+ ); +} diff --git a/app/(dashboard)/dashboard/layout.tsx b/app/(dashboard)/dashboard/layout.tsx new file mode 100644 index 0000000..442a37b --- /dev/null +++ b/app/(dashboard)/dashboard/layout.tsx @@ -0,0 +1,38 @@ +import { Icons } from "@/components/icons"; +import { getCurrentUser } from "@/lib/session"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { Suspense } from "react"; +import Avatar, { AvatarSkeleton } from "./_components/avatar"; +import Footer from "./_components/footer"; +import NextPaycheck from "./_components/next-paycheck"; + +export default async function DashboardLayout({ children }) { + const user = await getCurrentUser(); + + if (!user) { + redirect("/login"); + } + + return ( + <> +
+ +
{children}
+
+ + ); +} diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx new file mode 100644 index 0000000..7880b6e --- /dev/null +++ b/app/(dashboard)/dashboard/page.tsx @@ -0,0 +1,38 @@ +import { getRequestDateNow } from "@/lib/date"; +import { getCurrentUser } from "@/lib/session"; +import { getUserEarnings } from "@/lib/user"; +import { getCalendarMonth } from "@/utils/calendar-utils"; +import { redirect } from "next/navigation"; +import { Suspense } from "react"; +import CalendarMonthWithSalary from "./_components/calendar-month-with-salary"; +import CompanyBenefits from "./_components/company-benefits"; +import YearlyEconomicOverview from "./_components/yearly-economic-overview"; + +export default async function DashboardPage() { + const user = await getCurrentUser(); + + if (!user) { + return redirect("/login"); + } + + const currentDate = getRequestDateNow(); + + const calendarMonth = getCalendarMonth(currentDate); + + const userEarnings = await getUserEarnings(user.activeDirectoryId); + + return ( + <> + + + }> + {/* @ts-expect-error Async Server Component */} + + + + ); +} diff --git a/app/(dashboard)/dashboard/profile/page.tsx b/app/(dashboard)/dashboard/profile/page.tsx new file mode 100644 index 0000000..96d61e2 --- /dev/null +++ b/app/(dashboard)/dashboard/profile/page.tsx @@ -0,0 +1,21 @@ +import { getCurrentUser } from "@/lib/session"; +import { redirect } from "next/navigation"; +import { Suspense } from "react"; +import SalaryForm from "../_components/salary-form"; + +export default async function ProfilePage() { + const user = await getCurrentUser(); + + if (!user) { + return redirect("/login"); + } + + return ( +
+ loading...
}> + {/* @ts-expect-error Async Server Component */} + + +
+ ); +} diff --git a/app/(dashboard)/dashboard/year/[year]/month/[month]/page.tsx b/app/(dashboard)/dashboard/year/[year]/month/[month]/page.tsx new file mode 100644 index 0000000..df3a230 --- /dev/null +++ b/app/(dashboard)/dashboard/year/[year]/month/[month]/page.tsx @@ -0,0 +1,38 @@ +import CalendarMonthWithSalary from "@/app/(dashboard)/dashboard/_components/calendar-month-with-salary"; +import { getRequestDateNow } from "@/lib/date"; +import { getCurrentUser } from "@/lib/session"; +import { getUserEarnings } from "@/lib/user"; +import { getCalendarMonth } from "@/utils/calendar-utils"; +import { redirect } from "next/navigation"; + +interface SelectedYearMonthPageProps { + params: { year: string; month: string }; +} + +export const dynamic = "force-dynamic"; +export const dynamicParams = true; + +export default async function SelectedYearMonthPage({ params }: SelectedYearMonthPageProps) { + const user = await getCurrentUser(); + + if (!user) { + return redirect("/login"); + } + + const currentDate = getRequestDateNow(); + + const date = new Date(+(params.year ?? currentDate.getFullYear()), +params.month); + const calendarMonth = getCalendarMonth(date); + + const userEarnings = await getUserEarnings(user.activeDirectoryId, date); + + return ( + <> + + + ); +} diff --git a/app/(dashboard)/dashboard/year/[year]/month/page.tsx b/app/(dashboard)/dashboard/year/[year]/month/page.tsx new file mode 100644 index 0000000..99d7bba --- /dev/null +++ b/app/(dashboard)/dashboard/year/[year]/month/page.tsx @@ -0,0 +1,21 @@ +import { getRequestDateNow } from "@/lib/date"; +import { redirect } from "next/navigation"; + +interface SelectedYearPageProps { + params: { year: string }; +} + +export const dynamic = "force-dynamic"; +export const dynamicParams = true; + +export default async function SelectedYearMonthPageRoot({ params }: SelectedYearPageProps) { + const dateNow = getRequestDateNow(); + + if (params.year === dateNow.getFullYear().toString()) { + redirect(`/dashboard/year/${params.year}/month/${dateNow.getMonth()}`); + } + + redirect(`/dashboard/year/${params.year}/month/0`); + + return
...
; +} diff --git a/app/(dashboard)/dashboard/year/[year]/page.tsx b/app/(dashboard)/dashboard/year/[year]/page.tsx new file mode 100644 index 0000000..1fd8690 --- /dev/null +++ b/app/(dashboard)/dashboard/year/[year]/page.tsx @@ -0,0 +1,19 @@ +import { getRequestDateNow } from "@/lib/date"; +import { CalendarYear } from "../../_components/calendar-year"; + +interface SelectedYearPageProps { + params: { year: string }; +} + +export const dynamic = "force-dynamic"; +export const dynamicParams = true; + +export default async function SelectedYearPage({ params }: SelectedYearPageProps) { + const date = new Date(params.year ?? getRequestDateNow().getFullYear()); + + return ( + <> + + + ); +} diff --git a/app/(dashboard)/dashboard/year/layout.tsx b/app/(dashboard)/dashboard/year/layout.tsx new file mode 100644 index 0000000..521f531 --- /dev/null +++ b/app/(dashboard)/dashboard/year/layout.tsx @@ -0,0 +1,20 @@ +import { Suspense } from "react"; +import CompanyBenefits from "../_components/company-benefits"; +import YearlyEconomicOverview from "../_components/yearly-economic-overview"; + +interface YearLayoutProps { + children: React.ReactNode; +} + +export default async function YearLayout({ children }: YearLayoutProps) { + return ( + <> + {children} + + }> + {/* @ts-expect-error Async Server Component */} + + + + ); +} diff --git a/app/(dashboard)/dashboard/year/page.tsx b/app/(dashboard)/dashboard/year/page.tsx new file mode 100644 index 0000000..0f1304b --- /dev/null +++ b/app/(dashboard)/dashboard/year/page.tsx @@ -0,0 +1,11 @@ +import { getRequestDateNow } from "@/lib/date"; +import { redirect } from "next/navigation"; + +export const dynamic = "force-dynamic"; +export const dynamicParams = true; + +export default async function RootYearPage() { + redirect("/dashboard/year/" + getRequestDateNow().getFullYear()); + + return
...
; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..e710cb9 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,36 @@ +import { ThemeProvider } from "@/components/theme-provider"; +import { Toaster } from "@/components/ui/toaster"; +import { cn } from "@/lib/utils"; +import "@/styles/globals.css"; +import { Inter as FontSans } from "next/font/google"; + +const fontSans = FontSans({ + subsets: ["latin"], + variable: "--font-sans", + display: "swap" +}); + +interface RootLayoutProps { + children: React.ReactNode; +} + +export default function RootLayout({ children }: RootLayoutProps) { + return ( + <> + + + + + {children} + + + + + + ); +} diff --git a/app/route.ts b/app/route.ts new file mode 100644 index 0000000..0a12c7a --- /dev/null +++ b/app/route.ts @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export async function GET(request: Request) { + redirect("/login"); +} diff --git a/components/auth/LoginButtons.tsx b/components/auth/LoginButtons.tsx deleted file mode 100644 index c8d81ca..0000000 --- a/components/auth/LoginButtons.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useAuthProviders } from "@/components/auth"; -import { Button } from "@/components/ui"; -import { signIn } from "next-auth/react"; -import * as React from "react"; - -type LoginButtonsProps = {} & React.HTMLAttributes; - -const LoginButtons = ({ onClick, ...other }: LoginButtonsProps) => { - const authProviders = useAuthProviders(); - const providers = React.useMemo(() => Object.keys(authProviders ?? {}), [authProviders]); - - return ( - - ); -}; - -export default LoginButtons; diff --git a/components/auth/hooks/index.tsx b/components/auth/hooks/index.tsx deleted file mode 100644 index 26f6ede..0000000 --- a/components/auth/hooks/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import useAuthProviders from "@/components/auth/hooks/useAuthProviders"; - -export { useAuthProviders }; diff --git a/components/auth/hooks/useAuthProviders.tsx b/components/auth/hooks/useAuthProviders.tsx deleted file mode 100644 index c656c32..0000000 --- a/components/auth/hooks/useAuthProviders.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { getProviders } from "next-auth/react"; -import useSWR from "swr"; - -export default function useAuthProviders() { - const { data } = useSWR("/auth/providers", () => getProviders()); - - return data; -} diff --git a/components/auth/index.tsx b/components/auth/index.tsx deleted file mode 100644 index 89323a8..0000000 --- a/components/auth/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { useAuthProviders } from "@/components/auth/hooks"; -import LoginButtons from "@/components/auth/LoginButtons"; - -export { LoginButtons, useAuthProviders }; diff --git a/components/auth/login-button.tsx b/components/auth/login-button.tsx new file mode 100644 index 0000000..82b728e --- /dev/null +++ b/components/auth/login-button.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import { useSearchParams } from "next/navigation"; +import { Button } from "../ui/button"; + +function LoginButton() { + const searchParams = useSearchParams(); + + return ( + + ); +} + +export { LoginButton }; diff --git a/components/calendar/calendar-day.tsx b/components/calendar/calendar-day.tsx new file mode 100644 index 0000000..a22b730 --- /dev/null +++ b/components/calendar/calendar-day.tsx @@ -0,0 +1,92 @@ +import { cn } from "@/lib/utils"; +import { CalendarDay, CalendarEntries } from "@/types"; +import { ComponentPropsWithoutRef, forwardRef } from "react"; +import { Icons } from "../icons"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "../ui/hover-card"; +import { Show } from "../ui/show"; + +type CalendarDayProps = ComponentPropsWithoutRef<"div"> & { + calendarDay: CalendarEntries; + big?: boolean; + holidayInfos?: CalendarDay[]; +}; + +const CalendarDay = forwardRef, CalendarDayProps>( + ({ className, calendarDay, big = false, holidayInfos = [], ...other }, ref) => { + return ( +
+
+ {big && calendarDay.isStartOfWeek ? ( +
{calendarDay.week}
+ ) : null} +
+ + + {calendarDay.value} + + Edit work day details + + + + {calendarDay.value} + 0}> + + + + + +
+
+ {big && + (calendarDay.type === "day" || calendarDay.type === "spacing") && + holidayInfos + .filter( + x => + x.day === calendarDay.value && + x.weekNumber === calendarDay.week && + !!x?.holidayInformation?.name + ) + .map((x, index) => ( +
+
+ {x.holidayInformation?.name} +
+
+
+ ))} +
+ ); + } +); + +CalendarDay.displayName = "CalendarDay"; + +export { CalendarDay }; diff --git a/components/calendar/components/calendar.tsx b/components/calendar/components/calendar.tsx deleted file mode 100644 index c981ee9..0000000 --- a/components/calendar/components/calendar.tsx +++ /dev/null @@ -1,220 +0,0 @@ -import { CalendarDay } from "@/components/calendar"; -import { useCalendar } from "@/components/calendar/providers/calendarProvider"; -import { Box, Button, Card, Flex, Grid, IconButton, Svg, Text } from "@/components/ui"; -import { CalendarDay as CalendarDayType, WithChildren } from "@/types"; -import { motion } from "framer-motion"; -import * as React from "react"; -import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; -import { useToggle } from "react-use"; - -type CalendarDayColorDescriptionProps = WithChildren<{ - color: string; -}>; - -const CalendarDayColorDescription = ({ children, color }: CalendarDayColorDescriptionProps) => { - return ( - - - - {children} - - - ); -}; - -const CalendarDayHeading = ({ children }: WithChildren<{}>) => { - return ( - - - {children} - - - ); -}; - -const Calendar = ({ ...other }) => { - const { yearName, activeCalendarMonthDetail, years, setYear, incrementMonth, decrementMonth } = - useCalendar(); - - const [showYearPicker, toggleYearPicker] = useToggle(false); - - const renderSpacingDays = (days: CalendarDayType[]): JSX.Element[] | null => { - if (days === null || days === undefined || days?.length <= 0) { - return null; - } - - const spacingDaysToRender: Record = { - MONDAY: 0, - TUESDAY: 1, - WEDNESDAY: 2, - THURSDAY: 3, - FRIDAY: 4, - SATURDAY: 5, - SUNDAY: 6 - }; - - return [...Array(spacingDaysToRender[days[0].name?.toUpperCase()] ?? 0)].map((key, index) => ( -
- )); - }; - - return ( - - - { - toggleYearPicker(false); - decrementMonth(); - }} - > - - - - { - toggleYearPicker(false); - incrementMonth(); - }} - > - - - - - Off work - Work - Non commissioned - - - mon. - tue. - wed. - thu. - fri. - sat. - sun. - {renderSpacingDays(activeCalendarMonthDetail?.days)} - {activeCalendarMonthDetail?.days?.map((day, i) => ( - - ))} - - - {showYearPicker && - years.map(year => ( - { - setYear(year); - toggleYearPicker(); - }} - exit={{ opacity: 0 }} - css={{ - cursor: "pointer", - padding: "$1", - fontSize: "$4", - textAlign: "center" - }} - > - {year} - - ))} - - - - - ); -}; - -export default Calendar; diff --git a/components/calendar/components/calendarDay.tsx b/components/calendar/components/calendarDay.tsx deleted file mode 100644 index 8e69dd3..0000000 --- a/components/calendar/components/calendarDay.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { - Box, - Dialog, - DialogContent, - DialogNonRemoveScrollOverlay, - DialogTrigger, - Svg -} from "@/components/ui"; -import { UserWorkDayDetails, useUser } from "@/components/user"; -import { getUserWorkDayDetails } from "@/logic/userLogic"; -import { CalendarDay as CalendarDayType, UserWorkDayDetail, WithChildren } from "@/types"; -import { Presence } from "@radix-ui/react-presence"; -import * as React from "react"; -import { HiChevronDoubleUp } from "react-icons/hi"; -import { IoBugOutline } from "react-icons/io5"; -import { CSS, styled } from "stitches.config"; - -type BaseDay = { - isWorkDay?: boolean; - isNonCommissionedToggled?: boolean; - isExtraHoursToggled?: boolean; - isSickDayToggled?: boolean; -}; - -type CalendarDayDateProps = WithChildren<{ - as?: React.ElementType; - className?: string; - isExpanded?: boolean; - onClick?: React.MouseEventHandler; - css?: CSS; -}> & - BaseDay; - -type RegularCalendarDayProps = { - day: CalendarDayType; - onExpand: () => void; -} & BaseDay; - -type ExpandedCalendarDayProps = { - day: CalendarDayType; - workDayDetails: UserWorkDayDetail; - onCollapse: () => void; -} & BaseDay; - -type CalendarDayProps = { - day: CalendarDayType; - isWorkDay?: boolean; -}; - -const DayText = styled("div", { - display: "flex", - - fontWeight: "bold", - borderWidth: "2px", - borderStyle: "solid", - borderColor: "transparent", - borderRadius: "$round", - transition: "border-color 0.2s ease-in-out", - variants: { - expanded: { - true: { - height: "auto", - width: "auto", - mb: "$2", - border: "none", - fontSize: "$4" - }, - false: { - height: "$6", - width: "$6", - justifyContent: "center", - alignItems: "center" - } - }, - workDay: { - true: { - color: "$green" - } - }, - nonCommissioned: { - true: { - color: "$red" - } - } - }, - compoundVariants: [ - { - expanded: false, - workDay: true, - css: { - "&:hover": { - borderColor: "$green" - } - } - }, - { - expanded: false, - nonCommissioned: true, - css: { - "&:hover": { - borderColor: "$red" - } - } - } - ] -}); - -const CalendarDayDate = React.forwardRef, CalendarDayDateProps>( - function CalendarDayDate( - { - children, - className, - isWorkDay, - isNonCommissionedToggled, - isExtraHoursToggled, - isSickDayToggled, - isExpanded = false, - ...other - }, - ref - ) { - const boxCss: CSS = isExpanded - ? { - display: "block" - } - : { - display: "flex", - justifyContent: "center", - alignItems: "center", - cursor: "pointer" - }; - - return ( - - - {children} - {isExtraHoursToggled ? : null} - {isSickDayToggled ? : null} - - - ); - } -); - -const RegularCalendarDay = React.forwardRef< - React.ElementRef, - RegularCalendarDayProps ->(function RegularCalendarDay( - { day, isWorkDay = false, isNonCommissionedToggled = false, onExpand = () => {}, ...other }, - ref -) { - return ( - onExpand()} - ref={ref} - {...other} - > - {day?.day} - - ); -}); - -const ExpandedCalendarDay = ({ - day, - isWorkDay = false, - isNonCommissionedToggled = false, - isExtraHoursToggled = false, - workDayDetails, - onCollapse = () => {}, - ...other -}: ExpandedCalendarDayProps) => { - return ( - - - {day?.formattedShortDate} - - - - ); -}; - -const CalendarDay = ({ day, isWorkDay = false, ...other }: CalendarDayProps) => { - const { user } = useUser(); - - const [isExpanded, setIsExpanded] = React.useState(false); - - const workDayDetails = React.useMemo( - () => getUserWorkDayDetails(user, day?.formattedDate), - [user, day?.formattedDate] - ); - - const isNonCommissionedToggled = React.useMemo( - () => workDayDetails?.nonCommissionedHours > 0, - [workDayDetails.nonCommissionedHours] - ); - - const isExtraHoursToggled = React.useMemo( - () => workDayDetails?.extraHours > 0, - [workDayDetails.extraHours] - ); - - const isSickDayToggled = React.useMemo( - () => (workDayDetails?.sickDay ?? false) && isNonCommissionedToggled, - [workDayDetails?.sickDay, isNonCommissionedToggled] - ); - - return ( - setIsExpanded(open)} - overlayProps={{ - enabled: false - }} - > - - setIsExpanded(true)} - {...other} - /> - - - - - - setIsExpanded(false)} - {...other} - /> - - - ); -}; - -export default CalendarDay; diff --git a/components/calendar/components/index.ts b/components/calendar/components/index.ts deleted file mode 100644 index 97c3615..0000000 --- a/components/calendar/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import Calendar from "./calendar"; -import CalendarDay from "./calendarDay"; - -export { Calendar, CalendarDay }; diff --git a/components/calendar/index.ts b/components/calendar/index.ts deleted file mode 100644 index ea9da9e..0000000 --- a/components/calendar/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Calendar, CalendarDay } from "./components"; -import { CalendarProvider, useCalendar } from "./providers"; - -export { Calendar, CalendarDay, CalendarProvider, useCalendar }; diff --git a/components/calendar/month-select.tsx b/components/calendar/month-select.tsx new file mode 100644 index 0000000..cc0c41f --- /dev/null +++ b/components/calendar/month-select.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { MONTH } from "@/constants/date-constants"; +import { useRouter } from "next/navigation"; +import { forwardRef, useTransition, type ComponentPropsWithoutRef, type ElementRef } from "react"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; + +const MonthSelect = forwardRef< + ElementRef, + ComponentPropsWithoutRef & { + year: number; + month: number; + isSelected?: boolean; + } +>(({ className, year, month, isSelected = false, ...other }, forwardedRef) => { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + return ( + + ); +}); + +MonthSelect.displayName = "MonthSelect"; + +export { MonthSelect }; diff --git a/components/calendar/providers/calendarProvider.tsx b/components/calendar/providers/calendarProvider.tsx deleted file mode 100644 index 26efcab..0000000 --- a/components/calendar/providers/calendarProvider.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { getPayDay } from "@/logic/calendarLogic"; -import { CalendarMonth, CalendarYear } from "@/types"; -import { getCalendarYear } from "@/utils/calendar/calendarUtils"; -import { getThisYearAndTwoYearsIntoTheFuture } from "@/utils/dateUtils"; -import * as React from "react"; - -type CalendarContextProps = { - isLoadingCalendar: boolean; - year: CalendarYear; - lastYear: CalendarYear; - nextYear: CalendarYear; - calendarYear: CalendarYear; - yearName: string; - years: number[]; - month: number; - activeCalendarMonthDetail: CalendarMonth; - currentMonthDetail: CalendarMonth; - lastMonthDetail: CalendarMonth; - nextMonthDetail: CalendarMonth; - setYear: (year: number) => void; - incrementYear: () => void; - decrementYear: () => void; - incrementMonth: () => void; - decrementMonth: () => void; - date: Date; -}; - -const CalendarContext = React.createContext(null); -CalendarContext.displayName = "CalendarContext"; - -const now = new Date(); - -const initialState = { - year: now.getFullYear(), - month: now.getMonth() -}; - -const getLastMonth = (month, year, lastYear) => - month === 0 ? lastYear?.months?.[11] : year?.months?.[month - 1]; -const getNextMonth = (month, year, nextYear) => - month === 11 ? nextYear?.months?.[0] : year?.months?.[month + 1]; - -const getDateFromYearDataAndMonth = (year, month) => { - const now = new Date(); - return new Date(year?.year ?? now.getFullYear(), month ?? now.getMonth(), 1); -}; - -function calendarReducer(state = initialState, action) { - switch (action.type) { - case "SET_YEAR": { - return { ...state, year: action.year }; - } - case "SET_MONTH": { - return { ...state, month: action.month }; - } - default: { - throw new Error(`Unhandled action type: ${action.type}`); - } - } -} - -function CalendarProvider({ children, year = initialState.year, month = initialState.month }) { - const [state, dispatch] = React.useReducer(calendarReducer, { year, month }); - - const calendarYear = React.useMemo(() => getCalendarYear(state.year), [state.year]); - - const currentYear = new Date().getFullYear(); - const data = React.useMemo(() => getCalendarYear(currentYear), [currentYear]); - const lastYear = React.useMemo(() => getCalendarYear(currentYear - 1), [currentYear]); - const nextYear = React.useMemo(() => getCalendarYear(currentYear + 1), [currentYear]); - const setYear = (year: number) => dispatch({ type: "SET_YEAR", year: year }); - - const incrementYear = React.useCallback( - () => dispatch({ type: "SET_YEAR", year: state.year + 1 }), - [state.year] - ); - const decrementYear = React.useCallback( - () => dispatch({ type: "SET_YEAR", year: state.year - 1 }), - [state.year] - ); - - const setMonth = month => dispatch({ type: "SET_MONTH", month: month }); - - const incrementMonth = React.useCallback(() => { - const newMonth = state.month + 1; - const shouldIncrementYear = newMonth > 11; - - if (shouldIncrementYear) { - incrementYear(); - } - - dispatch({ type: "SET_MONTH", month: shouldIncrementYear ? 0 : newMonth }); - }, [incrementYear, state.month]); - - const decrementMonth = React.useCallback(() => { - const newMonth = state.month - 1; - const shouldDecrementYear = newMonth < 0; - - if (shouldDecrementYear) { - decrementYear(); - } - - dispatch({ type: "SET_MONTH", month: shouldDecrementYear ? 11 : newMonth }); - }, [decrementYear, state.month]); - - React.useEffect(() => { - setYear(year); - setMonth(month); - }, [year, month]); - - const calendarValue = React.useMemo<{ - date: Date; - yearName: string; - years: number[]; - month: number; - activeCalendarMonthDetail: CalendarMonth; - calendarYear: CalendarYear; - }>(() => { - const date = getDateFromYearDataAndMonth(state.year, state.month); - - const activeCalendarMonthDetail = { - ...calendarYear?.months?.[date.getMonth()], - payDay: getPayDay( - getNextMonth(date.getMonth(), calendarYear, getCalendarYear(+calendarYear.year + 1)) - ) - }; - - return { - date, - calendarYear, - yearName: state.year, - month: state.month, - years: getThisYearAndTwoYearsIntoTheFuture(), - activeCalendarMonthDetail - }; - }, [calendarYear, state.month, state.year]); - - const value = React.useMemo(() => { - const currentMonth = new Date().getMonth(); - - const nextMonthDetail = { - ...getNextMonth(currentMonth, data, nextYear), - payDay: getPayDay( - currentMonth === 11 - ? nextYear?.months?.[1] - : currentMonth === 10 - ? nextYear?.months?.[0] - : data?.months?.[currentMonth + 2] - ) - }; - - const currentMonthDetail = { - ...data?.months?.[currentMonth], - payDay: getPayDay(nextMonthDetail) - }; - - const lastMonthDetail = { - ...getLastMonth(currentMonth, data, lastYear), - payDay: getPayDay(currentMonthDetail) - }; - - return { - year: data, - lastYear, - nextYear, - currentMonthDetail, - lastMonthDetail, - nextMonthDetail, - isLoadingCalendar: !data - }; - }, [data, lastYear, nextYear]); - - return ( - - {children} - - ); -} -function useCalendar() { - const context = React.useContext(CalendarContext); - - if (context === undefined) { - throw new Error("useCalendar must be used within a CalendarProvider"); - } - - return context; -} -export { CalendarProvider, useCalendar }; diff --git a/components/calendar/providers/index.ts b/components/calendar/providers/index.ts deleted file mode 100644 index 6bc61a8..0000000 --- a/components/calendar/providers/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CalendarProvider, useCalendar } from "./calendarProvider"; - -export { CalendarProvider, useCalendar }; diff --git a/components/companyBenefits.tsx b/components/companyBenefits.tsx deleted file mode 100644 index f41fb9d..0000000 --- a/components/companyBenefits.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { Box, Card, Flex, FlexItem, Grid, Heading, Paragraph, Svg } from "@/components/ui"; -import { WithChildren } from "@/types"; -import * as React from "react"; -import { HiOutlineCheckCircle } from "react-icons/hi"; -import { VariantProps } from "stitches.config"; - -type BenefitProps = WithChildren<{ - icon?: React.ElementType; - iconProps?: VariantProps; -}>; - -const Benefit = ({ - children, - icon = HiOutlineCheckCircle, - iconProps = { - variant: "green" - } -}: BenefitProps) => { - const Icon = icon; - return ( - - - - - - {children} - - - ); -}; - -const CompanyBenefits = () => { - return ( - - - - Knowit Experience Bergen - - - Company Benefits - - - You get paid by commission or guaranteed salary. Knowit covers both employer's - national insurance contributions (14.10%) and holyday payment (12%). This means you can - calculate your next payment by the following formulae{" "} - - Work hours in month x hourly rate x commission = your salary. - - - - - Full pay on paternity leave - Pension 5,5% of salary from 1G to 12G - Health insurance for all employees - New training facilities + scheduled training programmes - Free phone with free calls, free SMS and 15 GB data every month - Your choice of computer equipment, PC or Mac, mouse and keyboard - Subsidized good canteen - Generous social budget that guarantees lots of fun - - - ); -}; - -export default CompanyBenefits; diff --git a/components/container.tsx b/components/container.tsx deleted file mode 100644 index 4588aa2..0000000 --- a/components/container.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { WithChildren } from "@/types"; -import * as React from "react"; - -type ContainerProps = WithChildren<{ - as?: React.ElementType; - className?: string; -}>; - -const Container = ({ children, as = "div", ...other }: ContainerProps) => { - const Component = as; - return {children}; -}; - -export default Container; diff --git a/components/feedback/components/feedbackDialog.tsx b/components/feedback/components/feedbackDialog.tsx deleted file mode 100644 index 9be1f0a..0000000 --- a/components/feedback/components/feedbackDialog.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Button, Dialog, DialogContent, DialogTitle, DialogTrigger } from "@/components/ui"; -import * as React from "react"; -import FeedbackForm from "./feedbackForm"; - -const FeedbackDialog = () => { - const [open, setOpen] = React.useState(false); - - return ( - setOpen(open)}> - - - - - Feedback - - - - ); -}; - -export default FeedbackDialog; diff --git a/components/feedback/components/feedbackEmojiSelector.tsx b/components/feedback/components/feedbackEmojiSelector.tsx deleted file mode 100644 index 051d2a1..0000000 --- a/components/feedback/components/feedbackEmojiSelector.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { Flex } from "@/components/ui"; -import Image from "next/image"; -import * as React from "react"; -import { styled } from "stitches.config"; - -const EmojiButton = styled("button", { - all: "unset", - outline: "none", - margin: 0, - padding: 0, - display: "inline-flex", - justifyContent: "center", - alignItems: "center", - width: "34px", - height: "34px", - backgroundColor: "$grayDark", - border: "1px solid $gray", - borderRadius: "$round", - cursor: "pointer", - transition: "all .2s cubic-bezier(.5,-1,.5,2)", - "&:hover": { - transform: "scale(1.12)" - }, - "&:focus": { - transform: "scale(1.12)" - }, - variants: { - selected: { - true: { - transform: "scale(1.12)", - border: "1px solid $green" - } - } - } -}); - -type FeedbackEmojiSelectorProps = { - onSelected: (emojiValue: number) => void; -}; - -const twemojis = { - "1f60d": { - src: "/twemoji/1f60d.svg", - alt: "Emoji with heart eyes", - value: 1 - }, - "1f600": { - src: "/twemoji/1f600.svg", - alt: "Emoji with happy expression", - value: 2 - }, - "1f615": { - src: "/twemoji/1f615.svg", - alt: "Emoji with indifferent expression", - value: 3 - }, - "1f662d": { - src: "/twemoji/1f62d.svg", - alt: "Emoji with sad expression", - value: 4 - } -}; - -const FeedbackEmojiSelector = ({ onSelected = () => {} }: FeedbackEmojiSelectorProps) => { - const [selectedEmoji, setSelectedEmoji] = React.useState(0); - - const handleEmojiSelect = (emojiValue: number) => { - setSelectedEmoji(emojiValue); - onSelected(emojiValue); - }; - - return ( - - {Object.entries(twemojis).map(([emoji, { src, alt, value }]) => ( - handleEmojiSelect(value)} - > - {alt} - - ))} - - ); -}; - -export default FeedbackEmojiSelector; diff --git a/components/feedback/components/feedbackForm.tsx b/components/feedback/components/feedbackForm.tsx deleted file mode 100644 index 28154b6..0000000 --- a/components/feedback/components/feedbackForm.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { Button, Fieldset, Flex, Form, Svg, Text, TextArea, TextField } from "@/components/ui"; -import * as React from "react"; -import { useForm } from "react-hook-form"; -import { IoCheckmark, IoChevronForward, IoSyncOutline } from "react-icons/io5"; -import FeedbackEmojiSelector from "./feedbackEmojiSelector"; - -type FeedbackFormProps = { - compact?: boolean; -}; - -const FeedbackForm = ({ compact = false }: FeedbackFormProps) => { - const [isSubmitting, setIsSubmitting] = React.useState(false); - const [isCompleted, setIsCompleted] = React.useState(false); - const { - register, - handleSubmit, - formState: { errors }, - setValue - } = useForm({ - defaultValues: { - message: "", - reaction: 0 - } - }); - - const onSubmit = async (data: any) => { - setIsSubmitting(true); - - await fetch("/api/user/feedback", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json" - }, - body: JSON.stringify(data) - }); - - setIsSubmitting(false); - setIsCompleted(true); - }; - - return ( -
-
-