From d318e6881e0475a0efb8e804288eb26a63699352 Mon Sep 17 00:00:00 2001 From: michalges Date: Wed, 25 Mar 2026 11:09:07 +0100 Subject: [PATCH 1/7] fix: invalid libraries metadata --- src/components/inputs/date-picker.tsx | 4 +- src/components/inputs/date-time-picker.tsx | 33 +++++-------- src/components/inputs/time-picker.tsx | 47 ++++++++++++++++--- .../resources/data/resource-metadata.ts | 16 ++++--- src/features/resources/enums.ts | 4 +- .../resources/schemas/special-hour-schema.ts | 4 +- src/schemas/string-date-schema.ts | 5 +- 7 files changed, 71 insertions(+), 42 deletions(-) diff --git a/src/components/inputs/date-picker.tsx b/src/components/inputs/date-picker.tsx index a9fc118a..5c6f51a4 100644 --- a/src/components/inputs/date-picker.tsx +++ b/src/components/inputs/date-picker.tsx @@ -27,7 +27,9 @@ export function DatePicker({ }) { const parsed = isEmptyValue(value) ? null - : parse(value, "yyyy-MM-dd", new Date()); + : value.includes("T") + ? new Date(value) + : parse(value, "yyyy-MM-dd", new Date()); const date = parsed != null && isValid(parsed) ? parsed : null; const [isOpen, setIsOpen] = useState(false); diff --git a/src/components/inputs/date-time-picker.tsx b/src/components/inputs/date-time-picker.tsx index 05c2a556..ea2d5327 100644 --- a/src/components/inputs/date-time-picker.tsx +++ b/src/components/inputs/date-time-picker.tsx @@ -1,6 +1,6 @@ "use client"; -import { format, isValid, parse } from "date-fns"; +import { format, isValid, parseISO, set } from "date-fns"; import { isEmptyValue } from "@/utils"; @@ -17,37 +17,28 @@ export function DateTimePicker({ onChange: (date: string | null) => void; disabled?: boolean; }) { - const date = isEmptyValue(value) ? null : new Date(value); - - const formatedDateValue = date == null ? null : format(date, "yyyy-MM-dd"); + const date = + !isEmptyValue(value) && isValid(parseISO(value)) ? parseISO(value) : null; const handleDateChange = (dateValue: string | null) => { - if (dateValue === null) { + if (dateValue == null) { onChange(null); return; } - const selectedDate = parse(dateValue, "yyyy-MM-dd", new Date()); - if (!isValid(selectedDate)) { - return; - } - - date == null - ? selectedDate.setHours(0, 0, 0, 0) - : selectedDate.setHours( - date.getHours(), - date.getMinutes(), - date.getSeconds(), - 0, - ); - - onChange(selectedDate.toISOString()); + const [year, month, day] = dateValue.split("-").map(Number); + const newValue = set(date ?? new Date().setHours(0, 0, 0, 0), { + year, + month: month - 1, + date: day, + }); + onChange(newValue.toISOString()); }; return ( diff --git a/src/components/inputs/time-picker.tsx b/src/components/inputs/time-picker.tsx index 5d295c91..37a01030 100644 --- a/src/components/inputs/time-picker.tsx +++ b/src/components/inputs/time-picker.tsx @@ -1,4 +1,4 @@ -import { formatDate, isValid, parse, toDate } from "date-fns"; +import { formatDate, parse, parseISO } from "date-fns"; import { Clock } from "lucide-react"; import type { ChangeEvent } from "react"; @@ -11,6 +11,20 @@ import { isEmptyValue } from "@/utils"; import { InputSlot } from "./input-slot"; +function isISODatetime(value: string): boolean { + return value.includes("T"); +} + +function toBaseDate(value: Date | string | null): Date | null { + if (value instanceof Date) { + return value; + } + if (typeof value === "string" && isISODatetime(value)) { + return parseISO(value); + } + return null; +} + export function TimePicker({ value, onChange, @@ -20,15 +34,34 @@ export function TimePicker({ onChange: (date: string | null) => void; disabled?: boolean; }) { + const inputValue = (() => { + if (isEmptyValue(value)) { + return "00:00:00"; + } + if (value instanceof Date) { + return formatDate(value, "HH:mm:ss"); + } + if (isISODatetime(value)) { + return formatDate(parseISO(value), "HH:mm:ss"); + } + return value; + })(); + const handleTimeChange = (event_: ChangeEvent) => { const timeValue = event_.target.value; - const baseDate = value == null ? new Date() : toDate(value); - const parsedTime = parse(timeValue, "HH:mm:ss", baseDate); - if (isValid(parsedTime)) { + if (!timeValue) { + onChange(null); + return; + } + + const baseDate = toBaseDate(value); + if (baseDate != null) { + const parsedTime = parse(timeValue, "HH:mm:ss", baseDate); onChange(parsedTime.toISOString()); - } else { - event_.preventDefault(); + return; } + + onChange(timeValue); }; return ( @@ -39,7 +72,7 @@ export function TimePicker({ diff --git a/src/features/resources/data/resource-metadata.ts b/src/features/resources/data/resource-metadata.ts index dbec1d24..4e9375fb 100644 --- a/src/features/resources/data/resource-metadata.ts +++ b/src/features/resources/data/resource-metadata.ts @@ -1342,7 +1342,8 @@ export const RESOURCE_METADATA = { }, }, [Resource.RegularHours]: { - apiPath: "library_regular_hours", + queryName: "regularHours", + apiPath: "regular_hours", itemMapper: (item) => ({ name: POLISH_WEEKDAYS[item.weekDay], description: `${item.openTime} - ${item.closeTime}`, @@ -1369,14 +1370,15 @@ export const RESOURCE_METADATA = { }, defaultValues: { weekDay: Weekday.Monday, - openTime: "08:00", - closeTime: "16:00", + openTime: "00:00:00", + closeTime: "00:00:00", libraryId: -1, }, }, }, [Resource.SpecialHours]: { - apiPath: "library_special_hours", + queryName: "specialHours", + apiPath: "special_hours", itemMapper: (item) => ({ name: item.specialDate, description: `${item.openTime} - ${item.closeTime}`, @@ -1398,9 +1400,9 @@ export const RESOURCE_METADATA = { }, }, defaultValues: { - specialDate: getRoundedDate(0), - openTime: "08:00", - closeTime: "16:00", + specialDate: "", + openTime: "00:00:00", + closeTime: "00:00:00", libraryId: -1, }, }, diff --git a/src/features/resources/enums.ts b/src/features/resources/enums.ts index 853ffd0c..f30e4057 100644 --- a/src/features/resources/enums.ts +++ b/src/features/resources/enums.ts @@ -29,10 +29,10 @@ export enum Resource { NotificationTopics = "notifications/topics", PinkBoxes = "map/pink-boxes", PolinkaStations = "map/polinka-stations", - RegularHours = "library-regular-hours", + RegularHours = "regular-hours", Roles = "about-us/roles", SksOpeningHours = "misc/sks-opening-hours", - SpecialHours = "library-special-hours", + SpecialHours = "special-hours", StudentOrganizations = "student-organizations", StudentOrganizationLinks = "student-organization-links", StudentOrganizationTags = "student-organization-tags", diff --git a/src/features/resources/schemas/special-hour-schema.ts b/src/features/resources/schemas/special-hour-schema.ts index ca7320a4..2f84e84f 100644 --- a/src/features/resources/schemas/special-hour-schema.ts +++ b/src/features/resources/schemas/special-hour-schema.ts @@ -1,9 +1,9 @@ import { z } from "zod"; -import { IsoTimestampSchema, RequiredStringSchema } from "@/schemas"; +import { RequiredStringSchema, StringDateSchema } from "@/schemas"; export const SpecialHourSchema = z.object({ - specialDate: IsoTimestampSchema, + specialDate: StringDateSchema, openTime: RequiredStringSchema, closeTime: RequiredStringSchema, libraryId: z.number(), diff --git a/src/schemas/string-date-schema.ts b/src/schemas/string-date-schema.ts index 4592961f..e1da4d8f 100644 --- a/src/schemas/string-date-schema.ts +++ b/src/schemas/string-date-schema.ts @@ -1,3 +1,4 @@ -import { z } from "zod"; +import { RequiredStringSchema } from "@/schemas"; -export const StringDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/); +export const StringDateSchema = + RequiredStringSchema.regex(/^\d{4}-\d{2}-\d{2}$/); From 1cdbeae86614a1a790deb5caeadb986e8d945609 Mon Sep 17 00:00:00 2001 From: michalges Date: Thu, 26 Mar 2026 18:40:19 +0100 Subject: [PATCH 2/7] fix: date parsing --- src/components/inputs/date-picker.tsx | 13 ++++++------- src/components/inputs/date-time-picker.tsx | 17 ++++++++++++++++- src/components/inputs/time-picker.tsx | 19 +++++++------------ .../resources/data/resource-metadata.ts | 4 ++-- .../resources/schemas/versions-schema.ts | 8 ++++++-- 5 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/components/inputs/date-picker.tsx b/src/components/inputs/date-picker.tsx index 5c6f51a4..6d3065e1 100644 --- a/src/components/inputs/date-picker.tsx +++ b/src/components/inputs/date-picker.tsx @@ -1,6 +1,6 @@ "use client"; -import { format, isValid, parse } from "date-fns"; +import { format, isValid, parseISO } from "date-fns"; import { pl } from "date-fns/locale/pl"; import { Calendar as CalendarIcon } from "lucide-react"; import { useState } from "react"; @@ -14,7 +14,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { isEmptyValue } from "@/utils"; +import { StringDateSchema } from "@/schemas"; export function DatePicker({ value, @@ -25,11 +25,10 @@ export function DatePicker({ onChange: (date: string | null) => void; disabled?: boolean; }) { - const parsed = isEmptyValue(value) - ? null - : value.includes("T") - ? new Date(value) - : parse(value, "yyyy-MM-dd", new Date()); + const parsed = + value != null && StringDateSchema.safeParse(value).success + ? parseISO(value) + : null; const date = parsed != null && isValid(parsed) ? parsed : null; const [isOpen, setIsOpen] = useState(false); diff --git a/src/components/inputs/date-time-picker.tsx b/src/components/inputs/date-time-picker.tsx index ea2d5327..c775551c 100644 --- a/src/components/inputs/date-time-picker.tsx +++ b/src/components/inputs/date-time-picker.tsx @@ -2,6 +2,7 @@ import { format, isValid, parseISO, set } from "date-fns"; +import { TooltipWrapper } from "@/components/core/tooltip-wrapper"; import { isEmptyValue } from "@/utils"; import { DatePicker } from "./date-picker"; @@ -42,7 +43,21 @@ export function DateTimePicker({ disabled={disabled} onChange={handleDateChange} /> - + +
+ +
+
); } diff --git a/src/components/inputs/time-picker.tsx b/src/components/inputs/time-picker.tsx index 37a01030..39e74493 100644 --- a/src/components/inputs/time-picker.tsx +++ b/src/components/inputs/time-picker.tsx @@ -34,18 +34,13 @@ export function TimePicker({ onChange: (date: string | null) => void; disabled?: boolean; }) { - const inputValue = (() => { - if (isEmptyValue(value)) { - return "00:00:00"; - } - if (value instanceof Date) { - return formatDate(value, "HH:mm:ss"); - } - if (isISODatetime(value)) { - return formatDate(parseISO(value), "HH:mm:ss"); - } - return value; - })(); + const base = toBaseDate(value); + const inputValue = + base == null + ? isEmptyValue(value) + ? "00:00:00" + : String(value) + : formatDate(base, "HH:mm:ss"); const handleTimeChange = (event_: ChangeEvent) => { const timeValue = event_.target.value; diff --git a/src/features/resources/data/resource-metadata.ts b/src/features/resources/data/resource-metadata.ts index 4e9375fb..d192c030 100644 --- a/src/features/resources/data/resource-metadata.ts +++ b/src/features/resources/data/resource-metadata.ts @@ -1401,8 +1401,8 @@ export const RESOURCE_METADATA = { }, defaultValues: { specialDate: "", - openTime: "00:00:00", - closeTime: "00:00:00", + openTime: "08:00", + closeTime: "16:00", libraryId: -1, }, }, diff --git a/src/features/resources/schemas/versions-schema.ts b/src/features/resources/schemas/versions-schema.ts index cb8baca1..632c1bd7 100644 --- a/src/features/resources/schemas/versions-schema.ts +++ b/src/features/resources/schemas/versions-schema.ts @@ -1,10 +1,14 @@ import { z } from "zod"; -import { NumericIdSchema, RequiredStringSchema } from "@/schemas"; +import { + NumericIdSchema, + RequiredStringSchema, + StringDateSchema, +} from "@/schemas"; export const VersionsSchema = z.object({ name: RequiredStringSchema, description: z.string().nullish(), - releaseDate: RequiredStringSchema.datetime(), + releaseDate: StringDateSchema, milestoneId: NumericIdSchema, }); From c72ac6e77f6b0b1e54e133b120a7465eb0e53aa3 Mon Sep 17 00:00:00 2001 From: michalges Date: Thu, 26 Mar 2026 18:58:18 +0100 Subject: [PATCH 3/7] refactor: parse string date util --- src/components/inputs/date-picker.tsx | 11 ++++------- src/components/inputs/date-time-picker.tsx | 14 ++++++++++---- src/components/inputs/utils/parse-string-date.ts | 13 +++++++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 src/components/inputs/utils/parse-string-date.ts diff --git a/src/components/inputs/date-picker.tsx b/src/components/inputs/date-picker.tsx index 6d3065e1..feebcacc 100644 --- a/src/components/inputs/date-picker.tsx +++ b/src/components/inputs/date-picker.tsx @@ -1,6 +1,6 @@ "use client"; -import { format, isValid, parseISO } from "date-fns"; +import { format } from "date-fns"; import { pl } from "date-fns/locale/pl"; import { Calendar as CalendarIcon } from "lucide-react"; import { useState } from "react"; @@ -14,7 +14,8 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { StringDateSchema } from "@/schemas"; + +import { parseStringDate } from "./utils/parse-string-date"; export function DatePicker({ value, @@ -25,11 +26,7 @@ export function DatePicker({ onChange: (date: string | null) => void; disabled?: boolean; }) { - const parsed = - value != null && StringDateSchema.safeParse(value).success - ? parseISO(value) - : null; - const date = parsed != null && isValid(parsed) ? parsed : null; + const date = parseStringDate(value); const [isOpen, setIsOpen] = useState(false); return ( diff --git a/src/components/inputs/date-time-picker.tsx b/src/components/inputs/date-time-picker.tsx index c775551c..dea30dc1 100644 --- a/src/components/inputs/date-time-picker.tsx +++ b/src/components/inputs/date-time-picker.tsx @@ -8,6 +8,7 @@ import { isEmptyValue } from "@/utils"; import { DatePicker } from "./date-picker"; import { InputRow } from "./input-row"; import { TimePicker } from "./time-picker"; +import { parseStringDate } from "./utils/parse-string-date"; export function DateTimePicker({ value, @@ -27,11 +28,16 @@ export function DateTimePicker({ return; } - const [year, month, day] = dateValue.split("-").map(Number); + const parsedDate = parseStringDate(dateValue); + if (parsedDate == null) { + onChange(null); + return; + } + const newValue = set(date ?? new Date().setHours(0, 0, 0, 0), { - year, - month: month - 1, - date: day, + year: parsedDate.getFullYear(), + month: parsedDate.getMonth(), + date: parsedDate.getDate(), }); onChange(newValue.toISOString()); }; diff --git a/src/components/inputs/utils/parse-string-date.ts b/src/components/inputs/utils/parse-string-date.ts new file mode 100644 index 00000000..993b064d --- /dev/null +++ b/src/components/inputs/utils/parse-string-date.ts @@ -0,0 +1,13 @@ +import { isValid, parseISO } from "date-fns"; + +import { StringDateSchema } from "@/schemas"; + +/** Parses values in yyyy-MM-dd format into a valid Date instance. */ +export const parseStringDate = (value: string | null): Date | null => { + if (value == null || !StringDateSchema.safeParse(value).success) { + return null; + } + + const parsed = parseISO(value); + return isValid(parsed) ? parsed : null; +}; From a092e43c69569a9384b7278d1d5ad6f06ce87454 Mon Sep 17 00:00:00 2001 From: michalges Date: Thu, 26 Mar 2026 19:08:58 +0100 Subject: [PATCH 4/7] fix: baseDate validity --- src/components/inputs/time-picker.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/inputs/time-picker.tsx b/src/components/inputs/time-picker.tsx index 39e74493..ccf2275b 100644 --- a/src/components/inputs/time-picker.tsx +++ b/src/components/inputs/time-picker.tsx @@ -1,4 +1,4 @@ -import { formatDate, parse, parseISO } from "date-fns"; +import { formatDate, isValid, parse, parseISO } from "date-fns"; import { Clock } from "lucide-react"; import type { ChangeEvent } from "react"; @@ -17,10 +17,11 @@ function isISODatetime(value: string): boolean { function toBaseDate(value: Date | string | null): Date | null { if (value instanceof Date) { - return value; + return isValid(value) ? value : null; } if (typeof value === "string" && isISODatetime(value)) { - return parseISO(value); + const parsedValue = parseISO(value); + return isValid(parsedValue) ? parsedValue : null; } return null; } From e2a57f8ceb340b0340ebd56989e344fb0ce62c6e Mon Sep 17 00:00:00 2001 From: michalges Date: Fri, 27 Mar 2026 10:20:01 +0100 Subject: [PATCH 5/7] fix: missing default values --- src/features/resources/data/resource-metadata.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/resources/data/resource-metadata.ts b/src/features/resources/data/resource-metadata.ts index d192c030..d70aaa66 100644 --- a/src/features/resources/data/resource-metadata.ts +++ b/src/features/resources/data/resource-metadata.ts @@ -1370,8 +1370,8 @@ export const RESOURCE_METADATA = { }, defaultValues: { weekDay: Weekday.Monday, - openTime: "00:00:00", - closeTime: "00:00:00", + openTime: "08:00:00", + closeTime: "16:00:00", libraryId: -1, }, }, @@ -1401,8 +1401,8 @@ export const RESOURCE_METADATA = { }, defaultValues: { specialDate: "", - openTime: "08:00", - closeTime: "16:00", + openTime: "08:00:00", + closeTime: "16:00:00", libraryId: -1, }, }, From 7be872378f35496e6e306b882a37445501c529e3 Mon Sep 17 00:00:00 2001 From: michalges Date: Fri, 27 Mar 2026 11:13:02 +0100 Subject: [PATCH 6/7] test: time picker tests --- src/components/inputs/time-picker.test.tsx | 39 +++++++++++++++++++ .../resources/data/resource-metadata.ts | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/components/inputs/time-picker.test.tsx diff --git a/src/components/inputs/time-picker.test.tsx b/src/components/inputs/time-picker.test.tsx new file mode 100644 index 00000000..d1742e00 --- /dev/null +++ b/src/components/inputs/time-picker.test.tsx @@ -0,0 +1,39 @@ +import { render } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { InputComponentWrapper } from "@/tests/unit"; + +import { TimePicker } from "./time-picker"; + +function renderTimePicker(initialValue: string | null) { + const screen = render( + , + ); + const input = screen.container.querySelector('input[type="time"]'); + return { screen, input }; +} + +describe("TimePicker Component", () => { + it("should render default time for null value", () => { + const { input } = renderTimePicker(null); + expect(input).toHaveValue("00:00:00"); + }); + + it("should render with provided time value", () => { + const { input } = renderTimePicker("12:30:45"); + expect(input).toHaveValue("12:30:45"); + }); + + it("should render ISO datetime value correctly", () => { + const { input } = renderTimePicker("2026-03-25T12:30:45Z"); + expect(input).toHaveValue("13:30:45"); + }); + + it("should not throw for invalid string value", () => { + const { input } = renderTimePicker("not-a-time"); + expect(input).not.toHaveValue("not-a-time"); + }); +}); diff --git a/src/features/resources/data/resource-metadata.ts b/src/features/resources/data/resource-metadata.ts index d70aaa66..bef82c14 100644 --- a/src/features/resources/data/resource-metadata.ts +++ b/src/features/resources/data/resource-metadata.ts @@ -1012,7 +1012,7 @@ export const RESOURCE_METADATA = { defaultValues: { name: "", description: null, - releaseDate: getRoundedDate(0), + releaseDate: "", milestoneId: -1, }, }, From 09b5c02176a0200e32c77819317cfe7a93dcc494 Mon Sep 17 00:00:00 2001 From: michalges Date: Fri, 27 Mar 2026 11:25:23 +0100 Subject: [PATCH 7/7] fix: time-picker test timezones --- src/components/inputs/time-picker.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/inputs/time-picker.test.tsx b/src/components/inputs/time-picker.test.tsx index d1742e00..7b8542cd 100644 --- a/src/components/inputs/time-picker.test.tsx +++ b/src/components/inputs/time-picker.test.tsx @@ -1,4 +1,5 @@ import { render } from "@testing-library/react"; +import { formatDate, parseISO } from "date-fns"; import { describe, expect, it } from "vitest"; import { InputComponentWrapper } from "@/tests/unit"; @@ -28,8 +29,9 @@ describe("TimePicker Component", () => { }); it("should render ISO datetime value correctly", () => { - const { input } = renderTimePicker("2026-03-25T12:30:45Z"); - expect(input).toHaveValue("13:30:45"); + const value = "2026-03-25T12:30:45Z"; + const { input } = renderTimePicker(value); + expect(input).toHaveValue(formatDate(parseISO(value), "HH:mm:ss")); }); it("should not throw for invalid string value", () => {