add support for opening hours that automatically change over time

This commit is contained in:
2024-07-08 02:42:02 +02:00
parent ada7d25083
commit 355887518b
23 changed files with 834 additions and 18 deletions

View File

@ -8,6 +8,8 @@ import { ContactSectionBlock, ContactSubsectionBlock } from "./ContactSection";
import { ContactListBlock } from "./ContactListBlock";
import { ContactEntityBlock } from "./ContactEntityBlock";
import { NeufAddressSectionBlock } from "./NeufAddressSectionBlock";
import { OpeningHoursSectionBlock } from "./OpeningHoursSectionBlock";
export const Blocks = ({ blocks }: any) => {
const sections = blocks.filter(
@ -52,6 +54,9 @@ export const Blocks = ({ blocks }: any) => {
case "NeufAddressSectionBlock":
return <NeufAddressSectionBlock />;
break;
case "OpeningHoursSectionBlock":
return <OpeningHoursSectionBlock />;
break;
default:
return <div>Unsupported block type {block.blockType}</div>;
console.log("unsupported block", block);

View File

@ -0,0 +1,66 @@
import {
getOpeningHours,
getOpeningHoursForFunction,
getPrettyOpeningHoursForFunction,
groupOpeningHours,
PrettyOpeningHours,
} from "@/lib/openinghours";
import styles from "./openingHoursSectionBlock.module.scss";
function OpeningHoursSubsection({
title,
prettyHours,
}: {
title: string;
prettyHours: PrettyOpeningHours[];
}) {
return (
<section className={styles.openingHoursSubsection}>
<h3>{title}</h3>
<ul>
{prettyHours.map(({ range, time, custom }) => (
<li key={range}>
<span className={styles.dayRange}>{range}</span>:&nbsp;
{time && <span className={styles.timeRange}>{time}</span>}
{custom && <span className={styles.timeRange}>{custom}</span>}
{!time && !custom && <span className={styles.closed}>Stengt</span>}
</li>
))}
</ul>
</section>
);
}
export async function OpeningHoursSectionBlock() {
const allOpeningHours = await getOpeningHours();
const subsections = [
["glassbaren", "Glassbaren"],
["bokcafeen", "Bokcaféen"],
["ekspedisjonen", "Ekspedisjonen"],
];
const { announcement } = allOpeningHours;
return (
<section className={styles.openingHoursSection}>
{announcement && <p>{announcement}</p>}
{subsections.map((subsection) => {
const [slug, title] = subsection;
const prettyHours = getPrettyOpeningHoursForFunction(
allOpeningHours,
slug
);
console.log("prettyHours", prettyHours, slug);
if (!prettyHours || prettyHours?.length === 0) {
return <></>;
}
return (
<OpeningHoursSubsection
key={slug}
title={title}
prettyHours={prettyHours}
/>
);
})}
</section>
);
}

View File

@ -5,8 +5,9 @@ import {
formatDate,
formatExtendedDateTime,
isTodayOrFuture,
compareDates,
} from "@/lib/date";
import { EventFragment, EventOccurrence, compareDates } from "@/lib/event";
import { EventFragment, EventOccurrence } from "@/lib/event";
import styles from "./dateList.module.scss";
import Link from "next/link";

View File

@ -42,6 +42,10 @@ const documents = {
"\n fragment News on NewsPage {\n __typename\n id\n slug\n title\n firstPublishedAt\n excerpt\n featuredImage {\n ...Image\n }\n body {\n ...Blocks\n }\n }\n": types.NewsFragmentDoc,
"\n fragment NewsIndex on NewsIndex {\n __typename\n id\n slug\n title\n lead\n }\n": types.NewsIndexFragmentDoc,
"\n query news {\n index: newsIndex {\n ... on NewsIndex {\n ...NewsIndex\n }\n }\n news: pages(contentType: \"news.NewsPage\") {\n ... on NewsPage {\n ...News\n }\n }\n }\n": types.NewsDocument,
"\n query openingHoursSets {\n openingHoursSets {\n ...OpeningHoursSetFragment\n }\n }\n": types.OpeningHoursSetsDocument,
"\n fragment OpeningHoursSetFragment on OpeningHoursSet {\n name\n effectiveFrom\n effectiveTo\n announcement\n items {\n id\n function\n week {\n id\n blockType\n ... on OpeningHoursWeekBlock {\n ...OpeningHoursWeekBlock\n }\n }\n }\n }\n": types.OpeningHoursSetFragmentFragmentDoc,
"\n fragment OpeningHoursRangeBlock on OpeningHoursRangeBlock {\n timeFrom\n timeTo\n custom\n }\n": types.OpeningHoursRangeBlockFragmentDoc,
"\n fragment OpeningHoursWeekBlock on OpeningHoursWeekBlock {\n monday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n tuesday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n wednesday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n thursday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n friday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n saturday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n sunday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n }\n": types.OpeningHoursWeekBlockFragmentDoc,
};
/**
@ -174,6 +178,22 @@ export function graphql(source: "\n fragment NewsIndex on NewsIndex {\n __ty
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query news {\n index: newsIndex {\n ... on NewsIndex {\n ...NewsIndex\n }\n }\n news: pages(contentType: \"news.NewsPage\") {\n ... on NewsPage {\n ...News\n }\n }\n }\n"): (typeof documents)["\n query news {\n index: newsIndex {\n ... on NewsIndex {\n ...NewsIndex\n }\n }\n news: pages(contentType: \"news.NewsPage\") {\n ... on NewsPage {\n ...News\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query openingHoursSets {\n openingHoursSets {\n ...OpeningHoursSetFragment\n }\n }\n"): (typeof documents)["\n query openingHoursSets {\n openingHoursSets {\n ...OpeningHoursSetFragment\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment OpeningHoursSetFragment on OpeningHoursSet {\n name\n effectiveFrom\n effectiveTo\n announcement\n items {\n id\n function\n week {\n id\n blockType\n ... on OpeningHoursWeekBlock {\n ...OpeningHoursWeekBlock\n }\n }\n }\n }\n"): (typeof documents)["\n fragment OpeningHoursSetFragment on OpeningHoursSet {\n name\n effectiveFrom\n effectiveTo\n announcement\n items {\n id\n function\n week {\n id\n blockType\n ... on OpeningHoursWeekBlock {\n ...OpeningHoursWeekBlock\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment OpeningHoursRangeBlock on OpeningHoursRangeBlock {\n timeFrom\n timeTo\n custom\n }\n"): (typeof documents)["\n fragment OpeningHoursRangeBlock on OpeningHoursRangeBlock {\n timeFrom\n timeTo\n custom\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment OpeningHoursWeekBlock on OpeningHoursWeekBlock {\n monday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n tuesday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n wednesday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n thursday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n friday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n saturday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n sunday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n }\n"): (typeof documents)["\n fragment OpeningHoursWeekBlock on OpeningHoursWeekBlock {\n monday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n tuesday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n wednesday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n thursday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n friday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n saturday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n sunday {\n ... on OpeningHoursRangeBlock {\n ...OpeningHoursRangeBlock\n }\n }\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import { isToday, isAfter, parse } from "date-fns";
import { isToday, isAfter, parse, compareAsc } from "date-fns";
import { nb } from "date-fns/locale";
import { toZonedTime, format } from "date-fns-tz";
@ -47,3 +47,7 @@ export function isTodayOrFuture(
const zonedDate = toLocalTime(date);
return isToday(zonedDate) || isAfter(zonedDate, zonedNow);
}
export function compareDates(a: Date | string, b: Date | string) {
return compareAsc(new Date(a), new Date(b));
}

View File

@ -1,5 +1,4 @@
import {
compareAsc,
endOfWeek,
startOfToday,
startOfWeek,
@ -8,10 +7,10 @@ import {
addWeeks,
parseISO,
} from "date-fns";
import { toLocalTime, formatDate } from "./date";
import { toLocalTime, formatDate, compareDates } from "./date";
import { graphql } from "@/gql";
import { EventFragment, EventCategory, EventOccurrence } from "@/gql/graphql";
import { PIG_NAMES, PigName, randomElement } from "@/lib/common";
import { EventFragment, EventOccurrence } from "@/gql/graphql";
import { PIG_NAMES, randomElement } from "@/lib/common";
export type {
EventFragment,
@ -134,10 +133,6 @@ export function getSingularEvents(events: EventFragment[]): SingularEvent[] {
.flat();
}
export function compareDates(a: Date | string, b: Date | string) {
return compareAsc(new Date(a), new Date(b));
}
export function sortSingularEvents(events: SingularEvent[]) {
return events.sort((a, b) =>
compareDates(a.occurrence.start, b.occurrence.start)

258
web/src/lib/openinghours.ts Normal file
View File

@ -0,0 +1,258 @@
import {
startOfToday,
isAfter,
parseISO,
isSameDay,
compareDesc,
} from "date-fns";
import { graphql } from "@/gql";
import {
OpeningHoursRangeBlock,
OpeningHoursSet,
OpeningHoursWeekBlock,
} from "@/gql/graphql";
import { getClient } from "@/app/client";
const MISSING_OPENING_HOURS = {
name: "Åpningstider mangler",
effectiveFrom: "",
effectiveTo: null,
announcement: "Åpningstider mangler",
items: [],
};
const WEEKDAYS = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
];
const WEEKDAYS_NORWEGIAN = [
"mandag",
"tirsdag",
"onsdag",
"torsdag",
"fredag",
"øørdag",
"søndag",
];
const openingHoursQuery = graphql(`
query openingHoursSets {
openingHoursSets {
...OpeningHoursSetFragment
}
}
`);
export async function fetchOpeningHoursSets() {
const { data, error } = await getClient().query(openingHoursQuery, {});
const sets = (data?.openingHoursSets ?? []) as OpeningHoursSet[];
return sets;
}
export async function getOpeningHours() {
const today = startOfToday();
const sets = await fetchOpeningHoursSets();
const validSets = sets
.filter((set) => {
const from = parseISO(set.effectiveFrom);
return isAfter(today, from) || isSameDay(today, from);
})
.filter((set) => {
if (!set.effectiveTo) {
return true;
}
const to = parseISO(set.effectiveTo);
return isAfter(to, today) || isSameDay(today, to);
});
if (validSets.length === 0) {
return MISSING_OPENING_HOURS as OpeningHoursSet;
}
if (validSets.length === 1) {
return validSets[0];
}
// pick the set that msot recently took effect
return validSets.sort((a, b) =>
compareDesc(a.effectiveFrom, b.effectiveFrom)
)[0];
}
type OpeningHoursGroup = {
days: string[];
timeFrom: string | null;
timeTo: string | null;
custom: string | null;
};
type OpeningHoursPerDay = Record<string, OpeningHoursRangeBlock>
export function groupOpeningHours(week: OpeningHoursPerDay): OpeningHoursGroup[] {
const grouped: OpeningHoursGroup[] = [];
let previous: string | null = null;
for (const day of WEEKDAYS) {
if (!week.hasOwnProperty(day)) {
continue;
}
const hours = week[day];
if (
hours === null ||
previous === null ||
week[previous]?.timeFrom !== hours.timeFrom ||
week[previous]?.timeTo !== hours.timeTo ||
week[previous]?.custom !== hours.custom
) {
grouped.push({
days: [day],
timeFrom: hours.timeFrom ?? null,
timeTo: hours.timeTo ?? null,
custom: hours.custom ?? null,
});
} else {
grouped[grouped.length - 1].days.push(day);
}
previous = day;
}
return grouped;
}
export type PrettyOpeningHours = {
range: string;
time?: string;
custom?: string;
};
function formatGroupedHours(
grouped: OpeningHoursGroup[]
): PrettyOpeningHours[] {
return grouped.map((group) => {
const startDayIndex = WEEKDAYS.indexOf(group.days[0]);
const endDayIndex = WEEKDAYS.indexOf(group.days[group.days.length - 1]);
const startDayName = WEEKDAYS_NORWEGIAN[startDayIndex];
const endDayName =
group.days.length > 1 ? WEEKDAYS_NORWEGIAN[endDayIndex] : "";
const rangeName = startDayName + (endDayName ? " - " + endDayName : "");
const formattedRange = {
range: rangeName,
...(group.timeFrom && group.timeTo
? {
time: `${group.timeFrom.slice(0, 5)} - ${group.timeTo.slice(0, 5)}`,
}
: {}),
...(group.custom ? { custom: group.custom } : {}),
};
return formattedRange;
});
}
export function getOpeningHoursForFunction(
openingHours: OpeningHoursSet,
name: string
) {
const item = openingHours.items?.find((x) => x?.function === name);
if (!item || !Array.isArray(item?.week) || item?.week.length !== 1) {
return;
}
const week = item.week[0] as OpeningHoursWeekBlock;
return week;
}
export function getPrettyOpeningHoursForFunction(
openingHours: OpeningHoursSet,
name: string
) {
const week = getOpeningHoursForFunction(openingHours, name);
if (!week) {
return [];
}
// just trying to satisfy the type checker, this is crap
const perDay: OpeningHoursPerDay = {
monday: week.monday as OpeningHoursRangeBlock,
tuesday: week.tuesday as OpeningHoursRangeBlock,
wednesday: week.wednesday as OpeningHoursRangeBlock,
thursday: week.thursday as OpeningHoursRangeBlock,
friday: week.friday as OpeningHoursRangeBlock,
saturday: week.friday as OpeningHoursRangeBlock,
sunday: week.friday as OpeningHoursRangeBlock,
}
const grouped = groupOpeningHours(perDay);
return formatGroupedHours(grouped);
}
const OpeningHoursSetFragmentDefinition = graphql(`
fragment OpeningHoursSetFragment on OpeningHoursSet {
name
effectiveFrom
effectiveTo
announcement
items {
id
function
week {
id
blockType
... on OpeningHoursWeekBlock {
...OpeningHoursWeekBlock
}
}
}
}
`);
const OpeningHoursRangeBlockFragmentDefinition = graphql(`
fragment OpeningHoursRangeBlock on OpeningHoursRangeBlock {
timeFrom
timeTo
custom
}
`);
const OpeningHoursWeekBlockFragmentDefinition = graphql(`
fragment OpeningHoursWeekBlock on OpeningHoursWeekBlock {
monday {
... on OpeningHoursRangeBlock {
...OpeningHoursRangeBlock
}
}
tuesday {
... on OpeningHoursRangeBlock {
...OpeningHoursRangeBlock
}
}
wednesday {
... on OpeningHoursRangeBlock {
...OpeningHoursRangeBlock
}
}
thursday {
... on OpeningHoursRangeBlock {
...OpeningHoursRangeBlock
}
}
friday {
... on OpeningHoursRangeBlock {
...OpeningHoursRangeBlock
}
}
saturday {
... on OpeningHoursRangeBlock {
...OpeningHoursRangeBlock
}
}
sunday {
... on OpeningHoursRangeBlock {
...OpeningHoursRangeBlock
}
}
}
`);