diff --git a/package.json b/package.json index ee547a0..f55e5c0 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-tabs": "^1.1.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "lucide-react": "^0.511.0", "next": "15.3.3", "next-auth": "^5.0.0-beta.25", diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx index fcee5ad..6149d47 100644 --- a/src/app/home/page.tsx +++ b/src/app/home/page.tsx @@ -4,6 +4,7 @@ import { Calendar, momentLocalizer } from 'react-big-calendar'; import moment from 'moment'; import 'react-big-calendar/lib/css/react-big-calendar.css'; import 'react-big-calendar/lib/addons/dragAndDrop/styles.css'; +import CustomToolbar from '@/components/custom-toolbar'; moment.updateLocale('en', { week: { @@ -24,6 +25,12 @@ const MyCalendar = (props) => ( style={{ height: 500 }} culture="de-DE" defaultView='week' + + /*CustomToolbar*/ + components={{ + toolbar: CustomToolbar + }} + /*CustomToolbar*/ /> ) diff --git a/src/components/custom-toolbar.css b/src/components/custom-toolbar.css new file mode 100644 index 0000000..16e86ed --- /dev/null +++ b/src/components/custom-toolbar.css @@ -0,0 +1,139 @@ +/* custom-toolbar.css */ + +/* Container der Toolbar */ +.custom-toolbar { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + background-color: #ffffff; + border: 1px solid #e0e0e0; + /*border-radius: 8px;*/ + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +/* Style für den Bereich, in dem die Ansichten (Month, Week, etc.) gewechselt werden */ +.custom-toolbar .view-change .view-switcher { + display: flex; + gap: 8px; + justify-content: center; +} + +.custom-toolbar .view-change .view-switcher button { + padding: 8px 16px; + background-color: #c1830d; + /*border: 1px solid #ccc;*/ + border-radius: 11px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s, border-color 0.2s; + height: 30px; + margin-top: 3.5px; + color: #ffffff; +} + +.custom-toolbar .view-change .view-switcher button:hover:not(:disabled) { + background-color: #e0e0e0; + border-color: #999; +} + +.custom-toolbar .view-change .view-switcher button:disabled { + background-color: #d0d0d0; + border-color: #aaa; + cursor: default; +} + +/* Anzeige des aktuellen Datums (Monat und Jahr) */ +.custom-toolbar .current-date { + font-weight: bold; + font-size: 12px; + text-align: center; + color: #ffffff; + margin: 4px 0; + background-color: #717171; + width: 178px; + height: 37px; + border-radius: 11px; +} + +/* Navigationsbereich (Today, Prev, Next) */ +.custom-toolbar .navigation-controls { + display: flex; + gap: 8px; + justify-content: center; +} + +.custom-toolbar .navigation-controls button { + padding: 8px 12px; + /*background-color: #2196F3;*/ + color: #ffffff; + border: none; + border-radius: 11px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s; +} + +.custom-toolbar .navigation-controls button:hover { + background-color: #1976D2; +} + +.custom-toolbar .navigation-controls button:active { + background-color: #1565C0; +} + +/* Dropdown-Bereich für Woche und Jahr */ +.custom-toolbar .dropdowns { + display: flex; + gap: 8px; + justify-content: center; + height: 30px; + font-size: 10px; + margin-top: 3.5px; + border-radius: 11px; +} + +.custom-toolbar .dropdowns select { + padding: 8px 12px; + /*border: 1px solid #ccc;*/ + border-radius: 11px; + font-size: 10px; + background-color: #555555; + color: #ffffff; + cursor: pointer; + transition: border-color 0.2s; +} + +.custom-toolbar .dropdowns select:hover { + border-color: #999; +} + +.right-section { + background-color: #717171; + width: 393px; + height: 37px; + border-radius: 11px; +} + +.custom-toolbar .navigation-controls .handleWeek button { + background-color: #717171; + height: 30px; + width: 30px; + margin-bottom: 3.5px; +} + +.custom-toolbar .navigation-controls .today button { + background-color: #c6c6c6; + height: 30px; + width: 100px; + color: #000000; + margin-top: 3.5px; +} + +.view-change { + background-color: #717171; + height: 37px; + width: 290px; + border-radius: 11px; +} diff --git a/src/components/custom-toolbar.tsx b/src/components/custom-toolbar.tsx new file mode 100644 index 0000000..10feafa --- /dev/null +++ b/src/components/custom-toolbar.tsx @@ -0,0 +1,218 @@ +import React, { useState, useEffect } from 'react'; +import { format } from 'date-fns'; +import './custom-toolbar.css'; + +interface CustomToolbarProps { + // Das aktuell angezeigte Datum (wird z. B. von der Calendar-Komponente geliefert) + date: Date; + // Aktuelle Ansicht: "month", "week", "day" oder "agenda" + view: 'month' | 'week' | 'day' | 'agenda'; + /** + * onNavigate ermöglicht das Wechseln des angezeigten Datums. + * Action kann bspw. 'TODAY' oder 'SET_DATE' sein; newDate wird übergeben, wenn benötigt. + */ + onNavigate: (action: string, newDate?: Date) => void; + // onView wechselt die Ansicht + onView: (newView: 'month' | 'week' | 'day' | 'agenda') => void; +} + +const CustomToolbar: React.FC = ({ date, view, onNavigate, onView }) => { + + // Hilfsfunktion, um die ISO-Wochennummer eines Datums zu ermitteln + const getISOWeek = (date: Date): number => { + const tmp = new Date(date.getTime()); + // Verschiebe das Datum so, dass der nächste Donnerstag erreicht wird (ISO: Woche beginnt am Montag) + tmp.setDate(tmp.getDate() + 4 - (tmp.getDay() || 7)); + const yearStart = new Date(tmp.getFullYear(), 0, 1); + const weekNo = Math.ceil((((tmp.getTime() - yearStart.getTime()) / 86400000) + 1) / 7); + return weekNo; + }; + + // Neue Funktion: Ermittelt das ISO-Wochenjahr eines Datums. + // Das ISO-Wochenjahr entspricht dem Jahr des Donnerstags in dieser Woche. + const getISOWeekYear = (date: Date): number => { + const tmp = new Date(date.getTime()); + tmp.setDate(tmp.getDate() + 4 - (tmp.getDay() || 7)); + return tmp.getFullYear(); + }; + + // Ermittelt die Anzahl der ISO-Wochen im Jahr + const getISOWeeksInYear = (year: number): number => { + const d = new Date(year, 11, 31); + const week = getISOWeek(d); + return week === 1 ? getISOWeek(new Date(year, 11, 24)) : week; + }; + + /* + Berechnet den Montag der gewünschten ISO-Woche eines Jahres. + Wir ermitteln zunächst den ersten Montag der ersten ISO-Woche und addieren dann (week - 1) * 7 Tage. + */ + const getDateOfISOWeek = (week: number, year: number): Date => { + const jan1 = new Date(year, 0, 1); + const dayOfWeek = jan1.getDay(); + const isoDayOfWeek = dayOfWeek === 0 ? 7 : dayOfWeek; + let firstMonday: Date; + if (isoDayOfWeek <= 4) { + // Jan 1 gehört zur ersten ISO-Woche – bestimme den Montag dieser Woche + firstMonday = new Date(year, 0, 1 - isoDayOfWeek + 1); + } else { + // Andernfalls liegt der erste Montag in der darauffolgenden Woche + firstMonday = new Date(year, 0, 1 + (8 - isoDayOfWeek)); + } + firstMonday.setDate(firstMonday.getDate() + (week - 1) * 7); + return firstMonday; + }; + + // Lokaler State für Woche und ISO-Wochenjahr (statt des reinen Kalenderjahrs) + const [selectedWeek, setSelectedWeek] = useState(getISOWeek(date)); + const [selectedYear, setSelectedYear] = useState(getISOWeekYear(date)); + + // Aktualisiere die Auswahl, wenn sich die Prop "date" ändert + useEffect(() => { + setSelectedWeek(getISOWeek(date)); + setSelectedYear(getISOWeekYear(date)); + }, [date]); + + // Für die Dropdown-Liste der Wochen: Liste von 1 bis totalWeeks + const totalWeeks = getISOWeeksInYear(selectedYear); + const weekOptions = Array.from({ length: totalWeeks }, (_, i) => i + 1); + + // Beispielhafte Jahresliste: aktuelles ISO-Wochenjahr ± 10 + const yearOptions = Array.from({ length: 21 }, (_, i) => selectedYear - 10 + i); + + // Berechne den Start (Montag) und das Ende (Sonntag) der aktuell angezeigten Woche + const weekStartDate = getDateOfISOWeek(selectedWeek, selectedYear); + const weekEndDate = new Date(weekStartDate); + weekEndDate.setDate(weekStartDate.getDate() + 6); + + // Ermittele Monat und Jahr von Start- und Enddatum (normales Kalenderjahr) + const monthStart = format(weekStartDate, 'MMMM'); + const monthEnd = format(weekEndDate, 'MMMM'); + const yearAtStart = format(weekStartDate, 'yyyy'); + const yearAtEnd = format(weekEndDate, 'yyyy'); + + // Erstelle das Label: + // 1. Falls der Wochenanfang und das Wochenende in unterschiedlichen Jahren liegen, + // wird z. B. "Dezember 2025 - Januar 2026" angezeigt. + // 2. Liegen beide im gleichen Jahr, wird unterschieden zwischen gleichem Monat und unterschiedlichem Monat. + let dateLabel: string; + if (yearAtStart !== yearAtEnd) { + dateLabel = `${monthStart} ${yearAtStart} - ${monthEnd} ${yearAtEnd}`; + } else if (monthStart !== monthEnd) { + dateLabel = `${monthStart} - ${monthEnd} ${yearAtStart}`; + } else { + dateLabel = `${monthStart} ${yearAtStart}`; + } + + // Handler zum Wechseln der Ansicht + const handleViewChange = (newView: 'month' | 'week' | 'day' | 'agenda') => { + onView(newView); + }; + + // "Today"-Button: setzt das Datum auf das heutige Datum (unter Verwendung des ISO-Wochenjahrs) + const handleToday = () => { + const today = new Date(); + setSelectedWeek(getISOWeek(today)); + setSelectedYear(getISOWeekYear(today)); + onNavigate('TODAY', today); + }; + + // Wechselt zur vorherigen Woche. Bei Woche < 1, wird ins Vorjahr gewechselt. + const handlePrevWeek = () => { + let newWeek = selectedWeek - 1; + let newYear = selectedYear; + if (newWeek < 1) { + newYear = selectedYear - 1; + newWeek = getISOWeeksInYear(newYear); + } + setSelectedWeek(newWeek); + setSelectedYear(newYear); + const newDate = getDateOfISOWeek(newWeek, newYear); + onNavigate('SET_DATE', newDate); + }; + + // Wechselt zur nächsten Woche. Überschreitet die Woche die maximale Zahl, wechselt ins nächste Jahr. + const handleNextWeek = () => { + let newWeek = selectedWeek + 1; + let newYear = selectedYear; + if (newWeek > getISOWeeksInYear(selectedYear)) { + newYear = selectedYear + 1; + newWeek = 1; + } + setSelectedWeek(newWeek); + setSelectedYear(newYear); + const newDate = getDateOfISOWeek(newWeek, newYear); + onNavigate('SET_DATE', newDate); + }; + + // Handler, wenn der Nutzer über das Dropdown eine Woche auswählt + const handleWeekChange = (event: React.ChangeEvent) => { + const newWeek = parseInt(event.target.value, 10); + setSelectedWeek(newWeek); + const newDate = getDateOfISOWeek(newWeek, selectedYear); + onNavigate('SET_DATE', newDate); + }; + + // Handler, wenn der Nutzer über das Dropdown ein Jahr auswählt + const handleYearChange = (event: React.ChangeEvent) => { + const newYear = parseInt(event.target.value, 10); + setSelectedYear(newYear); + const totalWeeksInNewYear = getISOWeeksInYear(newYear); + const newWeek = Math.min(selectedWeek, totalWeeksInNewYear); + setSelectedWeek(newWeek); + const newDate = getDateOfISOWeek(newWeek, newYear); + onNavigate('SET_DATE', newDate); + }; + + return ( +
+ {/* Anzeige des Datums-Labels */} +
+ {dateLabel} +
+ + {/* Ansicht wechseln */} +
+
+ + + + +
+
+ +
+ {/* Navigationsbuttons */} +
+
+ + +
+
+ +
+
+ + {/* DropDowns für Woche und Jahr */} +
+ + +
+
+
+ ); +}; + +export default CustomToolbar; diff --git a/yarn.lock b/yarn.lock index e035d84..7c153e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2232,33 +2232,6 @@ __metadata: languageName: node linkType: hard -"bytes@npm:3.1.2, bytes@npm:^3.1.2": - version: 3.1.2 - resolution: "bytes@npm:3.1.2" - checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e - languageName: node - linkType: hard - -"cacache@npm:^19.0.1": - version: 19.0.1 - resolution: "cacache@npm:19.0.1" - dependencies: - "@npmcli/fs": "npm:^4.0.0" - fs-minipass: "npm:^3.0.0" - glob: "npm:^10.2.2" - lru-cache: "npm:^10.0.1" - minipass: "npm:^7.0.3" - minipass-collect: "npm:^2.0.1" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^7.0.2" - ssri: "npm:^12.0.0" - tar: "npm:^7.4.3" - unique-filename: "npm:^4.0.0" - checksum: 10c0/01f2134e1bd7d3ab68be851df96c8d63b492b1853b67f2eecb2c37bb682d37cb70bb858a16f2f0554d3c0071be6dfe21456a1ff6fa4b7eed996570d6a25ffe9c - languageName: node - linkType: hard - "call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": version: 1.0.2 resolution: "call-bind-apply-helpers@npm:1.0.2" @@ -2338,6 +2311,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^1.2.1": + version: 1.2.1 + resolution: "clsx@npm:1.2.1" + checksum: 10c0/34dead8bee24f5e96f6e7937d711978380647e936a22e76380290e35486afd8634966ce300fc4b74a32f3762c7d4c0303f442c3e259f4ce02374eb0c82834f27 + languageName: node + linkType: hard + "clsx@npm:^2.1.1": version: 2.1.1 resolution: "clsx@npm:2.1.1" @@ -2453,6 +2433,13 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^4.1.0": + version: 4.1.0 + resolution: "date-fns@npm:4.1.0" + checksum: 10c0/b79ff32830e6b7faa009590af6ae0fb8c3fd9ffad46d930548fbb5acf473773b4712ae887e156ba91a7b3dc30591ce0f517d69fd83bd9c38650fdc03b4e0bac8 + languageName: node + linkType: hard + "dayjs@npm:^1.11.7": version: 1.11.13 resolution: "dayjs@npm:1.11.13" @@ -3249,15 +3236,6 @@ __metadata: languageName: node linkType: hard -"get-user-locale@npm:^2.2.1": - version: 2.3.2 - resolution: "get-user-locale@npm:2.3.2" - dependencies: - mem: "npm:^8.0.0" - checksum: 10c0/2796b3fc3782b1f4826f31e899642cf72eeb23e296e1cf55280aab5caf7a25f4b906491ee1508a001519d6a410902ccf8fa8edaa895b7aee5dfd422ffe5523b9 - languageName: node - linkType: hard - "glob-parent@npm:^5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -3951,7 +3929,14 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.4.0": +"lodash@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c + languageName: node + linkType: hard + +"loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -3987,25 +3972,6 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^14.0.3": - version: 14.0.3 - resolution: "make-fetch-happen@npm:14.0.3" - dependencies: - "@npmcli/agent": "npm:^3.0.0" - cacache: "npm:^19.0.1" - http-cache-semantics: "npm:^4.1.1" - minipass: "npm:^7.0.2" - minipass-fetch: "npm:^4.0.0" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^1.0.0" - proc-log: "npm:^5.0.0" - promise-retry: "npm:^2.0.1" - ssri: "npm:^12.0.0" - checksum: 10c0/c40efb5e5296e7feb8e37155bde8eb70bc57d731b1f7d90e35a092fde403d7697c56fb49334d92d330d6f1ca29a98142036d6480a12681133a0a1453164cb2f0 - languageName: node - linkType: hard - "math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -4041,6 +4007,7 @@ __metadata: "@types/react-dom": "npm:19.1.6" class-variance-authority: "npm:^0.7.1" clsx: "npm:^2.1.1" + date-fns: "npm:^4.1.0" dotenv-cli: "npm:8.0.0" eslint: "npm:9.28.0" eslint-config-next: "npm:15.3.3" @@ -4051,8 +4018,9 @@ __metadata: next-themes: "npm:^0.4.6" postcss: "npm:8.5.4" prettier: "npm:3.5.3" - prisma: "npm:6.7.0" - react: "npm:^19.0.0" + prisma: "npm:6.8.2" + react: "npm:^19.1.0" + react-big-calendar: "npm:^1.18.0" react-dom: "npm:^19.0.0" tailwind-merge: "npm:^3.2.0" tailwindcss: "npm:4.1.8" @@ -4061,10 +4029,10 @@ __metadata: languageName: unknown linkType: soft -"merge-descriptors@npm:^2.0.0": - version: 2.0.0 - resolution: "merge-descriptors@npm:2.0.0" - checksum: 10c0/95389b7ced3f9b36fbdcf32eb946dc3dd1774c2fdf164609e55b18d03aa499b12bd3aae3a76c1c7185b96279e9803525550d3eb292b5224866060a288f335cb3 +"memoize-one@npm:^6.0.0": + version: 6.0.0 + resolution: "memoize-one@npm:6.0.0" + checksum: 10c0/45c88e064fd715166619af72e8cf8a7a17224d6edf61f7a8633d740ed8c8c0558a4373876c9b8ffc5518c2b65a960266adf403cc215cb1e90f7e262b58991f54 languageName: node linkType: hard @@ -4085,22 +4053,6 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:^1.54.0": - version: 1.54.0 - resolution: "mime-db@npm:1.54.0" - checksum: 10c0/8d907917bc2a90fa2df842cdf5dfeaf509adc15fe0531e07bb2f6ab15992416479015828d6a74200041c492e42cce3ebf78e5ce714388a0a538ea9c53eece284 - languageName: node - linkType: hard - -"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1": - version: 3.0.1 - resolution: "mime-types@npm:3.0.1" - dependencies: - mime-db: "npm:^1.54.0" - checksum: 10c0/bd8c20d3694548089cf229016124f8f40e6a60bbb600161ae13e45f793a2d5bb40f96bbc61f275836696179c77c1d6bf4967b2a75e0a8ad40fe31f4ed5be4da5 - languageName: node - linkType: hard - "minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -4406,13 +4358,6 @@ __metadata: languageName: node linkType: hard -"p-defer@npm:^1.0.0": - version: 1.0.0 - resolution: "p-defer@npm:1.0.0" - checksum: 10c0/ed603c3790e74b061ac2cb07eb6e65802cf58dce0fbee646c113a7b71edb711101329ad38f99e462bd2e343a74f6e9366b496a35f1d766c187084d3109900487 - languageName: node - linkType: hard - "p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" @@ -4612,25 +4557,6 @@ __metadata: languageName: node linkType: hard -"react-calendar@npm:^5.1.0": - version: 5.1.0 - resolution: "react-calendar@npm:5.1.0" - dependencies: - "@wojtekmaj/date-utils": "npm:^1.1.3" - clsx: "npm:^2.0.0" - get-user-locale: "npm:^2.2.1" - warning: "npm:^4.0.0" - peerDependencies: - "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10c0/27673f639c5d6296342a2a888436b31a5d602faeaae01be83b2beb98ff568b0a3d1514f5cc50fcacf3ac50b9c0b9d2fb423b0c001a8f5f1a22816671409e2616 - languageName: node - linkType: hard - "react-dom@npm:^19.0.0": version: 19.1.0 resolution: "react-dom@npm:19.1.0" @@ -5560,15 +5486,6 @@ __metadata: languageName: node linkType: hard -"warning@npm:^4.0.0": - version: 4.0.3 - resolution: "warning@npm:4.0.3" - dependencies: - loose-envify: "npm:^1.0.0" - checksum: 10c0/aebab445129f3e104c271f1637fa38e55eb25f968593e3825bd2f7a12bd58dc3738bb70dc8ec85826621d80b4acfed5a29ebc9da17397c6125864d72301b937e - languageName: node - linkType: hard - "which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": version: 1.1.1 resolution: "which-boxed-primitive@npm:1.1.1"