add support for opening hours that automatically change over time
This commit is contained in:
@ -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);
|
||||
|
66
web/src/components/blocks/OpeningHoursSectionBlock.tsx
Normal file
66
web/src/components/blocks/OpeningHoursSectionBlock.tsx
Normal 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>:
|
||||
{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>
|
||||
);
|
||||
}
|
@ -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";
|
||||
|
||||
|
@ -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
@ -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));
|
||||
}
|
||||
|
@ -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
258
web/src/lib/openinghours.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
Reference in New Issue
Block a user