diff --git a/bun.lockb b/bun.lockb
index fb32df4..1ed5e7e 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/lighthouserc.cjs b/lighthouserc.cjs
index 6abc5ee..1f06549 100644
--- a/lighthouserc.cjs
+++ b/lighthouserc.cjs
@@ -13,6 +13,7 @@ const config = {
'http://localhost:3000/en/news/1',
'http://localhost:3000/en/storage',
'http://localhost:3000/en/storage/shopping-cart',
+ 'http://localhost:3000/en/shift-schedule',
],
startServerCommand: 'bun run start',
},
diff --git a/messages/en.json b/messages/en.json
index 62e259c..f9b2767 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -38,6 +38,7 @@
"events": "Events",
"storage": "Storage",
"about": "About",
+ "shiftSchedule": "Shift Schedule",
"changeLocale": "Change language",
"toggleTheme": "Toggle theme",
"light": "Light",
@@ -132,5 +133,28 @@
"returnByDescription": "Select how long you would like to borrow the item for.",
"submit": "Submit"
}
+ },
+ "shiftSchedule": {
+ "title": "Shift Schedule",
+ "administratorMenu": {
+ "label": "Administrator Menu",
+ "open": "Open Administrator Menu",
+ "close": "Close Administrator Menu",
+ "clearShiftSchedule": "Clear shift schedule"
+ },
+ "scheduleTable": {
+ "time": "Time",
+ "day": "{day, select, monday {Monday} tuesday {Tuesday} wednesday {Wednesday} thursday {Thursday} other {Friday}}",
+ "scheduleCell": {
+ "onShift": "{count, plural, =0 {Closed} =1 {1 person on shift} other {# people on shift}}",
+ "scheduleCellDialog": {
+ "empty": "No one on shift",
+ "registerSection": {
+ "recurring": "Recurring",
+ "register": "Register"
+ }
+ }
+ }
+ }
}
}
diff --git a/messages/no.json b/messages/no.json
index 0c5810a..8062933 100644
--- a/messages/no.json
+++ b/messages/no.json
@@ -38,6 +38,7 @@
"events": "Hendelser",
"storage": "Lager",
"about": "Om oss",
+ "shiftSchedule": "Vaktliste",
"changeLocale": "Bytt språk",
"toggleTheme": "Bytt tema",
"light": "Lys",
@@ -132,5 +133,28 @@
"returnByDescription": "Velg hvor lenge du ønsker å låne gjenstanden(e)",
"submit": "Send"
}
+ },
+ "shiftSchedule": {
+ "title": "Vaktliste",
+ "administratorMenu": {
+ "label": "Administrator-meny",
+ "open": "Åpne Administrator-meny",
+ "close": "Lukk Administrator-meny",
+ "clearShiftSchedule": "Tøm vaktliste"
+ },
+ "scheduleTable": {
+ "time": "Tid",
+ "day": "{day, select, monday {Mandag} tuesday {Tirsdag} wednesday {Onsdag} thursday {Torsdag} other {Fredag}}",
+ "scheduleCell": {
+ "onShift": "{count, plural, =0 {Stengt} =1 {1 person på vakt} other {# personer på vakt}}",
+ "scheduleCellDialog": {
+ "empty": "Ingen på vakt",
+ "registerSection": {
+ "recurring": "Gjentagende",
+ "register": "Registrer"
+ }
+ }
+ }
+ }
}
}
diff --git a/package.json b/package.json
index 4fdad2a..a496c1d 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,8 @@
"@aws-sdk/client-s3": "^3.679.0",
"@lucia-auth/adapter-drizzle": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.1",
+ "@radix-ui/react-checkbox": "^1.1.2",
+ "@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-label": "^2.1.0",
diff --git a/src/app/[locale]/(default)/shift-schedule/layout.tsx b/src/app/[locale]/(default)/shift-schedule/layout.tsx
new file mode 100644
index 0000000..8aa4078
--- /dev/null
+++ b/src/app/[locale]/(default)/shift-schedule/layout.tsx
@@ -0,0 +1,23 @@
+import { getTranslations, setRequestLocale } from 'next-intl/server';
+
+type ShiftScheduleLayoutProps = {
+ children: React.ReactNode;
+ params: Promise<{ locale: string }>;
+};
+
+export default async function ShiftScheduleLayout({
+ params,
+ children,
+}: ShiftScheduleLayoutProps) {
+ const { locale } = await params;
+
+ setRequestLocale(locale);
+ const t = await getTranslations('shiftSchedule');
+
+ return (
+ <>
+
{t('title')}
+ {children}
+ >
+ );
+}
diff --git a/src/app/[locale]/(default)/shift-schedule/loading.tsx b/src/app/[locale]/(default)/shift-schedule/loading.tsx
new file mode 100644
index 0000000..7086b1d
--- /dev/null
+++ b/src/app/[locale]/(default)/shift-schedule/loading.tsx
@@ -0,0 +1,126 @@
+import { Skeleton } from '@/components/ui/Skeleton';
+import {
+ Table,
+ TableBody,
+ TableCaption,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/Table';
+import { useFormatter, useTranslations } from 'next-intl';
+
+export default function ShiftScheduleLayout() {
+ const t = useTranslations('shiftSchedule.scheduleTable');
+ const format = useFormatter();
+
+ const days = [
+ 'monday',
+ 'tuesday',
+ 'wednesday',
+ 'thursday',
+ 'friday',
+ ] as const;
+ const timeslots = ['first', 'second', 'third', 'fourth'] as const;
+
+ function getDateTimeRange(timeslot: string) {
+ let firstDate: Date;
+ let secondDate: Date;
+
+ switch (timeslot) {
+ case timeslots[0]:
+ firstDate = new Date(0, 0, 0, 10, 15, 0, 0);
+ secondDate = new Date(0, 0, 0, 12, 7, 0, 0);
+ break;
+
+ case timeslots[1]:
+ firstDate = new Date(0, 0, 0, 12, 7, 0, 0);
+ secondDate = new Date(0, 0, 0, 14, 7, 0, 0);
+ break;
+
+ case timeslots[2]:
+ firstDate = new Date(0, 0, 0, 14, 7, 0, 0);
+ secondDate = new Date(0, 0, 0, 16, 7, 0, 0);
+ break;
+
+ case timeslots[3]:
+ firstDate = new Date(0, 0, 0, 16, 7, 0, 0);
+ secondDate = new Date(0, 0, 0, 18, 0, 0, 0);
+ break;
+
+ default:
+ firstDate = new Date();
+ secondDate = new Date();
+ }
+
+ return format.dateTimeRange(firstDate, secondDate, {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ });
+ }
+
+ return (
+ <>
+ {/* Table shown on small screens */}
+
+ {days.map((day) => (
+
+
+
+ {t('time')}
+
+ {t('day', { day: day })}
+
+
+
+
+ {timeslots.map((timeslot) => (
+
+
+ {getDateTimeRange(timeslot)}
+
+
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+ {/* Table shown on all other screens */}
+
+
+
+ {t('time')}
+ {days.map((day) => (
+
+ {t('day', { day: day })}
+
+ ))}
+
+
+
+ {timeslots.map((timeslot) => (
+
+
+ {getDateTimeRange(timeslot)}
+
+ {days.map((day) => (
+
+
+
+ ))}
+
+ ))}
+
+ [skill icons legend]
+
+ >
+ );
+}
diff --git a/src/app/[locale]/(default)/shift-schedule/page.tsx b/src/app/[locale]/(default)/shift-schedule/page.tsx
new file mode 100644
index 0000000..decde74
--- /dev/null
+++ b/src/app/[locale]/(default)/shift-schedule/page.tsx
@@ -0,0 +1,42 @@
+import { AdministratorMenu } from '@/components/shift-schedule/AdministratorMenu';
+import { ScheduleTable } from '@/components/shift-schedule/ScheduleTable';
+import { shiftScheduleMockData } from '@/mock-data/shiftSchedule';
+import { getTranslations, setRequestLocale } from 'next-intl/server';
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ const t = await getTranslations({ locale, namespace: 'layout' });
+
+ return {
+ title: t('shiftSchedule'),
+ };
+}
+
+export default async function ShiftSchedulePage({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+
+ setRequestLocale(locale);
+ const t = await getTranslations('shiftSchedule');
+
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/components/shift-schedule/AdministratorMenu.tsx b/src/components/shift-schedule/AdministratorMenu.tsx
new file mode 100644
index 0000000..3548912
--- /dev/null
+++ b/src/components/shift-schedule/AdministratorMenu.tsx
@@ -0,0 +1,52 @@
+'use client';
+
+import { Button } from '@/components/ui/Button';
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from '@/components/ui/Collapsible';
+import { ChevronDownIcon, ChevronUpIcon, Trash2Icon } from 'lucide-react';
+import { useState } from 'react';
+
+type AdministratorMenuProps = {
+ t: {
+ label: string;
+ open: string;
+ close: string;
+ clearShiftSchedule: string;
+ };
+};
+
+function AdministratorMenu({ t }: AdministratorMenuProps) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+
+
+ {t.label}
+
+
+
+
+
+
+
+
+ );
+}
+
+export { AdministratorMenu };
diff --git a/src/components/shift-schedule/RegisterShift.tsx b/src/components/shift-schedule/RegisterShift.tsx
new file mode 100644
index 0000000..801dc2f
--- /dev/null
+++ b/src/components/shift-schedule/RegisterShift.tsx
@@ -0,0 +1,23 @@
+import { Button } from '@/components/ui/Button';
+import { Checkbox } from '@/components/ui/Checkbox';
+import { Label } from '@/components/ui/Label';
+import { cx } from '@/lib/utils';
+import { useTranslations } from 'next-intl';
+
+function RegisterShift({ className }: { className?: string }) {
+ const t = useTranslations(
+ 'shiftSchedule.scheduleTable.scheduleCell.scheduleCellDialog.registerSection',
+ );
+
+ return (
+
+
+
+
+ );
+}
+
+export { RegisterShift };
diff --git a/src/components/shift-schedule/ScheduleCell.tsx b/src/components/shift-schedule/ScheduleCell.tsx
new file mode 100644
index 0000000..8cd360e
--- /dev/null
+++ b/src/components/shift-schedule/ScheduleCell.tsx
@@ -0,0 +1,58 @@
+import { ScheduleCellDialog } from '@/components/shift-schedule/ScheduleCellDialog';
+import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/Dialog';
+import { TableCell } from '@/components/ui/Table';
+import { cx } from '@/lib/utils';
+import { UserIcon, UsersIcon } from 'lucide-react';
+import { useTranslations } from 'next-intl';
+
+type ScheduleCellProps = {
+ tDialog: {
+ day: string;
+ time: string;
+ };
+ members: {
+ name: string;
+ }[];
+};
+
+function ScheduleCell({ tDialog, members }: ScheduleCellProps) {
+ const t = useTranslations('shiftSchedule.scheduleTable.scheduleCell');
+
+ return (
+
+
+
+ );
+}
+
+export { ScheduleCell };
diff --git a/src/components/shift-schedule/ScheduleCellDialog.tsx b/src/components/shift-schedule/ScheduleCellDialog.tsx
new file mode 100644
index 0000000..248d3bf
--- /dev/null
+++ b/src/components/shift-schedule/ScheduleCellDialog.tsx
@@ -0,0 +1,47 @@
+import { RegisterShift } from '@/components/shift-schedule/RegisterShift';
+import { DialogHeader, DialogTitle } from '@/components/ui/Dialog';
+import { useTranslations } from 'next-intl';
+
+type ScheduleCellDialogProps = {
+ tDialog: {
+ day: string;
+ time: string;
+ };
+ members: {
+ name: string;
+ }[];
+};
+
+function ScheduleCellDialog({ tDialog, members }: ScheduleCellDialogProps) {
+ const t = useTranslations(
+ 'shiftSchedule.scheduleTable.scheduleCell.scheduleCellDialog',
+ );
+
+ return (
+ <>
+
+
+ {tDialog.day}
+ {tDialog.time}
+
+
+
+ {members.length === 0 ? (
+
{t('empty')}
+ ) : (
+
+ {members.map((member) => (
+
+ ))}
+
+ )}
+
+
+ >
+ );
+}
+
+export { ScheduleCellDialog };
diff --git a/src/components/shift-schedule/ScheduleTable.tsx b/src/components/shift-schedule/ScheduleTable.tsx
new file mode 100644
index 0000000..a438008
--- /dev/null
+++ b/src/components/shift-schedule/ScheduleTable.tsx
@@ -0,0 +1,160 @@
+import { ScheduleCell } from '@/components/shift-schedule/ScheduleCell';
+import {
+ Table,
+ TableBody,
+ TableCaption,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/Table';
+import { useFormatter, useTranslations } from 'next-intl';
+
+type ScheduleEntryProps = {
+ members: {
+ name: string;
+ }[];
+};
+
+type ScheduleDayProps = {
+ first: ScheduleEntryProps;
+ second: ScheduleEntryProps;
+ third: ScheduleEntryProps;
+ fourth: ScheduleEntryProps;
+};
+
+type ScheduleTableProps = {
+ week: {
+ monday: ScheduleDayProps;
+ tuesday: ScheduleDayProps;
+ wednesday: ScheduleDayProps;
+ thursday: ScheduleDayProps;
+ friday: ScheduleDayProps;
+ };
+};
+
+function ScheduleTable({ week }: ScheduleTableProps) {
+ const t = useTranslations('shiftSchedule.scheduleTable');
+ const format = useFormatter();
+
+ const days = [
+ 'monday',
+ 'tuesday',
+ 'wednesday',
+ 'thursday',
+ 'friday',
+ ] as const;
+ const timeslots = ['first', 'second', 'third', 'fourth'] as const;
+
+ function getDateTimeRange(timeslot: string) {
+ let firstDate: Date;
+ let secondDate: Date;
+
+ switch (timeslot) {
+ case timeslots[0]:
+ firstDate = new Date(0, 0, 0, 10, 15, 0, 0);
+ secondDate = new Date(0, 0, 0, 12, 7, 0, 0);
+ break;
+
+ case timeslots[1]:
+ firstDate = new Date(0, 0, 0, 12, 7, 0, 0);
+ secondDate = new Date(0, 0, 0, 14, 7, 0, 0);
+ break;
+
+ case timeslots[2]:
+ firstDate = new Date(0, 0, 0, 14, 7, 0, 0);
+ secondDate = new Date(0, 0, 0, 16, 7, 0, 0);
+ break;
+
+ case timeslots[3]:
+ firstDate = new Date(0, 0, 0, 16, 7, 0, 0);
+ secondDate = new Date(0, 0, 0, 18, 0, 0, 0);
+ break;
+
+ default:
+ firstDate = new Date();
+ secondDate = new Date();
+ }
+
+ return format.dateTimeRange(firstDate, secondDate, {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ });
+ }
+
+ return (
+ <>
+ {/* Table shown on small screens */}
+
+ {days.map((day) => (
+
+
+
+ {t('time')}
+
+ {t('day', { day: day })}
+
+
+
+
+ {timeslots.map((timeslot) => (
+
+
+ {getDateTimeRange(timeslot)}
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+ {/* Table shown on all other screens */}
+
+
+
+ {t('time')}
+ {days.map((day) => (
+
+ {t('day', { day: day })}
+
+ ))}
+
+
+
+ {timeslots.map((timeslot) => (
+
+
+ {getDateTimeRange(timeslot)}
+
+ {days.map((day) => (
+
+ ))}
+
+ ))}
+
+ [skill icons legend]
+
+ >
+ );
+}
+
+export { ScheduleTable };
diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx
new file mode 100644
index 0000000..e4f5cd9
--- /dev/null
+++ b/src/components/ui/Checkbox.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
+import { Check } from 'lucide-react';
+import { forwardRef } from 'react';
+
+import { cx } from '@/lib/utils';
+
+const Checkbox = forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+
+));
+Checkbox.displayName = CheckboxPrimitive.Root.displayName;
+
+export { Checkbox };
diff --git a/src/components/ui/Collapsible.tsx b/src/components/ui/Collapsible.tsx
new file mode 100644
index 0000000..86ab87d
--- /dev/null
+++ b/src/components/ui/Collapsible.tsx
@@ -0,0 +1,11 @@
+'use client';
+
+import * as CollapsiblePrimitive from '@radix-ui/react-collapsible';
+
+const Collapsible = CollapsiblePrimitive.Root;
+
+const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
+
+const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
+
+export { Collapsible, CollapsibleTrigger, CollapsibleContent };
diff --git a/src/lib/locale/index.ts b/src/lib/locale/index.ts
index a4bb4b6..0166353 100644
--- a/src/lib/locale/index.ts
+++ b/src/lib/locale/index.ts
@@ -43,6 +43,10 @@ const routing = defineRouting({
en: '/storage/shopping-cart',
no: '/lager/handlekurv',
},
+ '/shift-schedule': {
+ en: '/shift-schedule',
+ no: '/vaktliste',
+ },
},
});
diff --git a/src/mock-data/shiftSchedule.ts b/src/mock-data/shiftSchedule.ts
new file mode 100644
index 0000000..d6090d6
--- /dev/null
+++ b/src/mock-data/shiftSchedule.ts
@@ -0,0 +1,91 @@
+const shiftScheduleMockData = {
+ monday: {
+ first: {
+ members: [{ name: 'En person' }],
+ },
+ second: {
+ members: [{ name: 'En person' }],
+ },
+ third: {
+ members: [
+ { name: 'En person' },
+ { name: 'En annen person' },
+ { name: 'Person 3' },
+ ],
+ },
+ fourth: {
+ members: [{ name: 'En person' }],
+ },
+ },
+ tuesday: {
+ first: {
+ members: [{ name: 'En person' }],
+ },
+ second: {
+ members: [{ name: 'En person' }],
+ },
+ third: {
+ members: [
+ { name: 'En person' },
+ { name: 'En annen person' },
+ { name: 'Person 3' },
+ ],
+ },
+ fourth: {
+ members: [{ name: 'En person' }],
+ },
+ },
+ wednesday: {
+ first: {
+ members: [{ name: 'En person' }, { name: 'En annen person' }],
+ },
+ second: {
+ members: [],
+ },
+ third: {
+ members: [{ name: 'En person' }, { name: 'En annen person' }],
+ },
+ fourth: {
+ members: [{ name: 'En person' }, { name: 'En annen person' }],
+ },
+ },
+ thursday: {
+ first: {
+ members: [{ name: 'En person' }],
+ },
+ second: {
+ members: [
+ { name: 'En person' },
+ { name: 'En annen person' },
+ { name: 'Person 3' },
+ ],
+ },
+ third: {
+ members: [],
+ },
+ fourth: {
+ members: [],
+ },
+ },
+ friday: {
+ first: {
+ members: [],
+ },
+ second: {
+ members: [
+ {
+ name: 'En person med veldig langt navn så jeg kan teste navn som bruker to linjer',
+ },
+ { name: 'En annen person' },
+ ],
+ },
+ third: {
+ members: [{ name: 'En person' }],
+ },
+ fourth: {
+ members: [{ name: 'En person' }],
+ },
+ },
+};
+
+export { shiftScheduleMockData };