diff --git a/src/components/inputs/date-picker.tsx b/src/components/inputs/date-picker.tsx
index a9fc118a..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, parse } 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 { isEmptyValue } from "@/utils";
+
+import { parseStringDate } from "./utils/parse-string-date";
export function DatePicker({
value,
@@ -25,10 +26,7 @@ export function DatePicker({
onChange: (date: string | null) => void;
disabled?: boolean;
}) {
- const parsed = isEmptyValue(value)
- ? null
- : parse(value, "yyyy-MM-dd", new Date());
- 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 05c2a556..dea30dc1 100644
--- a/src/components/inputs/date-time-picker.tsx
+++ b/src/components/inputs/date-time-picker.tsx
@@ -1,12 +1,14 @@
"use client";
-import { format, isValid, parse } from "date-fns";
+import { format, isValid, parseISO, set } from "date-fns";
+import { TooltipWrapper } from "@/components/core/tooltip-wrapper";
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,
@@ -17,41 +19,51 @@ 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)) {
+ const parsedDate = parseStringDate(dateValue);
+ if (parsedDate == null) {
+ onChange(null);
return;
}
- date == null
- ? selectedDate.setHours(0, 0, 0, 0)
- : selectedDate.setHours(
- date.getHours(),
- date.getMinutes(),
- date.getSeconds(),
- 0,
- );
-
- onChange(selectedDate.toISOString());
+ const newValue = set(date ?? new Date().setHours(0, 0, 0, 0), {
+ year: parsedDate.getFullYear(),
+ month: parsedDate.getMonth(),
+ date: parsedDate.getDate(),
+ });
+ onChange(newValue.toISOString());
};
return (
-
+
+
+
+
+
);
}
diff --git a/src/components/inputs/time-picker.test.tsx b/src/components/inputs/time-picker.test.tsx
new file mode 100644
index 00000000..7b8542cd
--- /dev/null
+++ b/src/components/inputs/time-picker.test.tsx
@@ -0,0 +1,41 @@
+import { render } from "@testing-library/react";
+import { formatDate, parseISO } from "date-fns";
+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 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", () => {
+ const { input } = renderTimePicker("not-a-time");
+ expect(input).not.toHaveValue("not-a-time");
+ });
+});
diff --git a/src/components/inputs/time-picker.tsx b/src/components/inputs/time-picker.tsx
index 5d295c91..ccf2275b 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, isValid, parse, parseISO } from "date-fns";
import { Clock } from "lucide-react";
import type { ChangeEvent } from "react";
@@ -11,6 +11,21 @@ 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 isValid(value) ? value : null;
+ }
+ if (typeof value === "string" && isISODatetime(value)) {
+ const parsedValue = parseISO(value);
+ return isValid(parsedValue) ? parsedValue : null;
+ }
+ return null;
+}
+
export function TimePicker({
value,
onChange,
@@ -20,15 +35,29 @@ export function TimePicker({
onChange: (date: string | null) => void;
disabled?: boolean;
}) {
+ 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;
- 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 +68,7 @@ export function TimePicker({
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;
+};
diff --git a/src/features/resources/data/resource-metadata.ts b/src/features/resources/data/resource-metadata.ts
index dbec1d24..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,
},
},
@@ -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: "08:00:00",
+ closeTime: "16: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: "08:00:00",
+ closeTime: "16: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/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,
});
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}$/);