Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/components/inputs/date-picker.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand All @@ -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 (
Expand Down
50 changes: 31 additions & 19 deletions src/components/inputs/date-time-picker.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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());
Comment thread
michalges marked this conversation as resolved.
};

return (
<InputRow>
<DatePicker
value={formatedDateValue}
value={date == null ? null : format(date, "yyyy-MM-dd")}
disabled={disabled}
onChange={handleDateChange}
/>
Comment thread
michalges marked this conversation as resolved.
<TimePicker value={value} disabled={disabled} onChange={onChange} />
<TooltipWrapper
tooltip={
date == null
? "Najpierw wybierz datę, aby ustawić godzinę"
: undefined
}
>
<div>
<TimePicker
value={value}
disabled={disabled || date == null}
onChange={onChange}
/>
</div>
</TooltipWrapper>
</InputRow>
);
}
41 changes: 41 additions & 0 deletions src/components/inputs/time-picker.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<InputComponentWrapper
component={TimePicker}
initialValue={initialValue}
/>,
);
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");
});
});
43 changes: 36 additions & 7 deletions src/components/inputs/time-picker.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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;
Comment thread
michalges marked this conversation as resolved.
}

export function TimePicker({
value,
onChange,
Expand All @@ -20,15 +35,29 @@ export function TimePicker({
onChange: (date: string | null) => void;
Comment thread
michalges marked this conversation as resolved.
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<HTMLInputElement>) => {
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);
Comment thread
michalges marked this conversation as resolved.
onChange(parsedTime.toISOString());
} else {
event_.preventDefault();
return;
Comment thread
michalges marked this conversation as resolved.
}

onChange(timeValue);
};
Comment thread
michalges marked this conversation as resolved.
Comment thread
michalges marked this conversation as resolved.

return (
Expand All @@ -39,7 +68,7 @@ export function TimePicker({
<InputGroupInput
type="time"
step="1"
value={isEmptyValue(value) ? "00:00:00" : formatDate(value, "HH:mm:ss")}
value={inputValue}
onChange={handleTimeChange}
disabled={disabled}
/>
Expand Down
13 changes: 13 additions & 0 deletions src/components/inputs/utils/parse-string-date.ts
Original file line number Diff line number Diff line change
@@ -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;
};
18 changes: 10 additions & 8 deletions src/features/resources/data/resource-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1012,7 +1012,7 @@ export const RESOURCE_METADATA = {
defaultValues: {
name: "",
description: null,
releaseDate: getRoundedDate(0),
releaseDate: "",
milestoneId: -1,
},
},
Expand Down Expand Up @@ -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}`,
Expand All @@ -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}`,
Expand All @@ -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,
},
},
Expand Down
4 changes: 2 additions & 2 deletions src/features/resources/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions src/features/resources/schemas/special-hour-schema.ts
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down
8 changes: 6 additions & 2 deletions src/features/resources/schemas/versions-schema.ts
Original file line number Diff line number Diff line change
@@ -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,
Comment thread
michalges marked this conversation as resolved.
});
5 changes: 3 additions & 2 deletions src/schemas/string-date-schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { z } from "zod";
import { RequiredStringSchema } from "@/schemas";
Comment thread
michalges marked this conversation as resolved.

export const StringDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
export const StringDateSchema =
RequiredStringSchema.regex(/^\d{4}-\d{2}-\d{2}$/);
Loading