diff --git a/__test__/features/Events/EventPreview/utils/makeRecurrenceString.test.ts b/__test__/features/Events/EventPreview/utils/makeRecurrenceString.test.ts new file mode 100644 index 0000000..9be7256 --- /dev/null +++ b/__test__/features/Events/EventPreview/utils/makeRecurrenceString.test.ts @@ -0,0 +1,157 @@ +import { makeRecurrenceString } from "@/features/Events/EventPreview/utils/makeRecurrenceString"; +import { RepetitionObject } from "@/features/Events/EventsTypes"; + +describe("makeRecurrenceString", () => { + const mockT = jest.fn((key: string, params?: any) => { + if (params) { + if (key === "eventPreview.everyInterval") { + return `Every ${params.interval} ${params.unit}`; + } + if (key === "eventPreview.recurrenceOnDays") { + return `on ${params.days}`; + } + if (key === "eventPreview.forOccurrences") { + return `for ${params.count} times`; + } + if (key === "eventPreview.until") { + return `until ${params.date}`; + } + } + + const translations: Record = { + "event.repeat.frequency.days": "days", + "event.repeat.frequency.weeks": "weeks", + "event.repeat.frequency.months": "months", + "event.repeat.frequency.years": "years", + "eventPreview.onDays.MO": "Monday", + "eventPreview.onDays.TU": "Tuesday", + "eventPreview.onDays.WE": "Wednesday", + "eventPreview.onDays.TH": "Thursday", + "eventPreview.onDays.FR": "Friday", + "eventPreview.onDays.SA": "Saturday", + "eventPreview.onDays.SU": "Sunday", + locale: "en-US", + }; + + return translations[key] || key; + }); + + const startText = "Repeats"; + + beforeEach(() => { + mockT.mockClear(); + }); + + it("formats string for interval === 1 when enableStrForOneTimeInterval is true", () => { + const repetition: RepetitionObject = { freq: "daily", interval: 1 }; + const result = makeRecurrenceString({ + repetition, + t: mockT, + startText, + enableStrForOneTimeInterval: true, + }); + + expect(result).toBe("Repeats, days"); + }); + + it("formats string when interval is undefined", () => { + const repetition: RepetitionObject = { freq: "daily" }; + const result = makeRecurrenceString({ + repetition, + t: mockT, + startText, + enableStrForOneTimeInterval: true, + }); + + expect(result).toBe("Repeats, days"); + }); + + it("formats string for interval > 1", () => { + const repetition: RepetitionObject = { freq: "weekly", interval: 2 }; + const result = makeRecurrenceString({ repetition, t: mockT, startText }); + + expect(result).toBe("Repeats, Every 2 weeks"); + expect(mockT).toHaveBeenCalledWith("eventPreview.everyInterval", { + interval: 2, + unit: "weeks", + }); + }); + + it("formats string with specific days and sorts them according to WEEK_DAYS order", () => { + const repetition: RepetitionObject = { + freq: "weekly", + interval: 1, + byday: ["WE", "MO", "FR"], + }; + const result = makeRecurrenceString({ repetition, t: mockT, startText }); + + // Expected order: MO, WE, FR + expect(result).toBe("Repeats, on Monday, Wednesday, Friday"); + }); + + it("formats string with occurrences count", () => { + const repetition: RepetitionObject = { + freq: "daily", + interval: 1, + occurrences: 5, + }; + const result = makeRecurrenceString({ repetition, t: mockT, startText }); + + expect(result).toBe("Repeats, for 5 times"); + }); + + it("formats string with end date", () => { + const endDate = "2025-12-31T00:00:00.000Z"; + const repetition: RepetitionObject = { + freq: "daily", + interval: 1, + endDate, + }; + const result = makeRecurrenceString({ repetition, t: mockT, startText }); + + const formattedDate = new Date(endDate).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + + expect(result).toBe(`Repeats, until ${formattedDate}`); + }); + + it("combines multiple properties", () => { + const endDate = "2025-12-31T00:00:00.000Z"; + const repetition: RepetitionObject = { + freq: "monthly", + interval: 3, + byday: ["TU", "TH"], + endDate, + }; + const result = makeRecurrenceString({ repetition, t: mockT, startText }); + + const formattedDate = new Date(endDate).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + + expect(result).toBe( + `Repeats, Every 3 months, on Tuesday, Thursday, until ${formattedDate}` + ); + }); + + it("uses custom joinChar when provided", () => { + const repetition: RepetitionObject = { + freq: "weekly", + interval: 2, + occurrences: 10, + }; + const result = makeRecurrenceString({ + repetition, + t: mockT, + startText, + joinChar: ";", + }); + + expect(result).toBe("Repeats; Every 2 weeks; for 10 times"); + }); +}); diff --git a/src/components/Event/components/DateTimeSummary.tsx b/src/components/Event/components/DateTimeSummary.tsx index 36223ea..69b3b80 100644 --- a/src/components/Event/components/DateTimeSummary.tsx +++ b/src/components/Event/components/DateTimeSummary.tsx @@ -12,6 +12,7 @@ import { getTimezoneOffset } from "@/utils/timezone"; import { RepetitionObject } from "@/features/Events/EventsTypes"; import { LONG_DATE_FORMAT } from "../utils/dateTimeFormatters"; import { SectionPreviewRow } from "./SectionPreviewRow"; +import { makeRecurrenceString } from "@/features/Events/EventPreview/utils/makeRecurrenceString"; interface DateTimeSummaryProps { startDate: string; @@ -99,21 +100,15 @@ export const DateTimeSummary: React.FC = ({ return t("event.repeat.doesNotRepeat"); } - const interval = rep.interval || 1; - const freqMap: { [key: string]: string } = { - daily: t("event.repeat.frequency.days"), - weekly: t("event.repeat.frequency.weeks"), - monthly: t("event.repeat.frequency.months"), - yearly: t("event.repeat.frequency.years"), - }; - - const freqText = freqMap[rep.freq] || rep.freq; - - if (interval === 1) { - return `${t("event.repeat.every")} ${freqText}`; - } - - return `${t("event.repeat.every")} ${interval} ${freqText}`; + return ( + makeRecurrenceString({ + repetition: rep, + t, + startText: rep.interval === 1 ? t("event.repeat.every") : "", + joinChar: "", + enableStrForOneTimeInterval: true, + }) || "" + ); }; // Format date text: show both start and end date if showEndDate is true diff --git a/src/features/Events/EventPreview/EventPreviewDetails.tsx b/src/features/Events/EventPreview/EventPreviewDetails.tsx index 7bfe2f8..1a9d928 100644 --- a/src/features/Events/EventPreview/EventPreviewDetails.tsx +++ b/src/features/Events/EventPreview/EventPreviewDetails.tsx @@ -244,7 +244,14 @@ export function EventPreviewDetails({ } - text={makeRecurrenceString(event, t)} + text={makeRecurrenceString({ + repetition: event.repetition, + t, + startText: `${t("eventPreview.recurrentEvent")} · ${t( + `eventPreview.freq.${event.repetition.freq}`, + event.repetition.freq + )}`, + })} /> )} diff --git a/src/features/Events/EventPreview/utils/makeRecurrenceString.ts b/src/features/Events/EventPreview/utils/makeRecurrenceString.ts index 7a7632f..74748bc 100644 --- a/src/features/Events/EventPreview/utils/makeRecurrenceString.ts +++ b/src/features/Events/EventPreview/utils/makeRecurrenceString.ts @@ -1,17 +1,25 @@ -import { CalendarEvent } from "../../EventsTypes"; +import { RepetitionObject } from "../../EventsTypes"; -export function makeRecurrenceString( - event: CalendarEvent, - t: (k: string, p?: string | object) => string -): string | undefined { - if (!event.repetition) return; +const WEEK_DAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; - const recur: string[] = [ - `${t("eventPreview.recurrentEvent")} · ${t( - `eventPreview.freq.${event.repetition.freq}`, - event.repetition.freq - )}`, - ]; +interface MakeRecurrenceStringParams { + repetition: RepetitionObject; + t: (k: string, p?: string | object) => string; + startText: string; + joinChar?: string; + enableStrForOneTimeInterval?: boolean; +} + +export function makeRecurrenceString({ + repetition, + t, + startText, + joinChar, + enableStrForOneTimeInterval, +}: MakeRecurrenceStringParams): string | undefined { + if (!repetition) return; + + const recur: string[] = [startText]; const recurType: Record = { daily: t("event.repeat.frequency.days"), @@ -20,45 +28,51 @@ export function makeRecurrenceString( yearly: t("event.repeat.frequency.years"), }; - if (event.repetition.interval && event.repetition.interval > 1) { + const interval = repetition.interval ?? 1; + + if (interval === 1 && enableStrForOneTimeInterval) { + recur.push(recurType[repetition.freq]); + } + + if (interval > 1) { recur.push( t("eventPreview.everyInterval", { - interval: event.repetition.interval, - unit: recurType[event.repetition.freq] ?? event.repetition.freq, + interval, + unit: recurType[repetition.freq] ?? repetition.freq, }) ); } - if (event.repetition.byday) { + if (repetition.byday) { + const weekDaysByOrder = WEEK_DAYS.filter((day) => + repetition.byday?.includes(day) + ); recur.push( t("eventPreview.recurrenceOnDays", { - days: event.repetition.byday + days: weekDaysByOrder .map((s) => t(`eventPreview.onDays.${s}`)) .join(", "), }) ); } - if (event.repetition.occurrences) { + if (repetition.occurrences) { recur.push( t("eventPreview.forOccurrences", { - count: event.repetition.occurrences, + count: repetition.occurrences, }) ); } - if (event.repetition.endDate) { + if (repetition.endDate) { recur.push( t("eventPreview.until", { - date: new Date(event.repetition.endDate).toLocaleDateString( - t("locale"), - { - year: "numeric", - month: "long", - day: "numeric", - } - ), + date: new Date(repetition.endDate).toLocaleDateString(t("locale"), { + year: "numeric", + month: "long", + day: "numeric", + }), }) ); } - return recur.join(", "); + return recur.filter(Boolean).join(`${joinChar ?? ","} `); } diff --git a/src/locales/en.json b/src/locales/en.json index 3740ada..5c262e1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -318,7 +318,7 @@ "SU": "sunday" }, "recurrenceOnDays": "on %{days}", - "everyInterval": "every %{interval} %{unit}", + "everyInterval": "Every %{interval} %{unit}", "forOccurrences": "for %{count} occurrences", "until": "until %{date}", "days": "days", diff --git a/src/locales/fr.json b/src/locales/fr.json index 26603e9..ffed79c 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -319,7 +319,7 @@ "SU": "dimanche" }, "recurrenceOnDays": "le %{days}", - "everyInterval": "tous les %{interval} %{unit}", + "everyInterval": "Tous les %{interval} %{unit}", "forOccurrences": "pour %{count} occurrences", "until": "jusqu'au %{date}", "days": "jours", diff --git a/src/locales/ru.json b/src/locales/ru.json index 5308464..122c6ac 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -319,7 +319,7 @@ "SU": "ВС" }, "recurrenceOnDays": "по %{days}", - "everyInterval": "каждые %{interval} %{unit}", + "everyInterval": "Каждые %{interval} %{unit}", "forOccurrences": "в течение %{count} повторений", "until": "до %{date}", "days": "дни", diff --git a/src/locales/vi.json b/src/locales/vi.json index d723468..54ebb48 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -317,7 +317,7 @@ "SU": "chủ nhật" }, "recurrenceOnDays": "vào %{days}", - "everyInterval": "mỗi %{interval} %{unit}", + "everyInterval": "Mỗi %{interval} %{unit}", "forOccurrences": "trong %{count} lần", "until": "đến %{date}", "days": "ngày",