diff --git a/package-lock.json b/package-lock.json index e906c3d..490a36f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@radix-ui/react-slot": "^1.1.1", "@tsparticles/engine": "^3.7.1", "@tsparticles/react": "^3.0.0", + "@tsparticles/slim": "^3.7.1", "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", "astro": "^5.1.7", @@ -21,6 +22,7 @@ "lucide-react": "^0.473.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "remeda": "^2.19.2", "tailwind-merge": "^2.6.0", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", @@ -5981,6 +5983,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remeda": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.19.2.tgz", + "integrity": "sha512-192lSeU0P91TIsYOX+MZ2x8I+enSkVVF0YhUhABix0CZWl+1+3/zn4b3L2d/BiWBTa6RsIeJgvLJj5nYTiTXGA==", + "license": "MIT", + "dependencies": { + "type-fest": "^4.30.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", diff --git a/package.json b/package.json index 5e2ccec..c3c1405 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "@astrojs/tailwind": "^5.1.4", "@radix-ui/react-slot": "^1.1.1", "@tsparticles/engine": "^3.7.1", - "@tsparticles/slim": "^3.7.1", "@tsparticles/react": "^3.0.0", + "@tsparticles/slim": "^3.7.1", "@types/react": "^19.0.7", "@types/react-dom": "^19.0.3", "astro": "^5.1.7", @@ -23,6 +23,7 @@ "lucide-react": "^0.473.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "remeda": "^2.19.2", "tailwind-merge": "^2.6.0", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", diff --git a/src/lib/date-utils.ts b/src/lib/date-utils.ts new file mode 100644 index 0000000..d5dff77 --- /dev/null +++ b/src/lib/date-utils.ts @@ -0,0 +1,63 @@ +import { pipe } from 'remeda' + +export const monthNames = [ + 'Januar', 'Februar', 'Mars', 'April', 'Mai', 'Juni', + 'Juli', 'August', 'September', 'Oktober', 'November', 'Desember' +] as const; + +type MonthMap = Record; + +export const NORWEGIAN_MONTHS: MonthMap = { + 'jan': 0, 'januar': 0, + 'feb': 1, 'februar': 1, + 'mar': 2, 'mars': 2, + 'apr': 3, 'april': 3, + 'mai': 4, + 'jun': 5, 'juni': 5, + 'jul': 6, 'juli': 6, + 'aug': 7, 'august': 7, + 'sep': 8, 'september': 8, + 'okt': 9, 'oktober': 9, + 'nov': 10, 'november': 10, + 'des': 11, 'desember': 11 +} as const; + +const normalizeString = (str: string): string => + str?.toLowerCase().trim() ?? ''; + +const parseDate = (str: string): Date => { + if (NORWEGIAN_MONTHS[str] !== undefined) { + return new Date(2024, NORWEGIAN_MONTHS[str], 1) + } + + const [day, monthPart] = str.split('.') + const month = monthPart?.trim() + + return month && month in NORWEGIAN_MONTHS + ? new Date(2024, NORWEGIAN_MONTHS[month], parseInt(day)) + : new Date(0) +} + +const createDateInfo = (date: Date, originalStr: string) => ({ + day: date.getDate(), + month: monthNames[date.getMonth()], + originalStr +}) + +const formatDate = ({ day, month, originalStr }: { day: number; month: string; originalStr: string }): string => + day === 1 && !/\d/.test(originalStr) + ? month + : `${day}. ${month}` + +export const parseNorwegianDate = (dateStr: string): Date => + pipe(dateStr, normalizeString, parseDate) + +export const formatNorwegianDate = (dateStr: string): string => + dateStr === 'Udatert' + ? dateStr + : pipe( + dateStr, + parseNorwegianDate, + (date: Date) => createDateInfo(date, dateStr), + formatDate + ) diff --git a/src/pages/index.astro b/src/pages/index.astro index 02d8453..a99760f 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,121 +4,45 @@ import Layout from '../layouts/Layout.astro'; import '@/styles/globals.css' import { EventCard } from './EventCard' import events from '@/assets/events.json' +import { pipe } from 'remeda' +import { parseNorwegianDate, formatNorwegianDate } from '@/lib/date-utils' -const monthNames = [ - 'Januar', - 'Februar', - 'Mars', - 'April', - 'Mai', - 'Juni', - 'Juli', - 'August', - 'September', - 'Oktober', - 'November', - 'Desember' -] as const; - -type MonthMap = Record; - -/** - * Be hereby warned, a lot of data cleaning is done here that should not be done here - * We're simply reading the input data, filtering, then grouping by *cleaned* dates - * then we write to loops of info, the dates, and for the inner loop we write events. - * **/ -const NORWEGIAN_MONTHS: MonthMap = { - 'jan': 0, 'januar': 0, - 'feb': 1, 'februar': 1, - 'mar': 2, 'mars': 2, - 'apr': 3, 'april': 3, - 'mai': 4, - 'jun': 5, 'juni': 5, - 'jul': 6, 'juli': 6, - 'aug': 7, 'august': 7, - 'sep': 8, 'september': 8, - 'okt': 9, 'oktober': 9, - 'nov': 10, 'november': 10, - 'des': 11, 'desember': 11 -} as const; - -const cleanDateString = (dateStr: string): string => { - return dateStr.toLowerCase().trim(); -}; - -const isMonthOnly = (dateStr: string): boolean => { - return NORWEGIAN_MONTHS[dateStr] !== undefined; -}; - -const createDateFromMonthOnly = (month: string): Date => { - return new Date(2024, NORWEGIAN_MONTHS[month], 1); -}; - -const parseDateParts = (dateStr: string): { day: number; month: string } | null => { - const parts = dateStr.split('.'); - const day = parseInt(parts[0]); - const month = parts[1]?.trim(); - - if (!month || !(month in NORWEGIAN_MONTHS)) { - return null; - } - - return { day, month }; -}; - -const parseNorwegianDate = (dateStr: string): Date => { - if (!dateStr) return new Date(0); - - const cleanStr = cleanDateString(dateStr); - - // Handle full month names like "Februar" - if (isMonthOnly(cleanStr)) { - return createDateFromMonthOnly(cleanStr); - } +type Event = typeof events[number]; +type EventGroups = Record; - // Handle dates like "14.februar" or "14.februar " - const dateParts = parseDateParts(cleanStr); - if (!dateParts) { - return new Date(0); - } +const filterPublished = (events: Event[]): Event[] => + events.filter(event => event["Skal ut"] !== "FALSE"); - return new Date(2025, NORWEGIAN_MONTHS[dateParts.month], dateParts.day); -}; +const groupByDate = (events: Event[]): EventGroups => + Object.groupBy(events, (event): string => event.Dato || 'Udatert') as EventGroups; -const formatNorwegianDate = (dateStr: string): string => { - if (dateStr === 'Udatert') return dateStr; - - const date = parseNorwegianDate(dateStr); - const day = date.getDate(); - const month = monthNames[date.getMonth()]; - - // If it's just a month (day is 1 and original string doesn't contain a number) - if (day === 1 && !/\d/.test(dateStr)) { - return month; - } - - return `${day}. ${month}`; -}; +const sortByDate = (dates: string[]): string[] => + dates.sort((a, b) => parseNorwegianDate(a).getTime() - parseNorwegianDate(b).getTime()); -type Event = typeof events[number]; -const publishedEvents = events.filter(event => event["Skal ut"] !== "FALSE"); -const groupedEvents = Object.groupBy(publishedEvents, (event): string => event.Dato || 'Udatert') as Record; -const sortedDates = Object.keys(groupedEvents).sort((a, b) => - parseNorwegianDate(a).getTime() - parseNorwegianDate(b).getTime() +const { groupedEvents, sortedDates } = pipe( + events, + filterPublished, + groupByDate, + (grouped: EventGroups) => ({ + groupedEvents: grouped, + sortedDates: sortByDate(Object.keys(grouped)) + }) ); --- - {sortedDates.map((date) => ( -
-

{formatNorwegianDate(date)}

-
- {groupedEvents[date].map((event) => ( - - ))} +
+ {sortedDates.map((date) => ( +
+

{formatNorwegianDate(date)}

+
+ {groupedEvents[date].map((event) => ( + + ))} +
-
- ))} + ))} +
\ No newline at end of file