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

@ -169,6 +169,13 @@ class NeufAddressSectionBlock(blocks.StaticBlock):
admin_text = "Viser adressen og Google-kart."
class OpeningHoursSectionBlock(blocks.StaticBlock):
class Meta:
icon = "time"
label = "Chateau Neuf: Åpningstider"
admin_text = "Viser gjeldende åpningstider."
BASE_BLOCKS = [
("paragraph", blocks.RichTextBlock(label="Rik tekst")),
("image", ImageWithTextBlock(label="Bilde")),
@ -199,6 +206,7 @@ class PageSectionBlock(blocks.StructBlock):
[block for block in BASE_BLOCKS if block[0] != "page_section_navigation"]
+ [
("neuf_address", NeufAddressSectionBlock()),
("opening_hours", OpeningHoursSectionBlock()),
]
)

View File

@ -34,6 +34,7 @@ INSTALLED_APPS = [
"events",
"venues",
"news",
"openinghours",
# end cms apps
"grapple",
"graphene_django",
@ -51,6 +52,7 @@ INSTALLED_APPS = [
"wagtail",
"modelcluster",
"taggit",
"django_extensions",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
@ -195,6 +197,7 @@ GRAPPLE = {
"events",
"venues",
"news",
"openinghours",
],
"EXPOSE_GRAPHIQL": True,
}

File diff suppressed because one or more lines are too long

View File

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class OpeninghoursConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "openinghours"

View File

@ -0,0 +1,47 @@
# Generated by Django 5.0.6 on 2024-07-07 19:27
import django.db.models.deletion
import modelcluster.fields
import wagtail.blocks
import wagtail.fields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='OpeningHoursSet',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Enkel beskrivelse av hvilken periode disse åpningstidene dekker, f.eks. <em>Vanlige åpningstider</em> eller <em>Sommer 2032</em>.', max_length=256)),
('effective_from', models.DateField()),
('effective_to', models.DateField(blank=True, null=True)),
('announcement', models.TextField(blank=True, help_text='En kort notis som vil vises i forbindelse med åpningstidene, f.eks. <em>Huset er stengt i perioden A til B.</em>. Kan være blank.')),
],
options={
'verbose_name': 'Opening hours set',
'verbose_name_plural': 'Opening hours sets',
'ordering': ['effective_from'],
},
),
migrations.CreateModel(
name='OpeningHoursItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort_order', models.IntegerField(blank=True, editable=False, null=True)),
('function', models.CharField(choices=[('glassbaren', 'Glassbaren'), ('bokcafeen', 'Bokcaféen'), ('ekspedisjonen', 'Ekspedisjonen')], default='', help_text='Hvilket lokale/funksjon?', max_length=32)),
('week', wagtail.fields.StreamField([('monday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Mandag')), ('tuesday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Tirsdag')), ('wednesday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Onsdag')), ('thursday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Torsdag')), ('friday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Fredag')), ('saturday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Lørdag')), ('sunday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Søndag'))])),
('opening_hours_set', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='openinghours.openinghoursset')),
],
options={
'ordering': ['sort_order'],
'abstract': False,
},
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.6 on 2024-07-07 19:34
import wagtail.blocks
import wagtail.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('openinghours', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='openinghoursitem',
name='week',
field=wagtail.fields.StreamField([('days', wagtail.blocks.StructBlock([('monday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Mandag')), ('tuesday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Tirsdag')), ('wednesday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Onsdag')), ('thursday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Torsdag')), ('friday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Fredag')), ('saturday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Lørdag')), ('sunday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Søndag'))]))]),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.0.6 on 2024-07-08 00:21
import wagtail.blocks
import wagtail.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('openinghours', '0002_alter_openinghoursitem_week'),
]
operations = [
migrations.AlterField(
model_name='openinghoursitem',
name='week',
field=wagtail.fields.StreamField([('week', wagtail.blocks.StructBlock([('monday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Mandag')), ('tuesday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Tirsdag')), ('wednesday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Onsdag')), ('thursday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Torsdag')), ('friday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Fredag')), ('saturday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Lørdag')), ('sunday', wagtail.blocks.StructBlock([('time_from', wagtail.blocks.TimeBlock(label='Åpner', required=False)), ('time_to', wagtail.blocks.TimeBlock(label='Stenger', required=False)), ('custom', wagtail.blocks.CharBlock(help_text='Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>', label='Fritekst', required=False))], label='Søndag'))]))]),
),
]

View File

@ -0,0 +1,149 @@
from django.db import models
from django.utils.safestring import mark_safe
from grapple.helpers import register_query_field, register_streamfield_block
from grapple.models import GraphQLForeignKey, GraphQLStreamfield, GraphQLString
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from wagtail import blocks
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, TitleFieldPanel
from wagtail.models import Orderable, StreamField
from wagtail.snippets.models import register_snippet
@register_streamfield_block
class OpeningHoursRangeBlock(blocks.StructBlock):
# closed = blocks.BooleanBlock(required=False, help_text="Kryss av her om lokalet er stengt.")
# blocks.RegexBlock(regex=r'^\d\d:\d\d$', error_messages={'invalid': 'Må være på formatet HH:MM'}, help_text="Tidspunkt på formatet HH:MM")
time_from = blocks.TimeBlock(required=False, label="Åpner")
time_to = blocks.TimeBlock(required=False, label="Stenger")
custom = blocks.CharBlock(
required=False,
label="Fritekst",
help_text=mark_safe(
"Tekst som vises istedenfor tidspunkter. F.eks. <em>Åpent ved arrangement</em>"
),
)
graphql_fields = [
GraphQLString("time_from", required=False),
GraphQLString("time_to", required=False),
GraphQLString("custom", required=False),
]
@register_streamfield_block
class OpeningHoursWeekBlock(blocks.StructBlock):
monday = OpeningHoursRangeBlock(label="Mandag")
tuesday = OpeningHoursRangeBlock(label="Tirsdag")
wednesday = OpeningHoursRangeBlock(label="Onsdag")
thursday = OpeningHoursRangeBlock(label="Torsdag")
friday = OpeningHoursRangeBlock(label="Fredag")
saturday = OpeningHoursRangeBlock(label="Lørdag")
sunday = OpeningHoursRangeBlock(label="Søndag")
graphql_fields = [
GraphQLStreamfield("monday", required=False, is_list=False),
GraphQLStreamfield("tuesday", required=False, is_list=False),
GraphQLStreamfield("wednesday", required=False, is_list=False),
GraphQLStreamfield("thursday", required=False, is_list=False),
GraphQLStreamfield("friday", required=False, is_list=False),
GraphQLStreamfield("saturday", required=False, is_list=False),
GraphQLStreamfield("sunday", required=False, is_list=False),
]
class OpeningHoursItem(Orderable):
FUNCTION_CHOICES = [
("glassbaren", "Glassbaren"),
("bokcafeen", "Bokcaféen"),
("ekspedisjonen", "Ekspedisjonen"),
]
WEEKDAYS = (
("monday", "Mandag"),
("tuesday", "Tirsdag"),
("wednesday", "Onsdag"),
("thursday", "Torsdag"),
("friday", "Fredag"),
("saturday", "Lørdag"),
("sunday", "Søndag"),
)
opening_hours_set = ParentalKey(
"openinghours.OpeningHoursSet", on_delete=models.CASCADE, related_name="items"
)
function = models.CharField(
max_length=32,
default="",
choices=FUNCTION_CHOICES,
blank=False,
help_text="Hvilket lokale/funksjon?",
)
week = StreamField(
[("week", OpeningHoursWeekBlock())],
min_num=1,
max_num=1,
)
panels = [
FieldPanel("function"),
FieldPanel("week"),
]
graphql_fields = [
GraphQLString("function", required=True),
GraphQLStreamfield("week", required=False),
]
@register_snippet
@register_query_field("openingHoursSet", "openingHoursSets")
class OpeningHoursSet(ClusterableModel):
name = models.CharField(
max_length=256,
null=False,
blank=False,
help_text=mark_safe(
"Enkel beskrivelse av hvilken periode disse åpningstidene dekker, "
"f.eks. <em>Vanlige åpningstider</em> eller <em>Sommer 2032</em>."
),
)
effective_from = models.DateField(null=False, blank=False)
effective_to = models.DateField(null=True, blank=True)
announcement = models.TextField(
blank=True,
help_text=mark_safe(
"En kort notis som vil vises i forbindelse med åpningstidene, "
"f.eks. <em>Huset er stengt i perioden A til B.</em>. Kan være blank."
),
)
panels = [
TitleFieldPanel("name"),
FieldPanel("announcement"),
MultiFieldPanel(
heading="Gyldighet",
children=[
FieldPanel("effective_from", heading="Fra"),
FieldPanel("effective_to", heading="Til"),
],
),
InlinePanel("items", label="Lokaler/funksjoner"),
]
graphql_fields = [
GraphQLString("name", required=True),
GraphQLString("announcement", required=False),
GraphQLString("effective_from", required=True),
GraphQLString("effective_to", required=False),
GraphQLForeignKey("items", "openinghours.OpeningHoursItem", required=False, is_list=True),
]
class Meta:
verbose_name = "Opening hours set"
verbose_name_plural = "Opening hours sets"
ordering = ["effective_from"]
def __str__(self):
return f"{self.name} ({self.effective_from} - {self.effective_to or ''})"

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

16
dnscms/poetry.lock generated
View File

@ -201,6 +201,20 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[package]]
name = "django-extensions"
version = "3.2.3"
description = "Extensions for Django"
optional = false
python-versions = ">=3.6"
files = [
{file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"},
{file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"},
]
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-filter"
version = "24.2"
@ -960,4 +974,4 @@ wand = ["Wand (>=0.6,<1.0)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "24cb4c360211d1c02576389f51d3ba863b0da6ef2800e39e716ff4bf1f8f8a16"
content-hash = "2e3e69272f994e66fb5e9e8b50e9925e99a11e0ca45d6a60814e499c3bdd9229"

View File

@ -11,6 +11,7 @@ wagtail = "^6.0.1"
django = "^5.0.4"
wagtail-grapple = "^0.25.1"
psycopg2-binary = "^2.9.9"
django-extensions = "^3.2.3"
[tool.poetry.group.dev.dependencies]
ruff = "*"

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
}
}
}
`);