Compare commits
26
Commits
93956435cf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1853e6c470
|
||
|
|
54f45820ad
|
||
|
|
031d61e1fe
|
||
|
|
952a8bca13
|
||
|
|
6370373f0d
|
||
|
|
eabcd47bfd
|
||
|
|
848829f3b9
|
||
|
|
669a3cce8d
|
||
|
|
b231f40cdc
|
||
|
|
3892624382
|
||
|
|
ca397070ea
|
||
|
|
09207e57e2
|
||
|
|
9a3cd0032f
|
||
|
|
81c32719c8 | ||
|
|
7a784953a8 | ||
|
|
6092fc32fd | ||
|
|
6ee3695074 | ||
|
|
75282104f6 | ||
|
|
01ce1c76f3 | ||
|
|
38d332b574 | ||
|
|
e600d0a663
|
||
|
|
d63b14b2e5
|
||
|
|
36c01902a1
|
||
|
|
e57eb48c88
|
||
|
|
7ac332e352
|
||
|
|
acf70b99d3
|
@@ -0,0 +1,27 @@
|
||||
name: dnscms
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "dnscms/**"
|
||||
- ".gitea/workflows/dnscms.yaml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "dnscms/**"
|
||||
- ".gitea/workflows/dnscms.yaml"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: dnscms
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- run: uv sync --locked
|
||||
- run: uv run ruff check .
|
||||
- run: uv run ruff format --check .
|
||||
- run: uv run manage.py makemigrations --check --dry-run --settings=dnscms.settings.test
|
||||
- run: uv run pytest
|
||||
@@ -0,0 +1,29 @@
|
||||
name: web
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "web/**"
|
||||
- ".gitea/workflows/web.yaml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "web/**"
|
||||
- ".gitea/workflows/web.yaml"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: npm ci
|
||||
- run: npx tsc --noEmit
|
||||
- run: npm test
|
||||
@@ -24,13 +24,29 @@ npm install
|
||||
npm run dev # http://localhost:3000
|
||||
npm run codegen # regenerate GraphQL types (needs the backend running)
|
||||
npm run build
|
||||
npm test # vitest (npm run test:watch for watch mode)
|
||||
```
|
||||
|
||||
## Pre-commit hooks
|
||||
|
||||
[prek](https://github.com/j178/prek) runs ruff lint + format on `dnscms/**/*.py` plus a few sanity hooks. Hooks are configured in [prek.toml](prek.toml).
|
||||
[prek](https://github.com/j178/prek) runs ruff lint + format on `dnscms/**/*.py`, vitest on `web/` changes, plus a few sanity hooks. Hooks are configured in [prek.toml](prek.toml).
|
||||
|
||||
```bash
|
||||
prek install # registers the git hook
|
||||
prek run --all-files # run on everything
|
||||
```
|
||||
|
||||
## Caching and revalidation
|
||||
|
||||
The frontend caches all pages and GraphQL responses (ISR) under a single `cms` cache tag.
|
||||
Wagtail purges it via a webhook on any publish/unpublish/move/delete (see `dnscms/dnscms/revalidation.py`).
|
||||
Date-sensitive queries (`futureEvents`, opening hours) additionally expire at Oslo midnight.
|
||||
`REVALIDATE_WEBHOOK_SECRET` must be set to the same value on both sides
|
||||
(`web/.env.local` and `dnscms/dnscms/settings/local.py` or env); unset disables the webhook.
|
||||
|
||||
```bash
|
||||
# manual purge
|
||||
curl -X POST -H "X-Revalidate-Secret: $SECRET" https://<frontend>/api/revalidate
|
||||
```
|
||||
|
||||
Note: ISR is off under `npm run dev`; use `npm run build && npm run start` to test caching.
|
||||
|
||||
@@ -6,3 +6,6 @@ class DnsCmsConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
from dnscms import signals # noqa: F401
|
||||
from dnscms.revalidation import register_signal_handlers
|
||||
|
||||
register_signal_handlers()
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Purge the frontend cache when content changes.
|
||||
|
||||
The frontend caches all GraphQL responses under a single "cms" cache tag;
|
||||
any content change POSTs to its /api/revalidate endpoint for a global purge.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import urllib.request
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models.signals import post_delete, post_save
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WEBHOOK_TIMEOUT_SECONDS = 5
|
||||
|
||||
# Non-page models exposed over GraphQL; pages are covered by wagtail signals
|
||||
REVALIDATING_MODELS = [
|
||||
"contacts.ContactEntity",
|
||||
"events.EventCategory",
|
||||
"events.EventOrganizer",
|
||||
"images.CustomImage",
|
||||
"openinghours.OpeningHoursSet",
|
||||
]
|
||||
|
||||
_warned_unconfigured = False
|
||||
|
||||
|
||||
def _send_webhook(url, secret, reason):
|
||||
request = urllib.request.Request(
|
||||
url, data=b"", method="POST", headers={"X-Revalidate-Secret": secret}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=WEBHOOK_TIMEOUT_SECONDS) as response:
|
||||
logger.info("frontend revalidation ok (%s): HTTP %s", reason, response.status)
|
||||
except Exception:
|
||||
logger.exception("frontend revalidation failed (%s)", reason)
|
||||
|
||||
|
||||
def trigger_frontend_revalidation(reason):
|
||||
global _warned_unconfigured
|
||||
secret = settings.REVALIDATE_WEBHOOK_SECRET
|
||||
if not secret:
|
||||
if not _warned_unconfigured:
|
||||
logger.warning("REVALIDATE_WEBHOOK_SECRET is not set, skipping frontend revalidation")
|
||||
_warned_unconfigured = True
|
||||
return
|
||||
url = f"{settings.FRONTEND_BASE_URL}/api/revalidate"
|
||||
# After commit so the frontend can't re-render from pre-commit data,
|
||||
# in a thread so publishing never blocks on the frontend
|
||||
transaction.on_commit(
|
||||
lambda: threading.Thread(
|
||||
target=_send_webhook, args=(url, secret, reason), daemon=True
|
||||
).start()
|
||||
)
|
||||
|
||||
|
||||
def _on_page_published(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"page_published: {instance.slug}")
|
||||
|
||||
|
||||
def _on_page_unpublished(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"page_unpublished: {instance.slug}")
|
||||
|
||||
|
||||
def _on_page_moved(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"post_page_move: {instance.slug}")
|
||||
|
||||
|
||||
def _on_page_deleted(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"page_deleted: {instance.slug}")
|
||||
|
||||
|
||||
def _on_model_changed(sender, instance, **kwargs):
|
||||
trigger_frontend_revalidation(f"{sender._meta.label}: {instance}")
|
||||
|
||||
|
||||
def register_signal_handlers():
|
||||
from wagtail.models import Page
|
||||
from wagtail.signals import page_published, page_unpublished, post_page_move
|
||||
|
||||
page_published.connect(_on_page_published, dispatch_uid="revalidation.page_published")
|
||||
page_unpublished.connect(_on_page_unpublished, dispatch_uid="revalidation.page_unpublished")
|
||||
post_page_move.connect(_on_page_moved, dispatch_uid="revalidation.post_page_move")
|
||||
# Wagtail sends no signal on page deletion; MTI deletes of any page type
|
||||
# also emit post_delete for the base Page row
|
||||
post_delete.connect(_on_page_deleted, sender=Page, dispatch_uid="revalidation.page_deleted")
|
||||
|
||||
for label in REVALIDATING_MODELS:
|
||||
post_save.connect(
|
||||
_on_model_changed, sender=label, dispatch_uid=f"revalidation.save.{label}"
|
||||
)
|
||||
post_delete.connect(
|
||||
_on_model_changed, sender=label, dispatch_uid=f"revalidation.delete.{label}"
|
||||
)
|
||||
@@ -200,6 +200,10 @@ BASE_URL = WAGTAIL_BASE_URL
|
||||
# redirect "View Live" clicks on the CMS host over to the headless frontend.
|
||||
FRONTEND_BASE_URL = os.environ.get("FRONTEND_BASE_URL", "http://localhost:3000").rstrip("/")
|
||||
|
||||
# Shared secret for the frontend cache purge webhook (see dnscms/revalidation.py).
|
||||
# Unset means no webhooks are sent.
|
||||
REVALIDATE_WEBHOOK_SECRET = os.environ.get("REVALIDATE_WEBHOOK_SECRET", "")
|
||||
|
||||
WAGTAIL_HEADLESS_PREVIEW = {
|
||||
"CLIENT_URLS": {"default": f"{FRONTEND_BASE_URL}/api/preview"},
|
||||
"SERVE_BASE_URL": FRONTEND_BASE_URL,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .base import *
|
||||
from .base import * # noqa: F403
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
@@ -12,6 +12,6 @@ ALLOWED_HOSTS = ["*"]
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
try:
|
||||
from .local import *
|
||||
from .local import * # noqa: F403
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from .base import *
|
||||
from .base import * # noqa: F403
|
||||
|
||||
DEBUG = False
|
||||
|
||||
try:
|
||||
from .local import *
|
||||
from .local import * # noqa: F403
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import FormView
|
||||
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
|
||||
from wagtail.models import ReferenceIndex, Revision
|
||||
|
||||
from dnscms.utils import slugify
|
||||
from events.models import EventOrganizer, EventOrganizerLink, EventPage
|
||||
from events.views import event_organizer_chooser_viewset
|
||||
|
||||
|
||||
def _check_relations_are_handled():
|
||||
"""Refuse to merge if EventOrganizer has gained a relation this module doesn't handle."""
|
||||
for relation in EventOrganizer._meta.related_objects:
|
||||
if relation.many_to_many and relation.through is EventOrganizerLink:
|
||||
continue
|
||||
if relation.related_model is EventOrganizerLink and relation.field.name == "organizer":
|
||||
continue
|
||||
raise NotImplementedError(
|
||||
f"merge_event_organizers does not handle the {relation.name!r} relation"
|
||||
)
|
||||
|
||||
|
||||
def _repoint_links(survivor, loser):
|
||||
loser_links = EventOrganizerLink.objects.filter(organizer=loser)
|
||||
affected_event_ids = set(loser_links.values_list("event_id", flat=True))
|
||||
# drop the loser's link where the event links both, to respect the unique constraint
|
||||
doubly_linked = EventOrganizerLink.objects.filter(
|
||||
organizer=survivor, event_id__in=affected_event_ids
|
||||
).values_list("event_id", flat=True)
|
||||
loser_links.filter(event_id__in=doubly_linked).delete()
|
||||
loser_links.update(organizer=survivor)
|
||||
return affected_event_ids
|
||||
|
||||
|
||||
def _rewrite_revisions(survivor, loser):
|
||||
"""Rewrite organizer ids in revision JSON, which would otherwise dangle after the delete."""
|
||||
affected_event_ids = set()
|
||||
event_content_type = ContentType.objects.get_for_model(EventPage)
|
||||
for revision in Revision.objects.filter(content_type=event_content_type).iterator():
|
||||
links = revision.content.get("organizer_links") or []
|
||||
if not any(link.get("organizer") == loser.pk for link in links):
|
||||
continue
|
||||
seen_organizer_ids = set()
|
||||
rewritten = []
|
||||
for link in links:
|
||||
if link.get("organizer") == loser.pk:
|
||||
link = {**link, "organizer": survivor.pk}
|
||||
if link.get("organizer") in seen_organizer_ids:
|
||||
continue
|
||||
seen_organizer_ids.add(link.get("organizer"))
|
||||
rewritten.append(link)
|
||||
revision.content["organizer_links"] = rewritten
|
||||
revision.save(update_fields=["content"])
|
||||
affected_event_ids.add(int(revision.object_id))
|
||||
return affected_event_ids
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def merge_event_organizers(organizer_a, organizer_b, *, name=None, slug=None):
|
||||
"""
|
||||
Merge two organizers, keeping the one with the lowest primary key. Event
|
||||
links and event page revisions are repointed before the other organizer is
|
||||
deleted. ``name``/``slug`` override the survivor's fields; a blank
|
||||
``association``/``external_url`` is filled in from the deleted organizer.
|
||||
"""
|
||||
if organizer_a.pk == organizer_b.pk:
|
||||
raise ValueError("Cannot merge an organizer with itself.")
|
||||
_check_relations_are_handled()
|
||||
|
||||
survivor, loser = sorted([organizer_a, organizer_b], key=lambda organizer: organizer.pk)
|
||||
|
||||
affected_event_ids = _repoint_links(survivor, loser)
|
||||
affected_event_ids |= _rewrite_revisions(survivor, loser)
|
||||
|
||||
if name:
|
||||
survivor.name = name
|
||||
if slug:
|
||||
survivor.slug = slug
|
||||
if not survivor.association_id:
|
||||
survivor.association_id = loser.association_id
|
||||
if not survivor.external_url:
|
||||
survivor.external_url = loser.external_url
|
||||
survivor.save()
|
||||
|
||||
loser.delete()
|
||||
|
||||
# the bulk link update bypasses the save-time reference index maintenance
|
||||
for event in EventPage.objects.filter(pk__in=affected_event_ids):
|
||||
ReferenceIndex.create_or_update_for_object(event)
|
||||
|
||||
return survivor
|
||||
|
||||
|
||||
CUSTOM_VALUE = "__custom__"
|
||||
|
||||
|
||||
class MergeOrganizersSelectForm(forms.Form):
|
||||
organizer_a = forms.ModelChoiceField(
|
||||
label=_("First organizer"),
|
||||
queryset=EventOrganizer.objects.all(),
|
||||
widget=event_organizer_chooser_viewset.widget_class(),
|
||||
)
|
||||
organizer_b = forms.ModelChoiceField(
|
||||
label=_("Second organizer"),
|
||||
queryset=EventOrganizer.objects.all(),
|
||||
widget=event_organizer_chooser_viewset.widget_class(),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
organizer_a = cleaned.get("organizer_a")
|
||||
organizer_b = cleaned.get("organizer_b")
|
||||
if organizer_a and organizer_b and organizer_a.pk == organizer_b.pk:
|
||||
raise ValidationError(_("Pick two different organizers."))
|
||||
return cleaned
|
||||
|
||||
|
||||
class MergeOrganizersConfirmForm(forms.Form):
|
||||
name = forms.ChoiceField(label=_("Name to keep"), widget=forms.RadioSelect)
|
||||
name_custom = forms.CharField(label=_("Custom name"), required=False, max_length=100)
|
||||
slug = forms.ChoiceField(label=_("Slug to keep"), widget=forms.RadioSelect)
|
||||
slug_custom = forms.CharField(label=_("Custom slug"), required=False, max_length=255)
|
||||
|
||||
def __init__(self, *args, survivor, loser, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["name"].choices = self._choices(survivor.name, loser.name)
|
||||
self.fields["name"].initial = survivor.name
|
||||
self.fields["slug"].choices = self._choices(survivor.slug, loser.slug)
|
||||
self.fields["slug"].initial = survivor.slug
|
||||
|
||||
@staticmethod
|
||||
def _choices(*values):
|
||||
unique_values = dict.fromkeys(values)
|
||||
return [(value, value) for value in unique_values] + [
|
||||
(CUSTOM_VALUE, _("Something else (enter below)"))
|
||||
]
|
||||
|
||||
def _resolve(self, cleaned, field):
|
||||
choice = cleaned.get(field)
|
||||
custom = (cleaned.get(f"{field}_custom") or "").strip()
|
||||
if choice == CUSTOM_VALUE:
|
||||
if not custom:
|
||||
self.add_error(
|
||||
f"{field}_custom", _("Enter a value, or pick an existing one above.")
|
||||
)
|
||||
return custom
|
||||
return choice
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
cleaned["final_name"] = self._resolve(cleaned, "name")
|
||||
cleaned["final_slug"] = slugify(self._resolve(cleaned, "slug") or "")
|
||||
return cleaned
|
||||
|
||||
|
||||
class MergeOrganizersSelectView(WagtailAdminTemplateMixin, FormView):
|
||||
template_name = "events/merge_organizers_select.html"
|
||||
form_class = MergeOrganizersSelectForm
|
||||
page_title = _("Merge organizers")
|
||||
header_icon = "group"
|
||||
|
||||
def form_valid(self, form):
|
||||
params = urlencode(
|
||||
{
|
||||
"organizer_a": form.cleaned_data["organizer_a"].pk,
|
||||
"organizer_b": form.cleaned_data["organizer_b"].pk,
|
||||
}
|
||||
)
|
||||
return redirect(f"{reverse('events_merge_organizers_confirm')}?{params}")
|
||||
|
||||
|
||||
class MergeOrganizersConfirmView(WagtailAdminTemplateMixin, FormView):
|
||||
template_name = "events/merge_organizers_confirm.html"
|
||||
form_class = MergeOrganizersConfirmForm
|
||||
page_title = _("Merge organizers")
|
||||
header_icon = "group"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
pks = {int(request.GET.get(param, "")) for param in ("organizer_a", "organizer_b")}
|
||||
except ValueError:
|
||||
pks = set()
|
||||
organizers = list(EventOrganizer.objects.filter(pk__in=pks).order_by("pk"))
|
||||
if len(organizers) != 2:
|
||||
messages.error(request, _("Pick two different organizers to merge."))
|
||||
return redirect("events_merge_organizers")
|
||||
self.survivor, self.loser = organizers
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
return {**super().get_form_kwargs(), "survivor": self.survivor, "loser": self.loser}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["survivor"] = self.survivor
|
||||
context["loser"] = self.loser
|
||||
context["survivor_event_count"] = self.survivor.organized_events.count()
|
||||
context["loser_event_count"] = self.loser.organized_events.count()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
survivor = merge_event_organizers(
|
||||
self.survivor,
|
||||
self.loser,
|
||||
name=form.cleaned_data["final_name"],
|
||||
slug=form.cleaned_data["final_slug"],
|
||||
)
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Merged "%(loser)s" into "%(survivor)s".')
|
||||
% {"loser": self.loser.name, "survivor": survivor.name},
|
||||
)
|
||||
return redirect(reverse("wagtailsnippets_events_eventorganizer:list"))
|
||||
@@ -164,7 +164,7 @@ class EventOrganizerLink(Orderable):
|
||||
]
|
||||
|
||||
|
||||
@register_snippet
|
||||
# registered as a snippet via EventOrganizerSnippetViewSet in wagtail_hooks.py
|
||||
@register_query_field("eventOrganizer", "eventOrganizers")
|
||||
class EventOrganizer(index.Indexed, ClusterableModel):
|
||||
objects = WPAwareManager()
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "wagtailadmin/generic/base.html" %}
|
||||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
{% block main_content %}
|
||||
<table class="listing">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Outcome" %}</th>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Slug" %}</th>
|
||||
<th>{% trans "Association" %}</th>
|
||||
<th>{% trans "External URL" %}</th>
|
||||
<th>{% trans "Events" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{% trans "Survives" %}</strong></td>
|
||||
<td>{{ survivor.pk }}</td>
|
||||
<td>{{ survivor.name }}</td>
|
||||
<td>{{ survivor.slug }}</td>
|
||||
<td>{{ survivor.association|default:"—" }}</td>
|
||||
<td>{{ survivor.external_url|default:"—" }}</td>
|
||||
<td>{{ survivor_event_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{% trans "Is deleted" %}</strong></td>
|
||||
<td>{{ loser.pk }}</td>
|
||||
<td>{{ loser.name }}</td>
|
||||
<td>{{ loser.slug }}</td>
|
||||
<td>{{ loser.association|default:"—" }}</td>
|
||||
<td>{{ loser.external_url|default:"—" }}</td>
|
||||
<td>{{ loser_event_count }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p class="help-block help-warning">
|
||||
{% blocktrans with loser_name=loser.name survivor_name=survivor.name trimmed %}
|
||||
Events organized by “{{ loser_name }}” will be moved to “{{ survivor_name }}”,
|
||||
and “{{ loser_name }}” will be deleted. This cannot be undone.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<ul class="fields">
|
||||
{% for field in form.visible_fields %}
|
||||
<li>{% formattedfield field %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<button type="submit" class="button serious">{% trans "Merge organizers" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "wagtailadmin/generic/base.html" %}
|
||||
{% load i18n wagtailadmin_tags %}
|
||||
|
||||
{% block main_content %}
|
||||
<p>
|
||||
{% trans "Pick the two organizers to merge. The one with the lowest ID survives; the other one is deleted after its events have been moved over." %}
|
||||
</p>
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="help-block help-critical">{{ form.non_field_errors }}</div>
|
||||
{% endif %}
|
||||
<ul class="fields">
|
||||
{% for field in form.visible_fields %}
|
||||
<li>{% formattedfield field %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<button type="submit" class="button">{% trans "Continue" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
{{ form.media.js }}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
{{ form.media.css }}
|
||||
{% endblock %}
|
||||
@@ -1,11 +1,55 @@
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail.admin.forms import WagtailAdminModelForm
|
||||
from wagtail.admin.forms.choosers import BaseFilterForm
|
||||
from wagtail.admin.ui.tables import UpdatedAtColumn
|
||||
from wagtail.admin.views.generic.chooser import ChooseResultsView, ChooseView
|
||||
from wagtail.admin.viewsets.chooser import ChooserViewSet
|
||||
from wagtail.snippets.views.snippets import SnippetViewSet
|
||||
|
||||
from dnscms.utils import slugify
|
||||
from events.models import EventOrganizer
|
||||
|
||||
|
||||
class EventOrganizerSearchForm(BaseFilterForm):
|
||||
"""Substring search on name/slug"""
|
||||
|
||||
q = forms.CharField(
|
||||
label=_("Search term"),
|
||||
widget=forms.TextInput(attrs={"placeholder": _("Search")}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def filter(self, objects):
|
||||
objects = super().filter(objects)
|
||||
search_query = self.cleaned_data.get("q")
|
||||
if search_query:
|
||||
objects = objects.filter(
|
||||
Q(name__icontains=search_query) | Q(slug__icontains=search_query)
|
||||
)
|
||||
self.is_searching = True
|
||||
self.search_query = search_query
|
||||
return objects
|
||||
|
||||
|
||||
class EventOrganizerChooseView(ChooseView):
|
||||
filter_form_class = EventOrganizerSearchForm
|
||||
|
||||
|
||||
class EventOrganizerChooseResultsView(ChooseResultsView):
|
||||
filter_form_class = EventOrganizerSearchForm
|
||||
|
||||
|
||||
class EventOrganizerSnippetViewSet(SnippetViewSet):
|
||||
model = EventOrganizer
|
||||
icon = "group"
|
||||
list_display = ["name", "slug", UpdatedAtColumn()]
|
||||
# unset the search backend so the listing search filters on icontains instead
|
||||
search_backend_name = None
|
||||
search_fields = ["name", "slug"]
|
||||
|
||||
|
||||
class EventOrganizerCreationForm(WagtailAdminModelForm):
|
||||
class Meta:
|
||||
model = EventOrganizer
|
||||
@@ -29,6 +73,8 @@ class EventOrganizerChooserViewSet(ChooserViewSet):
|
||||
choose_another_text = _("Choose another organizer")
|
||||
edit_item_text = _("Edit this organizer")
|
||||
creation_form_class = EventOrganizerCreationForm
|
||||
choose_view_class = EventOrganizerChooseView
|
||||
choose_results_view_class = EventOrganizerChooseResultsView
|
||||
|
||||
|
||||
event_organizer_chooser_viewset = EventOrganizerChooserViewSet("event_organizer_chooser")
|
||||
|
||||
@@ -1,7 +1,48 @@
|
||||
from django.urls import path, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.auth import user_passes_test
|
||||
from wagtail.admin.menu import MenuItem
|
||||
from wagtail.snippets.models import register_snippet
|
||||
|
||||
from .admin import event_sidebar_viewset, event_explorer_viewset
|
||||
from .views import event_organizer_chooser_viewset
|
||||
from .merge_organizers import MergeOrganizersConfirmView, MergeOrganizersSelectView
|
||||
from .views import EventOrganizerSnippetViewSet, event_organizer_chooser_viewset
|
||||
|
||||
register_snippet(EventOrganizerSnippetViewSet)
|
||||
|
||||
superuser_only = user_passes_test(lambda user: user.is_superuser)
|
||||
|
||||
|
||||
class SuperuserMenuItem(MenuItem):
|
||||
def is_shown(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
|
||||
@hooks.register("register_admin_urls")
|
||||
def register_merge_organizers_urls():
|
||||
return [
|
||||
path(
|
||||
"merge-organizers/",
|
||||
superuser_only(MergeOrganizersSelectView.as_view()),
|
||||
name="events_merge_organizers",
|
||||
),
|
||||
path(
|
||||
"merge-organizers/confirm/",
|
||||
superuser_only(MergeOrganizersConfirmView.as_view()),
|
||||
name="events_merge_organizers_confirm",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@hooks.register("register_settings_menu_item")
|
||||
def register_merge_organizers_menu_item():
|
||||
return SuperuserMenuItem(
|
||||
_("Merge organizers"),
|
||||
reverse_lazy("events_merge_organizers"),
|
||||
icon_name="group",
|
||||
order=1100,
|
||||
)
|
||||
|
||||
|
||||
@hooks.register("register_admin_viewset")
|
||||
|
||||
@@ -13,8 +13,6 @@ 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(
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
||||
@@ -17,7 +17,7 @@ dependencies = [
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.15.20,<0.16",
|
||||
"ruff>=0.16.1,<0.17",
|
||||
"pytest>=9.1.1,<10",
|
||||
"pytest-cov>=7.0.0,<8",
|
||||
"pytest-django>=4.12.0,<5",
|
||||
@@ -33,11 +33,11 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 99
|
||||
exclude = ["**/migrations/*.py"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["F", "E", "W", "Q", "UP", "DJ"]
|
||||
ignore = []
|
||||
exclude = ["**/migrations/*.py"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
DJANGO_SETTINGS_MODULE = "dnscms.settings.test"
|
||||
|
||||
@@ -2,5 +2,5 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class SponsorsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'sponsors'
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "sponsors"
|
||||
|
||||
@@ -4,6 +4,7 @@ import pytest
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import connection
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from events.admin import EventDateColumn, OrganizersColumn
|
||||
@@ -572,3 +573,31 @@ def test_graphql_event_index_returns_all_fields_for_comprehensive_event(
|
||||
assert datetime.fromisoformat(venue_occ["end"]) == venue_occ_db.end
|
||||
assert datetime.fromisoformat(custom_occ["start"]) == custom_occ_db.start
|
||||
assert datetime.fromisoformat(custom_occ["end"]) == custom_occ_db.end
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def searchable_organizers(db):
|
||||
return [
|
||||
EventOrganizer.objects.create(name="Arrangementsutvalget", slug="arrangementsutvalget"),
|
||||
EventOrganizer.objects.create(name="Cinema Neuf", slug="kino"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url_name",
|
||||
["event_organizer_chooser:choose", "wagtailsnippets_events_eventorganizer:list"],
|
||||
)
|
||||
def test_organizer_search_matches_substrings_of_name_and_slug(
|
||||
admin_client, searchable_organizers, url_name
|
||||
):
|
||||
url = reverse(url_name)
|
||||
|
||||
# mid-word match inside a compound name
|
||||
content = admin_client.get(url, {"q": "utvalget"}).content.decode()
|
||||
assert "Arrangementsutvalget" in content
|
||||
assert "Cinema Neuf" not in content
|
||||
|
||||
# slug-only match
|
||||
content = admin_client.get(url, {"q": "kino"}).content.decode()
|
||||
assert "Cinema Neuf" in content
|
||||
assert "Arrangementsutvalget" not in content
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from wagtail.models import ReferenceIndex
|
||||
|
||||
from events.merge_organizers import CUSTOM_VALUE, merge_event_organizers
|
||||
from events.models import EventOrganizer, EventOrganizerLink, EventPage
|
||||
from tests.conftest import AssociationPageFactory, EventPageFactory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def organizers(db):
|
||||
keeper = EventOrganizer.objects.create(name="Kulturutvalget", slug="kulturutvalget")
|
||||
goner = EventOrganizer.objects.create(name="KU", slug="ku")
|
||||
return keeper, goner
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def editor_client(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="editor", password="pw")
|
||||
user.user_permissions.add(Permission.objects.get(codename="access_admin"))
|
||||
client.force_login(user)
|
||||
return client
|
||||
|
||||
|
||||
def test_merge_repoints_event_links_and_deletes_loser(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
|
||||
merged = merge_event_organizers(keeper, goner)
|
||||
|
||||
assert merged.pk == keeper.pk
|
||||
assert not EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
assert list(event.organizer_links.values_list("organizer_id", flat=True)) == [keeper.pk]
|
||||
|
||||
|
||||
def test_lowest_pk_survives_regardless_of_argument_order(organizers):
|
||||
keeper, goner = organizers
|
||||
|
||||
merged = merge_event_organizers(goner, keeper)
|
||||
|
||||
assert merged.pk == keeper.pk
|
||||
assert not EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
|
||||
|
||||
def test_merge_applies_chosen_name_and_slug(organizers):
|
||||
keeper, goner = organizers
|
||||
|
||||
merged = merge_event_organizers(keeper, goner, name="KU", slug="ku")
|
||||
|
||||
assert merged.name == "KU"
|
||||
assert merged.slug == "ku"
|
||||
|
||||
|
||||
def test_merge_keeps_survivor_name_and_slug_by_default(organizers):
|
||||
keeper, goner = organizers
|
||||
|
||||
merged = merge_event_organizers(keeper, goner)
|
||||
|
||||
assert merged.name == "Kulturutvalget"
|
||||
assert merged.slug == "kulturutvalget"
|
||||
|
||||
|
||||
def test_merge_collapses_duplicate_links_on_same_event(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=keeper)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
|
||||
merge_event_organizers(keeper, goner)
|
||||
|
||||
assert list(event.organizer_links.values_list("organizer_id", flat=True)) == [keeper.pk]
|
||||
|
||||
|
||||
def test_merge_fills_blank_fields_from_loser(association_index, organizers):
|
||||
keeper, goner = organizers
|
||||
association = AssociationPageFactory(parent=association_index)
|
||||
goner.association = association
|
||||
goner.external_url = "https://example.com"
|
||||
goner.save()
|
||||
|
||||
merged = merge_event_organizers(keeper, goner)
|
||||
|
||||
assert merged.association_id == association.pk
|
||||
assert merged.external_url == "https://example.com"
|
||||
|
||||
|
||||
def test_merge_keeps_survivor_fields_when_set(organizers):
|
||||
keeper, goner = organizers
|
||||
keeper.external_url = "https://keeper.example.com"
|
||||
keeper.save()
|
||||
goner.external_url = "https://goner.example.com"
|
||||
goner.save()
|
||||
|
||||
merged = merge_event_organizers(keeper, goner)
|
||||
|
||||
assert merged.external_url == "https://keeper.example.com"
|
||||
|
||||
|
||||
def test_merge_rewrites_event_page_revisions(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
revision = EventPage.objects.get(pk=event.pk).save_revision()
|
||||
assert [link["organizer"] for link in revision.content["organizer_links"]] == [goner.pk]
|
||||
|
||||
merge_event_organizers(keeper, goner)
|
||||
|
||||
revision.refresh_from_db()
|
||||
assert [link["organizer"] for link in revision.content["organizer_links"]] == [keeper.pk]
|
||||
|
||||
|
||||
def test_merge_dedupes_organizers_within_revisions(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=keeper)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
revision = EventPage.objects.get(pk=event.pk).save_revision()
|
||||
|
||||
merge_event_organizers(keeper, goner)
|
||||
|
||||
revision.refresh_from_db()
|
||||
assert [link["organizer"] for link in revision.content["organizer_links"]] == [keeper.pk]
|
||||
|
||||
|
||||
def test_merge_updates_reference_index(event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
ReferenceIndex.create_or_update_for_object(EventPage.objects.get(pk=event.pk))
|
||||
organizer_ct = ContentType.objects.get_for_model(EventOrganizer)
|
||||
assert ReferenceIndex.objects.filter(
|
||||
to_content_type=organizer_ct, to_object_id=str(goner.pk)
|
||||
).exists()
|
||||
|
||||
merge_event_organizers(keeper, goner)
|
||||
|
||||
assert not ReferenceIndex.objects.filter(
|
||||
to_content_type=organizer_ct, to_object_id=str(goner.pk)
|
||||
).exists()
|
||||
assert ReferenceIndex.objects.filter(
|
||||
to_content_type=organizer_ct, to_object_id=str(keeper.pk)
|
||||
).exists()
|
||||
|
||||
|
||||
def test_merge_refuses_same_organizer(organizers):
|
||||
keeper, _ = organizers
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
merge_event_organizers(keeper, keeper)
|
||||
|
||||
|
||||
def test_merge_views_require_superuser(editor_client, organizers):
|
||||
keeper, goner = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
urls = [
|
||||
reverse("events_merge_organizers"),
|
||||
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||
]
|
||||
|
||||
for url in urls:
|
||||
response = editor_client.get(url)
|
||||
assert response.status_code == 302
|
||||
assert response.url == reverse("wagtailadmin_home")
|
||||
assert EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
|
||||
|
||||
def test_select_view_redirects_to_confirm(admin_client, organizers):
|
||||
keeper, goner = organizers
|
||||
|
||||
response = admin_client.post(
|
||||
reverse("events_merge_organizers"),
|
||||
{"organizer_a": keeper.pk, "organizer_b": goner.pk},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
assert response.url == f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}"
|
||||
|
||||
|
||||
def test_select_view_rejects_same_organizer_twice(admin_client, organizers):
|
||||
keeper, _ = organizers
|
||||
|
||||
response = admin_client.post(
|
||||
reverse("events_merge_organizers"),
|
||||
{"organizer_a": keeper.pk, "organizer_b": keeper.pk},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Pick two different organizers." in response.content.decode()
|
||||
|
||||
|
||||
def test_confirm_view_shows_both_organizers(admin_client, organizers):
|
||||
keeper, goner = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
|
||||
response = admin_client.get(f"{confirm_url}?organizer_a={goner.pk}&organizer_b={keeper.pk}")
|
||||
|
||||
content = response.content.decode()
|
||||
assert response.status_code == 200
|
||||
assert "Kulturutvalget" in content
|
||||
assert "KU" in content
|
||||
|
||||
|
||||
def test_confirm_view_rejects_missing_or_equal_organizers(admin_client, organizers):
|
||||
keeper, _ = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
queries = [
|
||||
"",
|
||||
f"?organizer_a={keeper.pk}&organizer_b={keeper.pk}",
|
||||
f"?organizer_a=abc&organizer_b={keeper.pk}",
|
||||
f"?organizer_a={keeper.pk}&organizer_b=999999",
|
||||
]
|
||||
|
||||
for query in queries:
|
||||
response = admin_client.get(confirm_url + query)
|
||||
assert response.status_code == 302
|
||||
assert response.url == reverse("events_merge_organizers")
|
||||
|
||||
|
||||
def test_confirm_view_merges_with_picked_values(admin_client, event_index, organizers):
|
||||
keeper, goner = organizers
|
||||
event = EventPageFactory(parent=event_index)
|
||||
EventOrganizerLink.objects.create(event=event, organizer=goner)
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
|
||||
response = admin_client.post(
|
||||
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||
{"name": goner.name, "name_custom": "", "slug": goner.slug, "slug_custom": ""},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == reverse("wagtailsnippets_events_eventorganizer:list")
|
||||
keeper.refresh_from_db()
|
||||
assert keeper.name == "KU"
|
||||
assert keeper.slug == "ku"
|
||||
assert not EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
assert list(event.organizer_links.values_list("organizer_id", flat=True)) == [keeper.pk]
|
||||
|
||||
|
||||
def test_confirm_view_merges_with_custom_values(admin_client, organizers):
|
||||
keeper, goner = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
|
||||
response = admin_client.post(
|
||||
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||
{
|
||||
"name": CUSTOM_VALUE,
|
||||
"name_custom": "Kulturutvalget (KU)",
|
||||
"slug": CUSTOM_VALUE,
|
||||
"slug_custom": "Kulturutvalget KU",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 302
|
||||
keeper.refresh_from_db()
|
||||
assert keeper.name == "Kulturutvalget (KU)"
|
||||
assert keeper.slug == "kulturutvalget-ku"
|
||||
|
||||
|
||||
def test_confirm_view_requires_custom_value_when_picked(admin_client, organizers):
|
||||
keeper, goner = organizers
|
||||
confirm_url = reverse("events_merge_organizers_confirm")
|
||||
|
||||
response = admin_client.post(
|
||||
f"{confirm_url}?organizer_a={keeper.pk}&organizer_b={goner.pk}",
|
||||
{"name": CUSTOM_VALUE, "name_custom": "", "slug": keeper.slug, "slug_custom": ""},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert EventOrganizer.objects.filter(pk=goner.pk).exists()
|
||||
@@ -0,0 +1,127 @@
|
||||
import logging
|
||||
import urllib.error
|
||||
|
||||
import pytest
|
||||
|
||||
from dnscms import revalidation
|
||||
from events.models import EventCategory
|
||||
from tests.conftest import GenericPageFactory
|
||||
|
||||
WEBHOOK_URL = "https://frontend.example.com/api/revalidate"
|
||||
|
||||
|
||||
class InlineThread:
|
||||
def __init__(self, target=None, args=(), kwargs=None, daemon=None):
|
||||
self._target = target
|
||||
self._args = args
|
||||
self._kwargs = kwargs or {}
|
||||
|
||||
def start(self):
|
||||
self._target(*self._args, **self._kwargs)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webhook_calls(monkeypatch, settings):
|
||||
settings.REVALIDATE_WEBHOOK_SECRET = "s3cret"
|
||||
settings.FRONTEND_BASE_URL = "https://frontend.example.com"
|
||||
calls = []
|
||||
monkeypatch.setattr(revalidation.threading, "Thread", InlineThread)
|
||||
monkeypatch.setattr(
|
||||
revalidation, "_send_webhook", lambda url, secret, reason: calls.append((url, secret))
|
||||
)
|
||||
return calls
|
||||
|
||||
|
||||
def test_page_publish_triggers_webhook(
|
||||
home_page, webhook_calls, django_capture_on_commit_callbacks
|
||||
):
|
||||
page = GenericPageFactory(parent=home_page)
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
page.save_revision().publish()
|
||||
|
||||
assert webhook_calls == [(WEBHOOK_URL, "s3cret")]
|
||||
|
||||
|
||||
def test_page_unpublish_triggers_webhook(
|
||||
home_page, webhook_calls, django_capture_on_commit_callbacks
|
||||
):
|
||||
page = GenericPageFactory(parent=home_page)
|
||||
page.save_revision().publish()
|
||||
webhook_calls.clear()
|
||||
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
page.unpublish()
|
||||
|
||||
assert webhook_calls == [(WEBHOOK_URL, "s3cret")]
|
||||
|
||||
|
||||
def test_page_delete_triggers_webhook(
|
||||
home_page, webhook_calls, django_capture_on_commit_callbacks
|
||||
):
|
||||
# live=False isolates the post_delete path: deleting a live page would
|
||||
# also fire page_unpublished (a harmless duplicate purge in production)
|
||||
page = GenericPageFactory(parent=home_page, live=False)
|
||||
webhook_calls.clear()
|
||||
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
page.delete()
|
||||
|
||||
assert webhook_calls == [(WEBHOOK_URL, "s3cret")]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_snippet_save_triggers_webhook(webhook_calls, django_capture_on_commit_callbacks):
|
||||
with django_capture_on_commit_callbacks(execute=True):
|
||||
EventCategory.objects.create(name="Konsert", slug="konsert")
|
||||
|
||||
assert webhook_calls == [(WEBHOOK_URL, "s3cret")]
|
||||
|
||||
|
||||
def test_unset_secret_skips_and_warns_once(settings, monkeypatch, caplog):
|
||||
settings.REVALIDATE_WEBHOOK_SECRET = ""
|
||||
monkeypatch.setattr(revalidation, "_warned_unconfigured", False)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="dnscms.revalidation"):
|
||||
revalidation.trigger_frontend_revalidation("test")
|
||||
revalidation.trigger_frontend_revalidation("test")
|
||||
|
||||
warnings = [r for r in caplog.records if r.levelno == logging.WARNING]
|
||||
assert len(warnings) == 1
|
||||
|
||||
|
||||
def test_send_webhook_failure_is_swallowed(monkeypatch, caplog):
|
||||
def raise_urlerror(request, timeout):
|
||||
raise urllib.error.URLError("connection refused")
|
||||
|
||||
monkeypatch.setattr(revalidation.urllib.request, "urlopen", raise_urlerror)
|
||||
|
||||
with caplog.at_level(logging.ERROR, logger="dnscms.revalidation"):
|
||||
revalidation._send_webhook(WEBHOOK_URL, "s3cret", "test")
|
||||
|
||||
assert any("frontend revalidation failed" in r.message for r in caplog.records)
|
||||
|
||||
|
||||
def test_send_webhook_request_shape(monkeypatch):
|
||||
seen = {}
|
||||
|
||||
class FakeResponse:
|
||||
status = 200
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
return False
|
||||
|
||||
def fake_urlopen(request, timeout):
|
||||
seen["request"] = request
|
||||
seen["timeout"] = timeout
|
||||
return FakeResponse()
|
||||
|
||||
monkeypatch.setattr(revalidation.urllib.request, "urlopen", fake_urlopen)
|
||||
revalidation._send_webhook(WEBHOOK_URL, "s3cret", "test")
|
||||
|
||||
request = seen["request"]
|
||||
assert request.get_method() == "POST"
|
||||
assert request.get_header("X-revalidate-secret") == "s3cret"
|
||||
assert seen["timeout"] == revalidation.WEBHOOK_TIMEOUT_SECONDS
|
||||
Generated
+186
-163
@@ -13,53 +13,68 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.9.1"
|
||||
version = "3.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870, upload-time = "2025-07-08T09:07:43.344Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/26/3b59f2bdae5f640389becb1f673cded775287f5fc4f816309d9ca9a3f93d/asgiref-3.12.1.tar.gz", hash = "sha256:59dcb51c272ad209d59bed5708a64a333083e86017d7fcdd67498eeab7784340", size = 42378, upload-time = "2026-07-14T09:56:18.087Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790, upload-time = "2025-07-08T09:07:41.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.13.4"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "soupsieve" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.8.3"
|
||||
version = "2026.7.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.3"
|
||||
version = "3.4.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -73,41 +88,41 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.14.0"
|
||||
version = "7.15.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/45/78dbf9604ee5b3db24efbf26bed1cb58862fb40480cba821963c69348751/coverage-7.15.3.tar.gz", hash = "sha256:ae7ea5a4614acf399ef0483c4cb34f8f8f01df848d8fcbe7d3ce0865733f1c4d", size = 935592, upload-time = "2026-08-02T18:50:17.006Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/18/b9a6586d73992807c26f9a5f274131be3d76b56b18a82b9392e2a25d2e45/coverage-7.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9aed9fa983514ca032790f3fe0d1c0e42ca7e16b42432af1706b50a9a46bef5d", size = 220036, upload-time = "2026-05-10T18:01:33.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ba3b8390db29296dbbf49e91b6fe08f990743a90c8f447ba4c2ffc29670dfa63", size = 220368, upload-time = "2026-05-10T18:01:34.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/aa/c12e52a5ba148d9995229d557e3be6e554fe469addc0e9241b2f0956d8ea/coverage-7.14.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3a5d8e876dfa2f102e970b183863d6dedd023d3c0eeca1fe7a9787bc5f28b212", size = 251417, upload-time = "2026-05-10T18:01:36.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ebb8f4614a3787d567e610bbfdf96a4798dd69a1afb1bd8ad228d4111fe6ff3", size = 253924, upload-time = "2026-05-10T18:01:38.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/c4/59c3de0bd1b538824173fd518fed51c1ce740ca5ed68e74545983f4053a9/coverage-7.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b9bf47223dd8db3d4c4b2e443b02bace480d428f0822c3f991600448a176c97", size = 255269, upload-time = "2026-05-10T18:01:40.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/a9/36dfa153a62040296f6e7febfdb20a5720622f6ef5a81a41e8237b9a5344/coverage-7.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3485a836550b303d006d57cc06e3d5afaabc642c77050b7c985a97b13e3776b8", size = 257583, upload-time = "2026-05-10T18:01:42.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/7b/cc2c048d4114d9ab1c2409e9ee365e5ae10736df6dffcfc9444effa6c708/coverage-7.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e7e88110bae996d199d1693ca8ec3fd52441d426401ae963437598667b4c5eb", size = 251434, upload-time = "2026-05-10T18:01:44.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/df/6770eaa576e604575e9a78055313250faef5faa84bd6f71a39fece519c43/coverage-7.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15228a6800ce7bdf1b74800595e56db7138cecb338fdbf044806e10dcf182dfe", size = 253280, upload-time = "2026-05-10T18:01:46.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/9e/1c0264514a3f98259a6d64765a397b2c8373e3ba59ee722a4802d3ec0c61/coverage-7.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d26ac7f5398bafc5b57421ad994e8a4749e8a7a0e62d05ec7d53014d5963bfa", size = 251241, upload-time = "2026-05-10T18:01:48.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/16/4efdf3e3c4079cdbf0ece56a2fea872df9e8a3e15a13a0af4400e1075944/coverage-7.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb73254ff43c911c967a899e1359bc5049b4b115d6e8fbdde4937d0a2246cd5", size = 255516, upload-time = "2026-05-10T18:01:50.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/69/b1de96346603881b3d1bc8d6447c83200e1c9700ffbaff926ba01ff5724c/coverage-7.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:454a380af72c6adada298ed270d38c7a391288198dbfb8467f786f588751a90c", size = 251059, upload-time = "2026-05-10T18:01:52.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/66/2881853e0363a5e0a724d1103e53650795367471b6afb234f8b49e713bc6/coverage-7.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:65c86fb646d2bd2972e96bd1a8b45817ed907cee68655d6295fe7ec031d04cca", size = 252716, upload-time = "2026-05-10T18:01:54.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/5c/0d3305d002c41dcde873dbe456491e663dc55152ca526b630b5c47efd62f/coverage-7.14.0-cp314-cp314-win32.whl", hash = "sha256:6a6516b02a6101398e19a3f44820f69bab2590697f7def4331f668b14adaf828", size = 222788, upload-time = "2026-05-10T18:01:56.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:45e0f79d8351fa76e256716df91eab12890d32678b9590df7ae1042e4bd4cf5d", size = 223600, upload-time = "2026-05-10T18:01:58.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/70/a18c408e674bc26281cadaedc7351f929bd2094e191e4b15271c30b084cc/coverage-7.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:4b899594a8b2d81e5cc064a0d7f9cac2081fed91049456cae7676787e41549c9", size = 222168, upload-time = "2026-05-10T18:02:00.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/89/2681f071d238b62aff8dfc2ab44fc24cfdb38d1c01f391a80522ff5d3a16/coverage-7.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f580f8c80acd94ac72e863efe2cab791d8c38d153e0b463b92dfa000d5c84cd1", size = 220766, upload-time = "2026-05-10T18:02:02.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/c7/c987babafd9207ffa1995e1ef1f9b26762cf4963aa768a66b6f0501e4616/coverage-7.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2bd259c442cd43c49b30fbafc51776eb19ea396faf159d26a83e6a0a5f13b0c", size = 221035, upload-time = "2026-05-10T18:02:04.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/e9/d6a5ac3b333088143d6fc877d398a9a674dc03124a2f776e131f03864823/coverage-7.14.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a706b908dfa85538863504c624b237a3cc34232bf403c057414ebfdb3b4d9f84", size = 262405, upload-time = "2026-05-10T18:02:05.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/b1/e70838d29a7c08e22d44398a46db90815bbcbf28de06992bd9210d1a8d8e/coverage-7.14.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7333cd944ee4393b9b3d3c1b598c936d4fc8d70573a4c7dacfec5590dd50e436", size = 264530, upload-time = "2026-05-10T18:02:07.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/73/5c31ef97763288d03d9995152b96d5475b527c63d91c84b01caea894b83a/coverage-7.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f162bc9a15b82d947b02651b0c7e1609d6f7a8735ca330cfadec8481dd97d5a", size = 266932, upload-time = "2026-05-10T18:02:09.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/76/dd56d80f29c5f05b4d76f7e7c6d47cafacae017189c75c5759d24f9ff0cc/coverage-7.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:362cb78e01a5dc82009d88004cf60f2e6b6d6fcbfdec05b05af73b0abf40118f", size = 268062, upload-time = "2026-05-10T18:02:11.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c7/27ba85cd5b95614f159ff93ebff1901584a8d192e2e5e24c4943a7453f59/coverage-7.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:acebd068fca5512c3a6fde9c045f901613478781a73f0e82b307b214daef23fb", size = 261504, upload-time = "2026-05-10T18:02:13.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/2e/e8149f60ab5d5684c6eee881bdf34b127115cddbb958b196768dd9d63473/coverage-7.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:29fe3da551dface75deb2ccbf87b6b66e2e7ef38f6d89050b428be94afff3490", size = 264398, upload-time = "2026-05-10T18:02:15.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/7f/1261b025285323225f4b4abffa5a643649dfd67e25ddca7ebcbdea3b7cb3/coverage-7.14.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b4cc4fce8672fffcb09b0eafc167b396b3ba53c4a7230f54b7aaffbf6c835fa9", size = 262000, upload-time = "2026-05-10T18:02:16.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/dc/829c54f60b9d08389439c00f813c752781c496fc5788c78d8006db4b4f2b/coverage-7.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5d4a51aad8ba8bdcd2b8bd8f03d4aca19693fa2327a3470e4718a25b03481020", size = 265732, upload-time = "2026-05-10T18:02:18.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/b0/70bd1419941652fa062689cba9c3eeafb8f5e6fbb890bce41c3bdda5dbd6/coverage-7.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9f323af3e1e4f68b60b7b247e37b8515563a61375518fa59de1af48ba28a3db6", size = 260847, upload-time = "2026-05-10T18:02:20.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/73/be40b2390656c654d35ea0015ea7ba3d945769cf80790ad5e0bb2d56d2ba/coverage-7.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1a0abc7342ea9711c469dd8b821c6c311e6bc6aac1442e5fbd6b27fae0a8f3db", size = 263166, upload-time = "2026-05-10T18:02:22.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/55/4a643f712fcf7cf2881f8ec1e0ccb7b164aff3108f69b51801246c8799f2/coverage-7.14.0-cp314-cp314t-win32.whl", hash = "sha256:a9f864ef57b7172e2db87a096642dd51e179e085ab6b2c371c29e885f65c8fb2", size = 223573, upload-time = "2026-05-10T18:02:24.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/96/3acae5da0953be042c0b4dea6d6789d2f080701c77b88e44d5bd41b9219b/coverage-7.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29943e552fdc08e082eb51400fb2f58e118a83b5542bd06531214e084399b644", size = 224680, upload-time = "2026-05-10T18:02:25.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/3d/6ab5d2dd8325d838737c6f8d83d62eb6230e0d70b87b51b57bbfd08fa767/coverage-7.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:742a73ea621953b012f2c4c2219b512180dd84489acf5b1596b0aafc55b9100b", size = 222703, upload-time = "2026-05-10T18:02:27.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/e8/cb8e80d6f9f55b99588625062822bf946cf03ed06315df4bd8397f5632a1/coverage-7.14.0-py3-none-any.whl", hash = "sha256:8de5b61163aee3d05c8a2beab6f47913df7981dad1baf82c414d99158c286ab1", size = 211764, upload-time = "2026-05-10T18:02:29.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/6f/8c2dc014357618b3226c90f731b8282766c3685786f422558991dc49fbf2/coverage-7.15.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1e3bb08ad574bd9fb6a991f645728f70d333c1c1958dd5fcde65e24cb862813d", size = 222571, upload-time = "2026-08-02T18:49:11.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/50/d867c7ceae9d56b7e74ee61ea834f1aa4f9a1e1c7f0ce39393ba573b1c12/coverage-7.15.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e5860eaff02a0b7f1b73304bdf846596ee62ab3a78d25c68044ebf684cb1fef", size = 222902, upload-time = "2026-08-02T18:49:13.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/77/4f6dfc490c5f2bcacb2d296d9aa4d1e128c43b48e94ad313fec7f49f09ad/coverage-7.15.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:60874e5bd67f0b1bdbe42ab42c7bafa66a6fb8de88721af6df3f7a02713960cd", size = 253947, upload-time = "2026-08-02T18:49:15.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/8a/6777f192af264165103e2a3d3768dbadb9894a0a2359a16877141d9ae8f5/coverage-7.15.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9147be876e9d83765e0b82176674dc248a6b9283e25e01e7462611b97e9b731", size = 256452, upload-time = "2026-08-02T18:49:17.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/7b/3d7ac46a0234bc684f41ee42be95e29b2b6525695adb04083609d5ac2149/coverage-7.15.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61a01f8c3804760fcc5a3d31c4f3cab792d660d44e17bf7adeaf0ea51e07821e", size = 257798, upload-time = "2026-08-02T18:49:19.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/1e/c6ee59c29afcb5fdb35f936381340d1a06429a07c48f20e809646647acbe/coverage-7.15.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95bf3e7f26f792e25eb185f85a5a659d48479265176dcfe22b6f334fd0081b5c", size = 260112, upload-time = "2026-08-02T18:49:21.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/e1/e8ea39a46e89e3a143312ee5f80336e992e3ae8fe44bf9c76b83fefeed42/coverage-7.15.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44c41eff9e413fed8740eca75d5438ebeb9d3e45e7cd37c67329213e7a72c764", size = 253944, upload-time = "2026-08-02T18:49:23.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/67/31ab5f6a37fd887d1386f81f0da9306851ad2264e9baaa9c7f606e0b3e17/coverage-7.15.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54146bafb61f3ba9895b43af0dd17eba01561d586d44ce84ea221b0cbbee5a9e", size = 255805, upload-time = "2026-08-02T18:49:25.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/6a/ee505a80c8fd89620fb337c0596daecff87f33171fbb4ee3015fc3d7331f/coverage-7.15.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:af000dd1bb859ff8066fda4c79512ff938c798116540307226b373099c7b151f", size = 253769, upload-time = "2026-08-02T18:49:27.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/41/6ab0f81c9e89660230d8f3f581d4732e5ddb75a885b0a5dfc73d315dc94f/coverage-7.15.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a1b82490577f3889950b5a04f18712aef0207243e0749d60fe28c3c73ebfd5fd", size = 258045, upload-time = "2026-08-02T18:49:30.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/62/c995e91cae28cf31d6defab3bfb553dda5ac83ac7381b0f2b121264c307a/coverage-7.15.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:c4fc90a60154c3e4b8a2dc206d6dbe852f1c235c249e0dc0cef909d032c9591a", size = 253587, upload-time = "2026-08-02T18:49:32.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/df/f2049980f82d6890321f2065f9e66216eabbf4b2001815db958bc543f40a/coverage-7.15.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f25bb884814a892948b4c20394db3f2364dd452d9492736479e7a493e63b0eb6", size = 255243, upload-time = "2026-08-02T18:49:34.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/82/2c841b67a978c0eb9c3707630b68f93f9e7585d78bb906bc8823ec6b07a5/coverage-7.15.3-cp314-cp314-win32.whl", hash = "sha256:722dbf8e7828fbcfe0dc8586167dc0a5ce85ad6ea171dbb21ed3f8d6581d3cb8", size = 224759, upload-time = "2026-08-02T18:49:36.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/78/5c93ec43784fd3e404ca23cd0584ae24bc1732de4a3fc194b68c3be88db0/coverage-7.15.3-cp314-cp314-win_amd64.whl", hash = "sha256:64d0845f9c3ed47302bed265c15ab4dbb64aa4ec1490839b8e328f4e7fa914d2", size = 225246, upload-time = "2026-08-02T18:49:38.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/77/813a054371f3b018cc63c6bdb46a3c35d5e95d4e3ed4f1449d4196106db5/coverage-7.15.3-cp314-cp314-win_arm64.whl", hash = "sha256:69bc14684f8fbbee9f9dbaa4fe79719b0da9725fc37956785c06ec365acf6926", size = 224673, upload-time = "2026-08-02T18:49:40.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/63/8c9f36cc71178d26db930baa03a4494abcc516d8d41bf820d0d85ef1d80b/coverage-7.15.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f92df943c24b96cb215ca26b4f6a2283e63c5db80f1635aceea7fff11311917b", size = 223298, upload-time = "2026-08-02T18:49:42.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/66/211f24d058ce9f56ebf1420d55b7574fdae924f6da3836f83c8bd4793e38/coverage-7.15.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:66591c46bdd2971d3ae2bc503a5f0459c2edcaf6b7e045b292000cc95bc6cb95", size = 223568, upload-time = "2026-08-02T18:49:44.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/bb/9c2ad5574a0d6420a96c6cade4f8a683931b9e79fe609f8924d7b6964616/coverage-7.15.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa64458b81b18bfc67cdf1f6dc02b23e3edc672f2f8e11771fad75865415a43", size = 264932, upload-time = "2026-08-02T18:49:47.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/91/938c39e77bdd5a0a440412f975609ce3702dabbda6ac715719d93ca45a7b/coverage-7.15.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:447f5421ccf5475956cf516d4ca1d575f487947b6f4e11f9d80c6aefe24b3dc8", size = 267052, upload-time = "2026-08-02T18:49:49.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/a3/7b431a98af35d9cc6394e54cde9435b33b8591672fbece6a4931267d7a8e/coverage-7.15.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0c77ef8cd483a4987a5d12d1d9d5f7ee598dfdc6c0844417d847e5768dc779", size = 269473, upload-time = "2026-08-02T18:49:51.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/58/dbc9951dce46be47a732823a1c571f62bcabdd54a68d8c281489a1a55cfb/coverage-7.15.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b273f4ff657446a06c2d85bf80e134fa869a92852ba5f87854a70e1fb44da77", size = 270591, upload-time = "2026-08-02T18:49:53.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/bd/1d610772c7c0889bfe477a59c46ee66ea53e271f3f06951e9d55b317f7c6/coverage-7.15.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daea8c4fafa22488600405be2c2be525a9406fba3fc0a83acc726db3e14e2005", size = 264007, upload-time = "2026-08-02T18:49:55.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/97/852eb3dcdba156b1a9078503f098499916bf889f964b61ad4a08223ac169/coverage-7.15.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93ff57c530f3fa7aa69f92fb9b8892b8aa82712aa970842f4abf28657f42fb57", size = 266926, upload-time = "2026-08-02T18:49:57.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/f8/b72cd238757fba2b587fc7dee047efe6e10b0c18343509faaaf502dd4680/coverage-7.15.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4df21bef8b800eebda9018f53d49c9ace3aeb0090c850139b27923aafcb83e91", size = 264529, upload-time = "2026-08-02T18:50:00.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/0a/6c52ec4b7fb007cb6433d1fcfda4080cb15d75ad37ef9c31025f3427293e/coverage-7.15.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:db567b02685f26034adcbd85055f80d12cdf02111b8ed00886093d98b2874ce2", size = 268263, upload-time = "2026-08-02T18:50:02.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/4e/f1f9aa3efd109a04353563a43fb5155340c1fdcdeaa6296ebed3b6f510ea/coverage-7.15.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5318dd51b8600b947e058cf5a4fe54d183d9d13c49b97b64ca7be05a34df9bef", size = 263377, upload-time = "2026-08-02T18:50:04.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/fb/6b268a0b2728ef1c379ad656b899274477a5f6bed1bf6765b4b387fb0601/coverage-7.15.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c995bfa383c54704839b6c4c2627a1c00895597ada0e5e8190c81d8bd620555c", size = 265688, upload-time = "2026-08-02T18:50:06.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/54/1a3ea96e5d5e7cd41dc432597bfc60692910e635d05e1cc25a8ccc243581/coverage-7.15.3-cp314-cp314t-win32.whl", hash = "sha256:6433fafb8da0e1d02eb53411e0ecdadb6b88f0224fdc23317e703c0e88937d42", size = 225066, upload-time = "2026-08-02T18:50:08.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/9d/a7b0d9afd18ed5274dd00651a78e7810a931c70d94b79996f150bec1a30f/coverage-7.15.3-cp314-cp314t-win_amd64.whl", hash = "sha256:fe578952b1b29fe8c777f43f241d49efac4b56724a3434f5d22ebe3c208df429", size = 225897, upload-time = "2026-08-02T18:50:10.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/11/34c5ae40b945e69aa72b87dc268135b7049905f3824af573b7073acbb946/coverage-7.15.3-cp314-cp314t-win_arm64.whl", hash = "sha256:d2e1acb7aee29dfa8f3e48c23f36670898baca1209d9bdd3985a50c7f982165e", size = 225212, upload-time = "2026-08-02T18:50:12.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/e7/7069b3d6c018917f49ba2e1c5fb910e498c7fefa3a1b78cb1b79e61ff45d/coverage-7.15.3-py3-none-any.whl", hash = "sha256:da78fa6fc7dafe4212839173133ee85afcf42c5cd5f3e47fa7c1c210453b445e", size = 214297, upload-time = "2026-08-02T18:50:14.709Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -121,16 +136,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "6.0.6"
|
||||
version = "6.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/29/ac41e16097af67066d97a7d5775c5d8e7efc5d0284f6b0a159e07b9adb92/django-6.0.6.tar.gz", hash = "sha256:ad03916ba59523d781ae5c3f631960c23d69a9d9c43cecda52fc23b47e953713", size = 10905525, upload-time = "2026-06-03T13:02:46.503Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/55/664f24ff81c9ea19cb7dfc851afeae1f3c2390c7aee01d4ded68b5c1580d/django-6.0.7.tar.gz", hash = "sha256:2998503fc083124fb58037084bfa00de323c7c743f05f1b4284e77bff0ab8890", size = 10921299, upload-time = "2026-07-07T13:51:26.485Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/50/23f9dc45483419a3cc2085b498b25adfbf10642b2941c73e6d2dfaffc9ab/django-6.0.6-py3-none-any.whl", hash = "sha256:25148b1194c47c2e685e5f5e9c5d59c78b075dfd282cb9618861ba6c1708f4d2", size = 8373354, upload-time = "2026-06-03T13:02:41.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/ec/1ce5334b6a2c52ce619c23a0be8d366a57a0e080ebb2d88266e5c849157c/django-6.0.7-py3-none-any.whl", hash = "sha256:a037427c2288443a8c02a1b02295a31c239663aa682bc50b1976afb7cf6a769e", size = 8373344, upload-time = "2026-07-07T13:51:20.007Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -147,14 +162,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-filter"
|
||||
version = "25.1"
|
||||
version = "26.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/40/c702a6fe8cccac9bf426b55724ebdf57d10a132bae80a17691d0cf0b9bac/django_filter-25.1.tar.gz", hash = "sha256:1ec9eef48fa8da1c0ac9b411744b16c3f4c31176c867886e4c48da369c407153", size = 143021, upload-time = "2025-02-14T16:30:53.238Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/3e/563965173d4cbb5fc308087e7b3d11a115b7b67273d093622480b1e31f78/django_filter-26.1.tar.gz", hash = "sha256:66ea04031b068c77c86e1ac26ced7a3f8f13ce797f5795751707e3deefc58054", size = 144299, upload-time = "2026-07-11T09:27:02.767Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/a6/70dcd68537c434ba7cb9277d403c5c829caf04f35baf5eb9458be251e382/django_filter-25.1-py3-none-any.whl", hash = "sha256:4fa48677cf5857b9b1347fed23e355ea792464e0fe07244d1fdfb8a806215b80", size = 94114, upload-time = "2025-02-14T16:30:50.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/01/afffed1e3c4540fb75bf550a18b6176a9f6371b5f3e52b69a28995b6480c/django_filter-26.1-py3-none-any.whl", hash = "sha256:7d98ef2899218e6242619b532cb1b95af14e09dfcf74844aecb550ad27b59ff2", size = 94069, upload-time = "2026-07-11T09:27:01.012Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -183,15 +198,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-stubs-ext"
|
||||
version = "5.2.2"
|
||||
version = "6.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/06/5e94715d103e6cc72380cb0d0b6682a7d5ad2c366cee478c94d77aad777d/django_stubs_ext-5.2.2.tar.gz", hash = "sha256:d9d151b919fe2438760f5bd938f03e1cb08c84d0651f9e5917f1313907e42683", size = 6244, upload-time = "2025-07-17T08:34:35.054Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/36/50/917f7224ea470e89cdcdc93d3dfe75b8391adf976cf12f2ecdb5f5d122be/django_stubs_ext-6.0.7.tar.gz", hash = "sha256:c3172c5126614fd2a44d0196b313b44c21f717cb09477ba52b447d41f4ce613e", size = 6665, upload-time = "2026-07-14T10:07:56.933Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/38/2903676f97f7902ee31984a06756b0e8836e897f4b617e1a03be4a43eb4f/django_stubs_ext-5.2.2-py3-none-any.whl", hash = "sha256:8833bbe32405a2a0ce168d3f75a87168f61bd16939caf0e8bf173bccbd8a44c5", size = 8816, upload-time = "2025-07-17T08:34:33.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/65/4d73fce956b5ebf26449259664360e539fbe95f0e86be396c28b636ba72a/django_stubs_ext-6.0.7-py3-none-any.whl", hash = "sha256:53a9c7c5a7c7e718cc6308cfce1e7470f2cac0b9d38dbcd60fbfa82704f1d592", size = 10362, upload-time = "2026-07-14T10:07:55.653Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -208,40 +223,40 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django-tasks"
|
||||
version = "0.9.0"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "django-stubs-ext" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/b1/064645bf246a1f5b46d9638755b1869ea44a6d05e7c7c12841fddebb71f6/django_tasks-0.9.0.tar.gz", hash = "sha256:971b3829efeee68147f7deced8d21b907131b11ec7953af83eb94b11f128a24d", size = 32343, upload-time = "2025-10-17T16:21:08.58Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/6e/34d4e77bb7951e5a5acbd846f43240f062c68bb04d9c246cb446a55d4cbc/django_tasks-0.12.0.tar.gz", hash = "sha256:58be66c1e487da32a3ce7320bd1949d0d1dc381b9004819f92591eb37fb2c1b8", size = 15445, upload-time = "2026-02-06T16:15:33.593Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e1/ce539ce1e21be71696649f84b6dbd508b2c0b89b559a6e620478bb126f7c/django_tasks-0.9.0-py3-none-any.whl", hash = "sha256:fddc344934a605d9eafa08ac8ba32c0cde9da23ef534e03a41f09fa0417b535a", size = 44057, upload-time = "2025-10-17T16:21:07.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e7/40c45768e84efd77e3ae6ffdcd2b5540011036cbb00c6df6ef2a2ad69551/django_tasks-0.12.0-py3-none-any.whl", hash = "sha256:ffcc1d7bdfad3bc5ef9c2d498596c6546484c9ec6b182fb9709c691eb66a8709", size = 15758, upload-time = "2026-02-06T16:15:32.168Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-treebeard"
|
||||
version = "5.1.0"
|
||||
version = "5.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/16/aa732ea1033586e4f946d8e47be9116ffb22b050485b179a7cafb82dfc34/django_treebeard-5.1.0.tar.gz", hash = "sha256:8ac2ba41307469a679c98188124933bc3baade36b02480343d1a301a1fca0700", size = 302339, upload-time = "2026-05-12T02:06:37.002Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/b4/d63da0e36e42fa0cf355d5692ee70066922ff9e8f54b30f69605d43cc9c5/django_treebeard-5.3.0.tar.gz", hash = "sha256:8606cefd3a65689e99d2a493e5af8a8ece450b96915c949b5c72f6f302cfbe19", size = 312630, upload-time = "2026-06-24T05:34:50.545Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/d3/311b9c43950238744f946540c48ea7068bf9e55bea976908a7be13b73082/django_treebeard-5.1.0-py3-none-any.whl", hash = "sha256:d0f68fcf1b49158e38d2322aa62a37bc0b89452d66a16f88e58dbe076d043c28", size = 78591, upload-time = "2026-05-12T02:06:35.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/5a/7b9fa8374e3da778109d1135c6d0131cc3ad27ba3d5436ef3a3ae3f55e7d/django_treebeard-5.3.0-py3-none-any.whl", hash = "sha256:ae9e570fa74b4c882bc08def1f0c97d27f163b6285a19f3383027d074882e9f4", size = 82023, upload-time = "2026-06-24T05:34:49.133Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "djangorestframework"
|
||||
version = "3.16.1"
|
||||
version = "3.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -285,17 +300,17 @@ dev = [
|
||||
{ name = "pytest", specifier = ">=9.1.1,<10" },
|
||||
{ name = "pytest-cov", specifier = ">=7.0.0,<8" },
|
||||
{ name = "pytest-django", specifier = ">=4.12.0,<5" },
|
||||
{ name = "ruff", specifier = ">=0.15.20,<0.16" },
|
||||
{ name = "ruff", specifier = ">=0.16.1,<0.17" },
|
||||
{ name = "wagtail-factories", specifier = ">=4.5.0,<5" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "draftjs-exporter"
|
||||
version = "5.1.0"
|
||||
version = "5.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/52/8b98525ab5477410bdbaf279c5fe0a99108211d40818bb769460784c1c41/draftjs_exporter-5.1.0.tar.gz", hash = "sha256:9f44b8dcecb702540e3aab24af2fad8683aec910fe0034c12cfab5d716ac5f84", size = 33500, upload-time = "2025-02-21T17:32:57.175Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/61/8b7fab482ef246f931b57f55f370e68377a53dcf5259f3fcf01e4889d4af/draftjs_exporter-5.2.0.tar.gz", hash = "sha256:4d665f8c75fd173d2c99326405300e8defcf4961b9b2f16ff117486489c6760b", size = 19674, upload-time = "2026-01-02T14:19:38.17Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/99/26d5524aaa3e89266e0af19332053aa9f8a61b1c39b29c0dc709f43fbb29/draftjs_exporter-5.1.0-py3-none-any.whl", hash = "sha256:c32932b7933b994fd5ea74c1decf47b5c41e13ba06363f5b69b76ef10137d4e9", size = 26302, upload-time = "2025-02-21T17:32:54.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/e1/2f81aa30ba22ceabb6779ed5dbd6b27fbf6f28deabad91140d1a32f3da5b/draftjs_exporter-5.2.0-py3-none-any.whl", hash = "sha256:f5255510a9c1de60807c1ba4be9666fb44ba263841b63329c8ce70d66146764c", size = 26251, upload-time = "2026-01-02T14:19:36.843Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -321,14 +336,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "40.18.0"
|
||||
version = "40.36.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/06/70886e82d8f1d2b73454f3a7c1b7405300128df22e70d85a828951366932/faker-40.18.0.tar.gz", hash = "sha256:2207575c0e8f90e6ccd6dbef764de875c614d16d3db4eee9712d9a00087f2e70", size = 1968243, upload-time = "2026-05-14T16:43:04.834Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/d2/026af1e002bbc6df534d1f8262b18ec79a974f928e9290bfbfdfe7c7b2af/faker-40.36.0.tar.gz", hash = "sha256:754048c76c03afa7de83eee8f4bcee3cf668cbb7d995f54a4e9678db7f110308", size = 2025903, upload-time = "2026-07-24T21:11:33.088Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/0b/5c0b2d3a4b7a715f1835dd3f963bfbe841a02ae5cad1df8ee0325dfad235/faker-40.18.0-py3-none-any.whl", hash = "sha256:61a6b94b74605ddb090a065deb197a1c585ae7a874c094cf6693671d271e6083", size = 2006355, upload-time = "2026-05-14T16:43:02.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/9a/b947ed175ce9a0dcb070ccf3607f0ce8720cfb5ed1a36166a150b2acd5af/faker-40.36.0-py3-none-any.whl", hash = "sha256:82b9497d9cfe017048075bcf969298a74b1b6e39f5e4dad1211085d1133f7b62", size = 2062829, upload-time = "2026-07-24T21:11:31.37Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -374,11 +389,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
version = "3.2.6"
|
||||
version = "3.2.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353, upload-time = "2025-01-26T16:36:27.374Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/90/f2aff026ab4aebd80eb71905106a0885f4cfde85dcf965543f45bed0d9ee/graphql_core-3.2.11.tar.gz", hash = "sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802", size = 528407, upload-time = "2026-06-05T13:45:22.915Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416, upload-time = "2025-01-26T16:36:24.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl", hash = "sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0", size = 214879, upload-time = "2026-06-05T13:45:21.245Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -407,11 +422,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
version = "3.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -462,59 +477,67 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.1"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788, upload-time = "2024-06-09T23:19:24.956Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985, upload-time = "2024-06-09T23:19:21.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.3.0"
|
||||
version = "12.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow-heif"
|
||||
version = "1.1.0"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/d4/597cf8c54d1ed494a46cc12e358d2f73a993b5f139402b35681f56beffde/pillow_heif-1.1.0.tar.gz", hash = "sha256:6c0c5f81a780185bbddc56e0d5537c53aa6cb5fb6018f5a60534a47c53f5455d", size = 18271020, upload-time = "2025-08-02T09:58:32.54Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/dc/6ab7a469ced899fbdbbc27b00067e4da02c21ff1ce7e9c6110f14e8fea6c/pillow_heif-1.5.0.tar.gz", hash = "sha256:16b11a37b762ff42da2d36527bb5cb14bd9194c24c389ee911155d6e23c53065", size = 17114105, upload-time = "2026-07-22T14:27:50.571Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/93/e32cd695bca9750f6fa166f28a4c84fd8a23209f99d56712359c0b42f2e6/pillow_heif-1.1.0-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:c47f12a30a86c43f714353d0fb8478dc4da7915d678642e0b6cf6f0fd53d711e", size = 3384304, upload-time = "2025-08-02T09:57:55.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/88/3e7cda3383c165a5eebf9a0193450de2b810b7654dbc363c8c26a87be6f2/pillow_heif-1.1.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3cc4a7a808b659f657c5144aeec8152d261d8f3c4a859eba66ef31dab26982d2", size = 2261529, upload-time = "2025-08-02T09:57:57.6Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/f2/469d9ef5c64efecbc0559aa56ebe6539007f478d8aee83253037dc203567/pillow_heif-1.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f3b0df4a03f9c7f150d801f3644961df5af0739503d6fc5d3e0aeb540d16c3d", size = 5780880, upload-time = "2025-08-02T09:57:59.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/74/a5c13f96ba06f4ec7d85a6d343b91c2767bd5a3fd90c146cf3c59fbcb12c/pillow_heif-1.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44fcd3a171337788bf672882484725f124143d2f290cfdbf029c22c113a15c79", size = 5502822, upload-time = "2025-08-02T09:58:00.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/04/1a6a88647dbb0eec95fa8b9d5cac501f690cdd83479e7cacf56772f1189f/pillow_heif-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f07f28e99ae74dffca936e24d8881f3e7e572233cf1f7bc16fe42fb08a795be", size = 6822779, upload-time = "2025-08-02T09:58:02.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/37/8873625e681505940381cff6a4a191301f98d9a5c1499f60a2e892ad8c89/pillow_heif-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7a6de39cfba4037479aa5deb65a6bb0b27da0a11b95f740202f03b578ee8932f", size = 6429866, upload-time = "2025-08-02T09:58:03.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/3c/63cbbe3a6a54e3ec925d2433bb431a4bf61695f34b5f1e689db642fb20c1/pillow_heif-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:514c856230995dc2f918fb12d83906885d42829e911868e23035e2d916c4c7c5", size = 5573890, upload-time = "2025-08-02T09:58:05.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/51/9b5a9352b86b7d82ad62969a84e3b4f998f6b5dc0320cec3e2d54d74d754/pillow_heif-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8d96783ca1648ba8384e66b888128ca3a48d769c0e3593acf84c40ce0c791629", size = 4743156, upload-time = "2026-07-22T14:27:15.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/10/dc89bec0edf37df5d7609d76ce125a2f8d1e901b2d34944d64d05f497bef/pillow_heif-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5c50626e77510f38cb11aacb3b4bba56eff17ce39a9d85b7ee210bd81387a083", size = 4264368, upload-time = "2026-07-22T14:27:17.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/54/837e9077784df4a0ba69d3e76e9f29a4e52929c84f0e1b0d29a13437a5cc/pillow_heif-1.5.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fb15135733626ee3fd4d7104bde1e54d4370f9f8b697a5d239b8b552b73fc7b", size = 6346432, upload-time = "2026-07-22T14:27:18.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/90/42f392c0513bf351ba2bca1350c457b0d7bd94f12d0e5fa74d7a9deae336/pillow_heif-1.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:646ddad98c8f9c5e637dbb84b6b7db905e565994e9fd6a496a36adff36ae28b3", size = 5602088, upload-time = "2026-07-22T14:27:20.112Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/4d/8070974a2e695f83fb10883b7d83c3eda635561f99993623bf111a55a984/pillow_heif-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9803c1291c7c598134f16016a071ec8d9dce268ab1806c8541450c4e3972893", size = 7374543, upload-time = "2026-07-22T14:27:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/78/9a5abfb163b0fb44a4f624146de142ba4887e5011f965442af2c997db6bc/pillow_heif-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1ad925299422d4592e4b5344c03e25b27dc7a5130447603a89404d7d138b746c", size = 6633558, upload-time = "2026-07-22T14:27:23.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/bc/7bae1c1efcec25dfdca950fe1a10cd723c544269db73203019e965394015/pillow_heif-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:cfd5f8a8a7b65f2ded217c5bde4dd9365b6b67187657a121b05224841a1b92c0", size = 6703549, upload-time = "2026-07-22T14:27:25.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/8d/fff074683f96eb004d7159228d2fc9e2e9fe8afd899d53aa695c622487d8/pillow_heif-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:6759febf63ea31fd00756e4a828cd643d0b9f98c93f5814b0ce5baa036da50e4", size = 4017234, upload-time = "2026-07-22T14:27:27.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/04/fa5727a62b2fc37b611c7a172fc10c6719530d27bc9126de8456fac69346/pillow_heif-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e2c3f47eac33d8368ea5779eb7bd8c736e7048b5897474d7e1799c073c9505e3", size = 4744030, upload-time = "2026-07-22T14:27:29.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/6f/afaccf55774c1084e5beba83f0ac201d315096f6a95d003696376184a7a3/pillow_heif-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d0881c82752d676fbe054cb0258100790ead94946fbb075ffa041bb9ae579b11", size = 4265061, upload-time = "2026-07-22T14:27:30.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a6/c90f9fffd5cb56e213753f22a9a955525d5ad99816481ae2392ab6c7ccd8/pillow_heif-1.5.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d5810606f70d3432b55987a5e44aa26fb7598fea565d8645b017f7ed935f5e99", size = 6351671, upload-time = "2026-07-22T14:27:32.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/b3/e15933817e71b9fcd8865e3b64171995ed26f5ca329bf4d03f3571213c52/pillow_heif-1.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e737d326410e733868566c045f651b5371086a4ec294c40d39e129481d024bef", size = 5606028, upload-time = "2026-07-22T14:27:34.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/55/2665f6c781b90bb5a25595c58ede1955125f0ad2c7d81bf3c153c9e9c9d8/pillow_heif-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522719e5dd56eae6f9f48828c08eefe0e5c02dd92f5a07780e6f3c04bdbcf880", size = 7379667, upload-time = "2026-07-22T14:27:35.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/77/0f0487b9f6cb1f5dff629b42ad08a7ae1997886048c4c2671e17079ad5d6/pillow_heif-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f1d281b2efee954ef78c0a473a41498d0548a32290d7250b4879ddd2480dffb8", size = 6637581, upload-time = "2026-07-22T14:27:37.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/b7/c65adbcab1b1c99c886c55b97b144b3092c432e7b0fedb91501c479cebe4/pillow_heif-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:65b3d619cea2d1f758cd039cddb331cb44d63768c355606ea9100cfee4421eb4", size = 6704174, upload-time = "2026-07-22T14:27:39.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/53/2a52f64f399717adae560068e9b0ae12ae3e43be2c64991e246af5aedc3f/pillow_heif-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:508fc9dd8fb4df933b666c30b60b0930270d9e072fcd90b75990166050e44656", size = 4017728, upload-time = "2026-07-22T14:27:41.102Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -619,7 +642,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.4"
|
||||
version = "2.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
@@ -627,34 +650,34 @@ dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.20"
|
||||
version = "0.16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -668,20 +691,20 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.7"
|
||||
version = "2.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.3"
|
||||
version = "0.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -704,29 +727,29 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
version = "4.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
version = "2026.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -808,15 +831,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "willow"
|
||||
version = "1.11.0"
|
||||
version = "1.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "defusedxml" },
|
||||
{ name = "filetype" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/bd/2a383be24c3e47423aa9b0aa5b4ca818ef193506b58800dd51e1b89d7bb3/willow-1.11.0.tar.gz", hash = "sha256:70292b2d0cd2d5bb4076f0b3d61308aeaa0b225f3970d00752f08a8fd386c3d1", size = 113827, upload-time = "2025-07-16T08:46:26.939Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/56/93e0bc60a3f9fe3b8ad9bac96c34179d1f4f37d42c8fddcf8de16ba5a711/willow-1.12.0.tar.gz", hash = "sha256:db29c8d2f19b9d91f4ce22881521055aa2f6070131d64b4098cb86a98e851a70", size = 113747, upload-time = "2025-10-26T13:22:22.345Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/05/b3f1b443c31ad871c48e19ea2be189681c2df4ccf594b1dd83d6775c032b/willow-1.11.0-py3-none-any.whl", hash = "sha256:0a4388dbf18726eef8f27449659047689c39b7023045ca5a8a75410d3864ee6f", size = 119459, upload-time = "2025-07-16T08:46:25.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/ef/a2e237f856b374e466fba458a6188a73fd68db7fb6b76706ce454980092f/willow-1.12.0-py3-none-any.whl", hash = "sha256:d3541690b6726a4f6f4654f80507d81bacfa0c686ab065eb5c90159f1b9cee38", size = 119425, upload-time = "2025-10-26T13:22:21.113Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
@@ -27,3 +27,14 @@ exclude = '/migrations/'
|
||||
id = "ruff-format"
|
||||
files = '^dnscms/.*\.py$'
|
||||
exclude = '/migrations/'
|
||||
|
||||
[[repos]]
|
||||
repo = "local"
|
||||
|
||||
[[repos.hooks]]
|
||||
id = "vitest"
|
||||
name = "vitest"
|
||||
entry = "npm --prefix web test"
|
||||
language = "system"
|
||||
files = '^web/(src/.*\.(ts|tsx)|package\.json|vitest\.config\.mts)$'
|
||||
pass_filenames = false
|
||||
|
||||
Generated
+1618
-476
File diff suppressed because it is too large
Load Diff
+19
-11
@@ -7,6 +7,8 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"codegen": "graphql-codegen",
|
||||
"perf:build": "next build",
|
||||
"perf:serve": "next start -p 3100",
|
||||
@@ -15,23 +17,23 @@
|
||||
"perf:lh": "wait-on http://localhost:3100/ && npm run perf:lh:desktop && npm run perf:lh:mobile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@graphql-codegen/cli": "^7.1.3",
|
||||
"@graphql-codegen/client-preset": "^6.0.1",
|
||||
"@graphql-codegen/cli": "^7.2.0",
|
||||
"@graphql-codegen/client-preset": "^6.1.0",
|
||||
"@parcel/watcher": "^2.5.6",
|
||||
"@sindresorhus/slugify": "^3.0.0",
|
||||
"@urql/next": "^2.0.1",
|
||||
"date-fns": "^4.4.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"graphql": "^16.14.2",
|
||||
"html-react-parser": "^6.1.3",
|
||||
"next": "^16.2.9",
|
||||
"nuqs": "^2.8.9",
|
||||
"graphql": "^17.0.2",
|
||||
"html-react-parser": "^6.1.4",
|
||||
"next": "^16.2.10",
|
||||
"nuqs": "^2.9.0",
|
||||
"react": "19.2.7",
|
||||
"react-dom": "19.2.7",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"sass": "^1.101.0",
|
||||
"sharp": "^0.35.1",
|
||||
"swiper": "^12.2.0",
|
||||
"sharp": "^0.35.3",
|
||||
"swiper": "^14.0.1",
|
||||
"urql": "^5.0.3",
|
||||
"use-debounce": "^10.1.1"
|
||||
},
|
||||
@@ -39,11 +41,12 @@
|
||||
"@types/node": "^24",
|
||||
"@types/react": "19.2.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"baseline-browser-mapping": "^2.10.38",
|
||||
"baseline-browser-mapping": "^2.10.42",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.9",
|
||||
"eslint-config-next": "16.2.10",
|
||||
"lighthouse": "^13.4.0",
|
||||
"typescript": "^6",
|
||||
"vitest": "^4.1.10",
|
||||
"wait-on": "^9.0.10"
|
||||
},
|
||||
"overrides": {
|
||||
@@ -53,5 +56,10 @@
|
||||
"browserslist": [
|
||||
"> 0.5% in NO",
|
||||
"not dead"
|
||||
]
|
||||
],
|
||||
"allowScripts": {
|
||||
"@parcel/watcher@2.5.6": true,
|
||||
"sharp@0.34.5": true,
|
||||
"unrs-resolver@1.7.2": true
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg width="385" height="56" viewBox="0 0 385 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_2021_703" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="385" height="56">
|
||||
<path d="M384.115 0H0V55.2045H384.115V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2021_703)">
|
||||
<path d="M17.8144 3.56014C18.4689 2.80011 19.2199 2.37034 20.0643 2.2693C20.9088 2.16525 21.708 2.31153 22.4651 2.70963C23.2251 3.10473 23.8102 3.70491 24.2204 4.51319C24.6335 5.32147 24.7391 6.27905 24.534 7.3844C22.3263 18.8617 20.6705 30.4416 19.5697 42.1225C19.4642 43.2248 19.1188 44.0512 18.5322 44.6016C17.9486 45.152 17.2685 45.4431 16.4934 45.4762C15.7168 45.5124 14.9583 45.3224 14.2194 44.9092C13.4789 44.4991 12.8833 43.8989 12.4339 43.1057C11.9875 42.3095 11.8156 41.3609 11.9212 40.2586C12.5757 33.539 13.4201 26.7681 14.4546 19.9429C13.6614 20.8402 12.8682 21.7375 12.072 22.6332C11.2788 23.5305 10.5052 24.4277 9.74968 25.3235C9.05752 26.1167 8.30805 26.554 7.49976 26.6384C6.69148 26.7229 5.92542 26.56 5.20159 26.1498C4.47775 25.7366 3.88964 25.1666 3.44025 24.4428C2.99389 23.719 2.77825 22.9197 2.79483 22.0421C2.81444 21.1614 3.1839 20.326 3.90471 19.5328C6.21646 16.8787 8.5267 14.2171 10.8354 11.5465C13.1427 8.87279 15.4695 6.21119 17.8144 3.56014Z" fill="#151E1D"/>
|
||||
<path d="M58.2907 8.57267C59.666 10.6763 60.5677 13.0212 60.999 15.6059C61.4333 18.1876 61.4695 20.8145 61.1076 23.4837C60.7457 26.1543 60.0369 28.7224 58.9844 31.1865C57.9348 33.6521 56.6168 35.7979 55.0334 37.6226C53.9628 38.8652 52.7383 39.9781 51.36 40.9583C49.9801 41.94 48.4978 42.6819 46.9144 43.1841C45.3295 43.6832 43.7114 43.8114 42.0587 43.5701C40.4376 43.362 38.904 42.8267 37.4563 41.9656C36.0086 41.1061 34.7419 40.0384 33.6562 38.7626C32.5704 37.4884 31.7516 36.0935 31.2011 34.5765C30.1305 31.8545 29.6645 29.012 29.8017 26.0473C29.942 23.0795 30.3898 20.2189 31.1469 17.4638C31.56 15.9799 32.0607 14.5066 32.6488 13.0424C33.2354 11.5796 33.9593 10.2104 34.8203 8.93459C35.7161 7.62415 36.7581 6.45244 37.9449 5.41796C39.1347 4.38498 40.4542 3.57519 41.9018 2.98708C43.7959 2.19538 45.8015 1.94656 47.9217 2.23911C50.0405 2.53317 52.0129 3.24042 53.8391 4.36236C55.6683 5.48129 57.1522 6.88372 58.2907 8.57267ZM47.5357 33.5932C48.914 32.42 50.0948 31.0146 51.0765 29.3769C52.0567 27.7407 52.7895 25.9839 53.2721 24.105C53.7546 22.2275 53.9356 20.341 53.815 18.447C53.6943 16.5499 53.2027 14.7916 52.3432 13.1751C51.9963 12.5206 51.5741 11.9083 51.0765 11.3413C50.5773 10.7713 49.9817 10.3823 49.291 10.1772C48.4661 9.9359 47.6322 9.97812 46.7877 10.3038C45.9433 10.6296 45.1742 11.0518 44.4835 11.5706C42.8308 12.7769 41.4525 14.294 40.3516 16.1187C39.2493 17.9448 38.4305 19.9173 37.8966 22.036C37.3613 24.1562 37.1275 26.2147 37.1969 28.2127C37.2331 29.7328 37.5528 31.0628 38.156 32.1999C38.7592 33.3339 39.5584 34.1949 40.5567 34.7815C41.558 35.3697 42.6589 35.5838 43.8622 35.427C45.0686 35.2701 46.2931 34.6594 47.5357 33.5932Z" fill="#151E1D"/>
|
||||
<path d="M73.848 36.6393C74.225 36.9862 74.5009 37.3993 74.6744 37.8819C74.8463 38.3645 74.9337 38.8651 74.9337 39.3839C74.9654 39.8664 74.8689 40.358 74.6442 40.8557C74.4225 41.3548 74.1571 41.777 73.848 42.1224C73.088 42.8794 72.1741 43.2564 71.1095 43.2564H70.4339C69.4356 43.2564 68.5233 42.8794 67.6954 42.1224C67.3169 41.777 67.0409 41.3548 66.869 40.8557C66.6956 40.358 66.593 39.8664 66.5614 39.3839C66.5252 38.9013 66.6172 38.4112 66.8388 37.9121C67.0635 37.4099 67.3485 36.9862 67.6954 36.6393C68.4554 35.8838 69.3677 35.5053 70.4339 35.5053H71.0552C72.0882 35.5053 73.0186 35.8838 73.848 36.6393Z" fill="#151E1D"/>
|
||||
<path d="M100.363 22.7901C101.365 22.7901 102.167 23.0586 102.77 23.5924C103.373 24.1232 103.768 24.7777 103.958 25.5528C104.147 26.3294 104.147 27.106 103.958 27.8811C103.768 28.6577 103.373 29.2956 102.77 29.7932C102.167 30.2924 101.365 30.5412 100.363 30.5412C97.6083 30.4733 94.8592 30.4055 92.1177 30.3361C89.3792 30.2683 86.6316 30.2004 83.8781 30.131C82.8768 30.131 82.082 29.8641 81.4954 29.3288C80.9118 28.795 80.5152 28.139 80.3071 27.3624C80.102 26.5873 80.102 25.8121 80.3071 25.0401C80.5152 24.265 80.9118 23.6271 81.4954 23.1279C82.082 22.6258 82.8768 22.3739 83.8781 22.3739C86.6316 22.4433 89.3792 22.5112 92.1177 22.579C94.8592 22.6484 97.6083 22.7178 100.363 22.7901Z" fill="#151E1D"/>
|
||||
<path d="M124.248 3.5602C124.902 2.80018 125.653 2.3704 126.498 2.26936C127.342 2.16531 128.142 2.31159 128.899 2.7097C129.659 3.10479 130.244 3.70497 130.654 4.51325C131.067 5.32154 131.173 6.27911 130.968 7.38447C128.76 18.8618 127.104 30.4416 126.003 42.1225C125.898 43.2249 125.552 44.0513 124.966 44.6017C124.382 45.1521 123.702 45.4431 122.927 45.4763C122.15 45.5125 121.392 45.3225 120.653 44.9093C119.913 44.4991 119.317 43.8989 118.868 43.1057C118.421 42.3095 118.249 41.361 118.355 40.2587C119.009 33.539 119.854 26.7682 120.888 19.943C120.095 20.8403 119.302 21.7375 118.506 22.6333C117.712 23.5305 116.939 24.4278 116.183 25.3235C115.491 26.1167 114.742 26.554 113.933 26.6385C113.125 26.7229 112.359 26.5601 111.635 26.1499C110.911 25.7367 110.323 25.1667 109.874 24.4429C109.427 23.719 109.212 22.9198 109.228 22.0421C109.248 21.1615 109.617 20.326 110.338 19.5328C112.65 16.8788 114.96 14.2172 117.269 11.5465C119.576 8.87285 121.903 6.21125 124.248 3.5602Z" fill="#151E1D"/>
|
||||
<mask id="mask1_2021_703" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="135" y="0" width="31" height="46">
|
||||
<path d="M135.889 0.236816H165.228V45.1674H135.889V0.236816Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2021_703)">
|
||||
<path d="M162.239 20.878C163.204 21.083 163.91 21.4796 164.356 22.0663C164.806 22.6498 165.032 23.33 165.032 24.1051C165.032 24.8817 164.851 25.6161 164.489 26.3067C164.127 26.9944 163.599 27.5463 162.909 27.9595C162.221 28.3697 161.413 28.5069 160.484 28.3697C160.414 30.4733 160.354 32.5769 160.303 34.6791C160.25 36.7782 160.224 38.8803 160.224 40.9825C160.188 41.9838 159.911 42.786 159.392 43.3892C158.876 43.9924 158.231 44.389 157.456 44.5775C156.684 44.766 155.918 44.766 155.158 44.5775C154.401 44.389 153.764 43.9924 153.245 43.3892C152.727 42.786 152.467 41.9838 152.467 40.9825C152.467 38.6753 152.493 36.3665 152.546 34.0578C152.597 31.7506 152.675 29.4403 152.781 27.1271C148.646 26.5767 144.442 26.2328 140.168 26.0956C139.408 26.0956 138.681 25.8543 137.991 25.3718C137.303 24.8892 136.796 24.2785 136.471 23.5381C136.145 22.7991 136.136 22.015 136.446 21.1856C137.511 18.4637 138.38 15.6814 139.052 12.8374C139.728 9.99479 140.186 7.10699 140.428 4.17546C140.495 3.17868 140.821 2.37793 141.405 1.77474C141.991 1.17154 142.673 0.776449 143.45 0.586442C144.225 0.397944 144.991 0.397944 145.748 0.586442C146.508 0.776449 147.119 1.17154 147.581 1.77474C148.047 2.37793 148.246 3.17868 148.179 4.17546C147.974 6.62444 147.655 9.05532 147.226 11.4681C146.794 13.8809 146.218 16.2575 145.494 18.5979C146.769 18.7034 148.043 18.8256 149.319 18.9658C150.593 19.1031 151.849 19.2403 153.089 19.376C153.193 17.9977 153.288 16.6013 153.372 15.1898C153.46 13.7753 153.556 12.3608 153.662 10.9494C153.73 9.98424 154.039 9.20008 154.591 8.59689C155.141 7.99369 155.812 7.58955 156.605 7.38446C157.397 7.17636 158.171 7.17636 158.928 7.38446C159.688 7.58955 160.309 7.98615 160.791 8.57276C161.274 9.16087 161.464 9.95257 161.364 10.9494C161.259 12.5704 161.162 14.1825 161.075 15.787C160.991 17.3885 160.896 18.999 160.791 20.6186C161.033 20.6518 161.274 20.694 161.515 20.7453C161.757 20.798 161.998 20.8418 162.239 20.878Z" fill="#151E1D"/>
|
||||
</g>
|
||||
<path d="M177.491 36.6393C177.868 36.9862 178.144 37.3993 178.317 37.8819C178.489 38.3645 178.576 38.8651 178.576 39.3839C178.608 39.8664 178.511 40.358 178.287 40.8557C178.065 41.3548 177.8 41.777 177.491 42.1224C176.731 42.8794 175.817 43.2564 174.752 43.2564H174.076C173.078 43.2564 172.166 42.8794 171.338 42.1224C170.959 41.777 170.683 41.3548 170.512 40.8557C170.338 40.358 170.236 39.8664 170.204 39.3839C170.168 38.9013 170.26 38.4112 170.481 37.9121C170.706 37.4099 170.991 36.9862 171.338 36.6393C172.098 35.8838 173.01 35.5053 174.076 35.5053H174.698C175.731 35.5053 176.661 35.8838 177.491 36.6393Z" fill="#151E1D"/>
|
||||
<path d="M223.701 21.9094C223.805 25.3205 223.891 28.7481 223.96 32.1939C224.028 35.6411 224.114 39.0869 224.22 42.5326C224.256 43.534 224.015 44.3362 223.496 44.9394C222.977 45.5426 222.338 45.9377 221.578 46.1277C220.821 46.3162 220.055 46.3162 219.279 46.1277C218.507 45.9377 217.853 45.5426 217.319 44.9394C216.784 44.3362 216.499 43.534 216.463 42.5326C216.429 41.4997 216.404 40.4758 216.384 39.4624C216.368 38.446 216.343 37.4206 216.312 36.3861L208.555 36.0724C208.28 37.3844 208.024 38.6843 207.783 39.9751C207.541 41.2659 207.3 42.5673 207.059 43.8778C206.885 44.8791 206.498 45.6029 205.895 46.0493C205.291 46.4987 204.593 46.7143 203.801 46.6947C203.008 46.6781 202.259 46.4715 201.552 46.0734C200.843 45.6783 200.291 45.1204 199.893 44.3965C199.498 43.6727 199.388 42.8101 199.561 41.8088C200.179 38.4309 200.877 35.0892 201.654 31.7837C202.429 28.4752 203.524 25.2511 204.935 22.1145C205.626 20.5673 206.505 18.8964 207.571 17.1019C208.641 15.3089 209.855 13.6637 211.215 12.1678C212.578 10.6688 214.086 9.57251 215.739 8.88034C217.29 8.22587 218.55 8.10523 219.515 8.51842C220.48 8.93312 221.237 9.67505 221.789 10.7442C222.339 11.8104 222.743 13.0424 223.001 14.4418C223.262 15.8382 223.435 17.2075 223.52 18.5496C223.607 19.8932 223.668 21.0136 223.701 21.9094ZM210.678 28.4239C211.607 28.4571 212.519 28.4902 213.416 28.5264C214.312 28.5596 215.208 28.5928 216.101 28.629C216.067 27.492 216.042 26.364 216.022 25.2451C216.006 24.1231 215.981 23.0117 215.95 21.9094C215.914 21.46 215.896 20.985 215.896 20.4858C215.896 19.9882 215.896 19.4785 215.896 18.9597C215.312 19.6519 214.813 20.3079 214.4 20.9262C213.707 21.9606 213.076 23.0374 212.506 24.1593C211.939 25.2782 211.432 26.4062 210.985 27.5432C210.949 27.6805 210.904 27.8267 210.853 27.9836C210.805 28.1374 210.746 28.2837 210.678 28.4239Z" fill="#151E1D"/>
|
||||
<path d="M257.042 15.1355C257.25 16.9979 257.268 19.0231 257.096 21.2097C256.923 23.3978 256.526 25.5874 255.908 27.7785C255.288 29.9666 254.418 31.9994 253.296 33.8768C252.177 35.7558 250.799 37.3422 249.158 38.636C247.521 39.9269 245.619 40.7623 243.452 41.1393C241.175 41.5178 239.236 41.3444 237.631 40.6205C236.03 39.8967 234.703 38.7778 233.65 37.2607C232.6 35.7452 231.765 34.0065 231.147 32.0431C230.527 30.0767 230.087 28.0258 229.826 25.8905C229.568 23.7522 229.431 21.7088 229.416 19.762C229.399 17.8167 229.441 16.1036 229.542 14.6228C229.61 13.6577 229.928 12.8735 230.495 12.2703C231.065 11.6671 231.739 11.263 232.516 11.0579C233.291 10.8498 234.057 10.8498 234.814 11.0579C235.574 11.263 236.196 11.6596 236.678 12.2462C237.161 12.8298 237.367 13.6215 237.299 14.6228C237.263 15.1385 237.227 16.0162 237.191 17.2588C237.158 18.4983 237.176 19.9008 237.245 21.4691C237.313 23.0374 237.459 24.6147 237.685 26.1981C237.91 27.783 238.237 29.196 238.669 30.4386C239.098 31.6782 239.676 32.5739 240.4 33.1288C241.124 33.6808 242.036 33.6959 243.138 33.1771C244.654 32.4533 245.86 31.3856 246.758 29.9741C247.653 28.5596 248.309 26.9732 248.724 25.2149C249.137 23.4581 249.378 21.6922 249.448 19.9189C249.516 18.1455 249.463 16.5515 249.291 15.1355C249.154 14.1704 249.309 13.3862 249.755 12.783C250.202 12.1798 250.794 11.7848 251.535 11.5947C252.278 11.4062 253.046 11.4168 253.839 11.6249C254.631 11.83 255.329 12.2266 255.932 12.8132C256.535 13.3968 256.905 14.1704 257.042 15.1355Z" fill="#151E1D"/>
|
||||
<path d="M283.254 22.9408C285.561 22.9092 287.335 23.4626 288.574 24.5996C289.817 25.7336 290.385 27.4919 290.281 29.8715C290.212 31.939 289.687 33.9115 288.707 35.7889C287.725 37.6679 286.391 39.1909 284.702 40.3611C282.839 41.6731 280.814 42.3622 278.628 42.4301C276.44 42.4995 274.294 42.2235 272.192 41.6037C269.642 40.8799 267.607 39.7519 266.087 38.2198C264.57 36.6846 263.53 34.8826 262.963 32.8151C262.396 30.7492 262.222 28.5792 262.444 26.3067C262.669 24.0311 263.237 21.7736 264.151 19.5328C265.063 17.2934 266.244 15.2259 267.692 13.3319C269.139 11.4349 270.801 9.8394 272.68 8.54857C274.558 7.25471 276.59 6.41778 278.778 6.03927C280.97 5.66227 283.219 5.88545 285.528 6.70882C286.493 7.01946 287.182 7.52766 287.597 8.2349C288.01 8.94366 288.208 9.70369 288.188 10.515C288.172 11.3233 287.973 12.0637 287.591 12.7347C287.213 13.4073 286.67 13.8974 285.963 14.2065C285.258 14.5172 284.442 14.4991 283.514 14.1523C281.856 13.6018 280.262 13.6124 278.73 14.1824C277.198 14.7494 275.802 15.707 274.544 17.0536C273.285 18.3972 272.259 19.9218 271.468 21.6259C270.675 23.3314 270.192 25.055 270.02 26.7952C269.847 28.537 270.088 30.0796 270.744 31.4218C271.121 32.215 271.733 32.886 272.578 33.4364C273.422 33.9884 274.369 34.3759 275.419 34.6006C276.471 34.8268 277.489 34.86 278.471 34.7031C279.451 34.5463 280.27 34.1422 280.926 33.4907C281.408 32.972 281.803 32.3507 282.114 31.6268C282.15 31.5228 282.176 31.3946 282.193 31.2408C282.208 31.084 282.235 30.9196 282.271 30.7462C282.235 30.7462 282.19 30.7462 282.138 30.7462C282.09 30.7462 282.048 30.7462 282.012 30.7462C281.874 30.7462 281.746 30.7462 281.626 30.7462C281.505 30.7462 281.375 30.7462 281.239 30.7462C280.651 31.3705 279.891 31.7595 278.959 31.9164C278.03 32.0702 277.126 31.9496 276.245 31.5545C275.367 31.1564 274.74 30.4235 274.363 29.3528C273.844 27.8689 273.826 26.6972 274.309 25.8362C274.791 24.9766 275.557 24.3493 276.607 23.9542C277.66 23.5561 278.798 23.2967 280.021 23.1761C281.243 23.0554 282.321 22.977 283.254 22.9408Z" fill="#151E1D"/>
|
||||
<path d="M323.104 15.1355C323.313 16.9979 323.331 19.0231 323.159 21.2097C322.985 23.3978 322.589 25.5874 321.97 27.7785C321.351 29.9666 320.481 31.9994 319.359 33.8768C318.24 35.7558 316.861 37.3422 315.221 38.636C313.583 39.9269 311.681 40.7623 309.514 41.1393C307.237 41.5178 305.298 41.3444 303.694 40.6205C302.092 39.8967 300.765 38.7778 299.713 37.2607C298.663 35.7452 297.828 34.0065 297.209 32.0431C296.59 30.0767 296.149 28.0258 295.888 25.8905C295.63 23.7522 295.493 21.7088 295.478 19.762C295.462 17.8167 295.504 16.1036 295.605 14.6228C295.673 13.6577 295.991 12.8735 296.558 12.2703C297.128 11.6671 297.802 11.263 298.579 11.0579C299.354 10.8498 300.12 10.8498 300.877 11.0579C301.637 11.263 302.258 11.6596 302.741 12.2462C303.223 12.8298 303.43 13.6215 303.362 14.6228C303.326 15.1385 303.29 16.0162 303.253 17.2588C303.22 18.4983 303.238 19.9008 303.308 21.4691C303.375 23.0374 303.522 24.6147 303.748 26.1981C303.973 27.783 304.3 29.196 304.731 30.4386C305.161 31.6782 305.738 32.5739 306.462 33.1288C307.186 33.6808 308.098 33.6959 309.201 33.1771C310.716 32.4533 311.923 31.3856 312.82 29.9741C313.716 28.5596 314.372 26.9732 314.786 25.2149C315.2 23.4581 315.441 21.6922 315.51 19.9189C315.578 18.1455 315.525 16.5515 315.353 15.1355C315.216 14.1704 315.372 13.3862 315.818 12.783C316.264 12.1798 316.857 11.7848 317.597 11.5947C318.341 11.4062 319.108 11.4168 319.902 11.6249C320.693 11.83 321.391 12.2266 321.995 12.8132C322.598 13.3968 322.967 14.1704 323.104 15.1355Z" fill="#151E1D"/>
|
||||
<path d="M345.13 22.1686C347.06 23.4761 348.583 25.1816 349.702 27.2838C350.824 29.3874 351.264 31.6268 351.023 34.0034C350.782 36.0708 350.073 37.8457 348.9 39.3296C347.73 40.8134 346.29 41.9701 344.581 42.798C342.876 43.6228 341.066 44.0797 339.152 44.1672C337.242 44.2517 335.408 43.932 333.651 43.2081C331.893 42.4843 330.412 41.3126 329.206 39.6915C328.586 38.8998 328.326 38.1337 328.427 37.3933C328.532 36.6499 328.851 36.003 329.387 35.451C329.92 34.9006 330.584 34.5146 331.377 34.2929C332.169 34.0682 332.97 34.0607 333.778 34.2688C334.589 34.4739 335.289 34.973 335.877 35.7647C336.15 36.1432 336.664 36.3588 337.421 36.4101C338.181 36.4629 338.938 36.4282 339.695 36.3076C340.455 36.1869 340.989 36.0572 341.3 35.9155C342.024 35.5747 342.566 35.068 342.928 34.3954C343.29 33.7199 343.42 32.987 343.32 32.1938C343.215 31.2287 342.87 30.437 342.283 29.8172C341.699 29.1989 341.001 28.656 340.19 28.1885C339.382 27.7226 338.563 27.2747 337.735 26.8434C336.905 26.4136 336.182 25.9416 335.563 25.4259C333.841 23.9104 332.79 22.2652 332.409 20.4918C332.03 18.7154 332.158 16.9389 332.795 15.1655C333.434 13.3891 334.425 11.7756 335.768 10.3279C337.115 8.88024 338.649 7.76131 340.371 6.96811C341.303 6.52174 342.165 6.40412 342.958 6.61222C343.75 6.81731 344.43 7.20335 344.997 7.77036C345.567 8.33736 345.956 9.00088 346.161 9.76091C346.367 10.5179 346.33 11.2493 346.053 11.9565C345.778 12.6653 345.193 13.2263 344.298 13.6395C343.714 13.9139 343.034 14.2909 342.259 14.7735C341.482 15.256 340.844 15.8336 340.347 16.5046C339.848 17.1772 339.753 17.9267 340.063 18.7546C340.235 19.3065 340.639 19.78 341.276 20.1781C341.915 20.5732 342.597 20.9261 343.32 21.2397C344.044 21.5504 344.647 21.8595 345.13 22.1686Z" fill="#151E1D"/>
|
||||
<path d="M377.229 8.98284C378.194 9.12459 378.968 9.48802 379.552 10.0746C380.138 10.6582 380.535 11.3459 380.74 12.1376C380.948 12.9308 380.948 13.7059 380.74 14.4659C380.535 15.2229 380.138 15.8171 379.552 16.2514C378.968 16.6826 378.194 16.8455 377.229 16.7399C375.644 16.5318 373.99 16.3765 372.265 16.2755C372.024 20.0319 371.79 23.7883 371.565 27.5432C371.344 31.2996 371.113 35.0741 370.872 38.8652C370.835 39.8303 370.531 40.6144 369.961 41.2176C369.394 41.8208 368.721 42.228 367.946 42.4361C367.174 42.6412 366.408 42.6412 365.648 42.4361C364.891 42.228 364.273 41.8299 363.79 41.2418C363.307 40.6552 363.081 39.8635 363.114 38.8652C363.356 35.0741 363.588 31.292 363.814 27.5191C364.039 23.7431 364.273 19.961 364.514 16.1729C362.961 16.2423 361.445 16.3449 359.966 16.4806C358.964 16.5861 358.162 16.4052 357.559 15.9377C356.956 15.4717 356.559 14.861 356.371 14.104C356.181 13.344 356.19 12.5779 356.395 11.8058C356.603 11.0307 357 10.3491 357.583 9.76097C358.17 9.17436 358.964 8.83054 359.966 8.7295C365.72 8.14289 371.475 8.22734 377.229 8.98284Z" fill="#151E1D"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.9463 3.91835C12.1758 4.04929 4.66515 5.07688 4.7608 14.767C4.85646 24.4571 -0.402086 49.2839 11.5555 51.3106C23.5131 53.3373 45.9213 52.267 48.8613 48.2136C51.8013 44.1601 53.4331 17.7524 51.4147 11.1855C48.8613 2.87994 33.773 3.45665 16.9469 3.91835H16.9463Z" stroke="#151E1D" stroke-width="5.69375" stroke-miterlimit="10"/>
|
||||
<path d="M29.441 16.464C23.628 15.585 13.3268 19.7905 16.674 29.8933C20.0207 39.9962 26.4666 41.5976 34.3994 37.4087C42.5303 33.1156 41.6139 18.3046 29.4415 16.464H29.441Z" stroke="#151E1D" stroke-width="5.69375" stroke-miterlimit="10"/>
|
||||
<path d="M42.4426 13.8809L42.4796 14.0073" stroke="#151E1D" stroke-width="5.69375" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 812 B |
@@ -0,0 +1,59 @@
|
||||
<svg width="169" height="141" viewBox="0 0 169 141" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2028_4438)">
|
||||
<path d="M137.304 89.3376C136.33 89.8793 135.251 90.2649 134.476 91.0634C134.349 91.2152 134.196 91.2906 134.034 91.3199C133.511 91.3767 133.086 91.2978 132.532 91.342C132.451 91.3446 132.384 91.3009 132.35 91.2311C132.071 90.6591 132.251 89.9811 132.612 89.5009C132.839 89.2076 133.159 88.9911 133.513 88.9359C134.26 88.8703 135.095 88.743 135.835 88.8281C136.051 88.8694 137.736 88.9869 137.304 89.3376Z" fill="#151E1D"/>
|
||||
<path d="M148.061 95.3948C146.994 95.7249 145.841 95.7796 144.917 96.4021C144.763 96.5163 144.625 96.5454 144.477 96.5043C143.989 96.3372 143.75 96.0458 143.069 95.924C142.966 95.9121 142.879 95.8437 142.846 95.7544C142.554 95.0071 143.069 94.1336 143.693 93.7856C144.451 93.3375 145.376 93.8893 146.143 94.036C146.414 94.1332 148.688 95.1041 148.064 95.3892L148.061 95.3948Z" fill="#151E1D"/>
|
||||
<path d="M149.963 107.471C149.191 107.186 148.292 106.663 147.489 106.899C147.357 106.931 147.28 106.933 147.208 106.818C146.994 106.444 146.745 105.967 146.449 105.567C146.389 105.488 146.362 105.383 146.391 105.291C146.615 104.319 147.796 103.662 148.579 104.32C149.43 105.138 150.121 106.114 150.196 107.316C150.207 107.439 150.062 107.527 149.958 107.468L149.963 107.471Z" fill="#151E1D"/>
|
||||
<path d="M29.2616 84.014C29.8515 82.8358 30.9772 82.1227 32.1923 81.6747C32.8009 81.4553 33.4483 81.8102 33.7917 82.3446C33.9937 82.5957 34.0845 83.005 34.0766 83.4255C34.0749 83.5317 34.0111 83.6174 33.9247 83.6644C33.5207 83.9178 33.1875 84.1419 32.8296 84.3856C32.6937 84.4719 32.5818 84.558 32.4413 84.5538C32.2274 84.4977 32.0433 84.2861 31.8148 84.2523C31.0244 84.1104 30.2616 84.1456 29.4404 84.246C29.3159 84.2722 29.2055 84.1365 29.2682 84.0223L29.2616 84.014Z" fill="#151E1D"/>
|
||||
<path d="M20.1027 91.0944C20.0448 89.9947 20.9954 89.1354 21.8924 88.6628C22.318 88.4434 22.7636 87.9984 23.29 88.1669C23.6782 88.273 24.0607 88.5585 24.3192 88.9181C24.4322 89.135 24.7416 89.4165 24.6096 89.7042C24.3299 90.2058 23.9387 90.7261 23.4794 90.9912C23.1583 91.2271 22.7994 90.8934 22.4461 90.8618C21.7341 90.7948 21.0119 90.7252 20.3788 91.1712C20.2882 91.2672 20.1208 91.2495 20.1046 91.1037L20.1027 91.0944Z" fill="#151E1D"/>
|
||||
<path d="M18.077 102.871C17.4011 101.877 17.2472 100.504 17.5637 99.33C17.7158 98.7252 17.7803 97.9559 18.368 97.5917C18.9718 97.1664 19.8107 97.0815 20.512 97.3721C20.6076 97.4146 20.6648 97.5036 20.6668 97.6043C20.6842 98.3516 20.6763 98.8635 20.501 99.4491C20.387 99.8918 19.8715 99.9809 19.6067 100.301C19.3096 100.629 19.0536 100.99 18.8487 101.39C18.6237 101.808 18.4685 102.283 18.3463 102.824C18.3254 102.953 18.14 102.988 18.0697 102.882L18.077 102.871Z" fill="#151E1D"/>
|
||||
<path d="M54.1769 114.127C66.5165 133.214 86.7624 142.855 100.422 114.245C100.422 114.245 103.074 114.246 104.826 113.17C107.036 111.82 108.273 110.448 109.006 110.52C109.738 110.593 112.391 134.753 114.091 141.077C114.934 144.216 106.229 145.603 106.229 145.603C106.229 145.603 111.296 155.811 112.113 164.53C112.662 170.417 110.575 178.119 104.94 182.89C97.4778 189.205 85.2841 173.972 79.597 173.435C70.5763 172.587 59.6078 178.595 53.1569 177.423C33.4424 173.837 27.4033 173.205 26.9797 167.022C26.6063 161.628 26.2413 151.716 35.1648 139.53C36.2606 138.033 29.5072 132.95 31.1694 130.324C33.4848 126.66 38.2022 122.332 54.1815 114.126L54.1769 114.127Z" fill="#ED7334" stroke="#ED7334" stroke-width="0.52448" stroke-miterlimit="10"/>
|
||||
<path d="M30.7847 116.45C30.7847 116.45 50.0121 119.918 50.7563 101.383C51.4543 83.9798 38.4248 82.0832 35.6333 82.2224C33.4042 82.3347 32.355 83.7779 33.8754 86.1106C35.1473 88.0623 27.4779 83.3157 23.2727 89.0951C21.4213 91.6455 29.3057 92.8811 29.3057 92.8811C29.3057 92.8811 22.8744 93.6121 20.4894 95.6393C19.072 96.8422 18.6457 99.3483 21.5057 101.207C23.8088 102.706 26.3735 98.4651 29.2575 101.262C31.6519 103.584 25.0202 106.591 30.7801 116.451L30.7847 116.45Z" fill="#F8F6EB" stroke="#F8F6EB" stroke-width="0.52448" stroke-miterlimit="10"/>
|
||||
<path d="M114.923 106.11C114.923 106.11 117.397 121.981 128.266 117.607C137.861 113.746 138.987 109.187 140.367 109.292C146.608 109.765 150.815 101.314 141.047 102.198C140.38 102.256 142.599 100.423 143.351 98.6425C144.264 96.4818 144.359 91.552 134.929 94.3911C132.914 94.9975 135.942 89.4079 129.567 91.0751C124.174 92.4833 118.796 101.022 114.928 106.114L114.923 106.11Z" fill="#F8F6EB" stroke="#F8F6EB" stroke-width="0.52448" stroke-miterlimit="10"/>
|
||||
<path d="M83.1092 130.583C83.1092 130.583 90.48 129.558 95.8483 121.229C98.9006 116.491 100.706 115.62 101.557 108.13C101.847 105.57 110.863 100.854 109 88.1676C107.601 78.6275 101.819 83.7276 101.819 83.7276C101.819 83.7276 96.036 78.3149 91.2497 79.475C86.7472 80.5659 95.6371 72.9594 103.731 75.5372C105.522 76.1091 94.8177 66.8403 83.1046 75.305C79.4938 77.9124 85.5806 65.3066 98.3313 60.9994C104.305 58.9832 77.4894 58.1408 68.5228 76.7865C67.1367 79.6724 67.3715 71.0116 73.537 64.4591C77.5035 60.2462 61.3851 71.8897 62.3629 81.7396C62.6079 84.2105 55.7112 82.2233 52.9271 92.1757C52.8593 92.4258 53.7905 90.7187 55.1247 89.6921C56.1939 88.8705 58.2483 88.8429 58.2483 88.8429C58.2483 88.8429 52.3904 93.9733 54.3635 105.742C54.542 106.797 53.7297 107.627 54.2512 108.711C55.1377 110.547 55.2898 110.606 56.1745 114.815C57.0592 119.024 56.6057 118.286 61.5522 124.048C64.8333 127.864 70.5523 127.316 73.9635 129.232C76.3212 130.556 78.7657 129.224 83.0963 130.591L83.1092 130.583Z" fill="#F8F6EB" stroke="#F8F6EB" stroke-width="0.52448" stroke-miterlimit="10"/>
|
||||
<path d="M106.707 85.6853C109.11 85.7385 111.365 79.2234 108.15 78.2146C103.986 76.9093 102.61 85.5933 106.707 85.6853Z" fill="#DED59F" stroke="#DED59F" stroke-width="0.52448" stroke-miterlimit="10"/>
|
||||
<path d="M86.0499 121.307C86.0499 121.307 90.2893 119.309 92.042 114.319" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M68.1541 103.792C68.1541 103.792 73.7362 114.18 92.9702 107.466C94.9838 106.763 94.9935 109.077 96.4945 110.08" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M59.1202 110.74C54.6316 111.919 53.7106 108.82 54.4148 106.857C54.9912 105.249 57.2848 105.397 57.2848 105.397L77.8246 87.8809" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M54.4907 106.231C52.4196 98.0499 56.2071 90.5581 58.1038 89.139C58.9604 88.4968 56.6993 88.64 55.1425 89.9588C53.0461 91.7328 52.0101 93.308 52.7531 91.4382C55.982 83.3273 62.1581 83.3287 62.1558 82.3328C62.1172 69.7149 75.7873 62.591 74.4067 63.6947C68.9011 68.0964 66.5425 80.2795 68.14 76.8871C76.7742 58.5616 102.642 59.3864 99.3446 60.4599C85.0488 65.1112 79.6377 77.5653 82.8202 75.3705C95.7878 66.4159 103.993 75.6565 103.993 75.6565C103.993 75.6565 95.8552 74.4307 90.6622 78.9688C89.857 79.6724 94.7358 77.4868 101.387 83.4102C102.086 84.0334 103.271 83.0527 104.308 80.9139C104.756 79.9967 106.151 77.4267 108.149 78.21C111.213 79.4143 108.648 84.5651 108.648 84.5651C108.648 84.5651 113.182 96.7001 101.914 107.146C100.39 108.554 104.997 110.454 93.5961 124.596" stroke="#151E1D" stroke-width="1.04896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28.2161 170.34C28.1411 169.754 27.4837 169.695 27.2756 168.728C25.0477 158.404 29.1192 147.18 35.3239 138.867" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M38.6129 143.633L30.4367 131.758C30.4367 131.758 31.1177 129.411 33.1566 127.889" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M55.1467 110.545C55.3047 111.296 56.872 117.287 56.8541 118.668" stroke="#151E1D" stroke-width="1.04896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M64.6702 127.293C62.2832 125.806 58.4334 121.095 56.8538 118.664C55.823 117.08 53.3904 112.744 53.3904 112.744L53.7752 113.636C53.7752 113.636 56.2965 120.249 48.8496 127.552C42.9153 133.372 31.3485 132.16 30.585 131.55" stroke="#151E1D" stroke-width="1.04896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M114.845 105.641C114.845 105.641 124.157 91.1149 131.072 90.576C134.548 90.3073 135.656 94.1323 132.9 97.3735" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M125.532 101.459C125.532 101.459 132.559 97.9986 134.109 100.47C136.818 104.795 130.018 109.2 130.018 109.2" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M134.474 94.5781C134.474 94.5781 142.153 91.1059 143.612 95.5982C145.072 100.091 138.073 103.757 138.073 103.757" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M140.514 102.045C140.514 102.045 145.924 101.172 146.996 104.37C147.654 106.33 145.786 108.735 141.014 109.343C137.442 109.8 139.679 113.114 128.074 118.176" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M30.7853 116.451C30.7853 116.451 27.6415 111.273 28.7754 106.592C29.571 103.301 29.7405 101.749 28.5146 100.909C26.8507 99.7715 24.86 100.835 23.6646 101.216C20.2937 102.286 18.3666 98.3394 19.9188 96.1744C21.9969 93.2826 25.2677 93.4086 27.7189 93.0474C30.3814 92.661 32.8862 96.173 32.8862 96.173C32.8862 96.173 31.5503 93.3443 25.2395 91.8549C21.5623 90.9864 23.7617 88.1221 24.5909 87.4183C26.484 85.8218 32.1718 85.8585 33.1775 86.2921C35.856 87.4475 36.2183 91.9876 35.9659 90.5581C35.2285 86.3857 31.609 84.7157 33.1712 83.1021C34.6263 81.5976 41.3721 80.8054 46.7264 86.4237C52.0444 92.0063 50.4737 104.914 50.4737 104.914" stroke="#151E1D" stroke-width="1.04896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M45.7924 96.2225C45.7924 96.2225 44.5207 89.0047 40.919 91.7017C37.0092 94.6271 40.9396 105.219 40.9396 105.219" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M56.0894 108.066C56.0894 108.066 57.7941 107.611 58.7352 108.905" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M104.8 84.5786C105.265 84.2017 105.958 83.4928 106.305 82.4186" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M106.474 85.3054C107.02 84.815 107.587 84.0316 107.776 83.2361" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M109.595 81.5354C109.595 81.5354 109.08 85.6147 106.707 85.685C105.45 85.7232 103.481 84.1682 104.448 80.6388" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||
<path d="M39.485 88.2243C40.4314 88.4442 41.2117 89.0648 41.8221 89.7933C42.1083 90.1471 42.6259 90.4571 42.5728 90.9833C42.5455 91.5859 42.1409 92.1571 41.6373 92.5325C41.5585 92.5924 41.4561 92.6091 41.37 92.5887C40.8225 92.4343 40.2306 92.1832 39.8665 91.8025C39.7322 91.667 39.6982 91.5057 39.7167 91.3189C39.8587 90.3456 39.612 89.401 39.3229 88.4606C39.2608 88.3485 39.3555 88.2034 39.486 88.2289L39.485 88.2243Z" fill="#151E1D"/>
|
||||
<path d="M103.821 183.911C103.821 183.911 118.711 174.827 108.746 151.761C105.557 144.379 107.158 141.005 104.184 130.794" stroke="#151E1D" stroke-width="1.04896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M106.925 145.963C110.705 145.169 115.269 142.125 114.197 141.489C108.352 138.024 104.846 125.081 108.963 112.904C111.113 106.551 106.022 114.803 100.416 114.243" stroke="#151E1D" stroke-width="1.04896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M114.357 141.725C114.357 141.725 114.327 137 113.571 134.506" stroke="#151E1D" stroke-width="1.04896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M136.734 99.0369C136.234 99.7486 135.513 100.348 135.293 101.179C135.261 101.325 135.194 101.397 135.072 101.408C134.607 101.419 134.259 101.3 133.626 101.38C133.532 101.39 133.44 101.361 133.37 101.304C133.086 101.055 132.905 100.718 132.867 100.355C132.79 99.8275 132.986 99.1798 133.52 99.0194C134.126 98.8777 134.809 98.6474 135.431 98.6515C135.671 98.6731 137.023 98.6008 136.735 99.0415L136.734 99.0369Z" fill="#151E1D"/>
|
||||
<path d="M96.4314 85.5777C96.4314 85.5777 100.799 84.8282 102.764 83.1636" stroke="#151E1D" stroke-width="1.04896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M94.2051 85.2422C94.2051 85.2422 97.8903 84.4772 102.21 83.5692" stroke="#151E1D" stroke-width="1.04896" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M33.701 131.815C33.701 131.815 51.0873 132.752 53.8314 117.297C54.0635 115.996 54.3576 112.633 51.5845 110.409C50.4783 109.52 51.5236 108.241 51.6394 106.961C51.7643 105.563 50.8653 104.423 49.3075 104.5C46.9408 104.622 48.6307 106.525 48.0521 107.436C47.0421 109.02 44.9353 108.134 44.9367 109.217C44.9404 109.876 44.6275 110.196 43.8398 110.227C42.9717 110.265 39.8886 107.895 38.5057 108.942C37.0227 110.062 37.9089 111.873 36.4545 112.511C34.5374 113.352 33.965 110.469 32.3488 111.757C30.7327 113.045 32.1741 114.292 30.5687 116.868C29.6728 118.308 29.4891 120.113 31.8006 124.354C33.9784 128.349 34.3825 130.454 33.6964 131.816L33.701 131.815Z" fill="#151E1D" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10"/>
|
||||
<path d="M109.881 110.775C110.379 110.232 112.641 105.651 113.812 105.404C115.471 105.056 114.709 107.728 115.425 108.315C116.539 109.221 116.96 108.131 118.362 108.299C120.185 108.517 117.691 110.645 118.485 111.426C119.28 112.207 121.063 111.231 121.462 112.807C121.746 113.932 119.758 114.961 120.951 116.125C122.144 117.29 124.07 114.796 125.134 115.804C126.198 116.813 124.176 117.522 125.355 118.117C126.26 118.572 127.798 116.616 128.626 117.236C130.433 118.594 126.39 121.62 125.761 122.383C125.132 123.146 125.825 125.32 124.224 125.97C122.623 126.619 121.512 126.877 121.556 128.576C121.63 131.242 120.602 131.183 119.528 131.683C118.541 132.141 117.758 131.873 116.884 133.943C116.359 135.18 115.448 137.899 111.949 138.634C111.166 138.799 107.812 129.737 107.764 123.095C107.711 116.044 109.443 111.252 109.877 110.776L109.881 110.775Z" fill="#151E1D" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10"/>
|
||||
<path d="M62.6275 124.586C62.3263 125.145 64.8845 127.881 67.2954 129.16C69.7063 130.439 82.6698 136.167 92.2947 127.958C95.2422 125.447 92.6972 124.056 91.7523 124.645C90.8073 125.233 91.8135 125.989 90.6936 126.822C89.8525 127.446 88.5666 126.137 87.8302 126.413C86.3399 126.971 86.6581 128.989 85.3527 129.465C81.9567 130.713 80.8581 127.777 78.1885 128.29C76.6062 128.594 76.2327 129.702 74.7648 129.154C73.7625 128.782 72.6546 122.825 68.7784 125.224C66.1631 126.842 65.5485 119.178 62.6275 124.586Z" fill="#151E1D" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10"/>
|
||||
<path d="M70.268 106.595C70.268 106.595 68.7552 110.139 72.0479 112.773C74.5071 114.739 78.6024 115.193 79.812 113.138C80.1637 112.544 78.2071 109.923 77.4619 109.767C72.7211 108.785 70.268 106.595 70.268 106.595Z" fill="#151E1D" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10"/>
|
||||
<path d="M78.8344 86.8941C78.8344 86.8941 76.7985 88.1548 77.8655 92.0172C78.3655 93.8231 80.5619 99.051 86.8622 98.338C95.2131 97.3961 96.6322 92.3083 96.1292 86.1371C95.7947 82.0726 82.6278 84.5371 78.8344 86.8941Z" fill="#151E1D" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10"/>
|
||||
<path d="M108.815 84.1969C108.681 82.6461 115.845 82.5122 116.194 83.7385C117.661 88.8645 115.415 93.1299 110.237 93.7562C109.575 93.8375 110.339 93.393 109.978 88.7707C109.883 87.5632 108.969 86.0275 108.815 84.1969Z" fill="#151E1D" stroke="#151E1D" stroke-width="1.04896" stroke-miterlimit="10"/>
|
||||
<path d="M33.4449 39.6636C36.2624 38.3917 34.64 35.0333 33.2446 33.4009C32.2298 32.2158 30.7676 30.6301 27.2857 30.4231C19.621 29.9691 17.648 34.8297 17.9885 39.1595C18.99 51.9752 30.7809 47.7022 32.0695 60.4579C32.4134 63.8596 29.5958 66.9075 27.7464 66.9075C25.897 66.9075 24.5083 65.0414 25.029 62.6578C25.7802 59.216 23.9708 55.5973 19.4908 55.604C16.1391 55.6107 15.0709 59.0157 15.0074 61.2057C14.9507 63.1252 15.0074 73.9346 23.3899 76.869C34.7969 80.8616 40.3986 69.8252 39.7643 60.0573C39.2569 52.239 33.258 48.1529 31.1315 47.0713C26.6147 44.7745 25.5064 43.4425 24.9189 41.72C24.3781 40.1343 25.4163 38.5118 26.5947 38.1914C28.6511 37.6339 30.6608 40.9221 33.4483 39.6636H33.4449Z" fill="#720328" stroke="#720328" stroke-width="0.371687" stroke-miterlimit="10"/>
|
||||
<path d="M35.7849 22.0611C37.6477 21.33 41.006 21.2766 44.9419 19.3537C50.8674 16.4594 51.7654 12.7171 55.2673 14.6033C57.6709 15.8952 59.3233 25.3426 55.9216 25.5763C53.4479 25.7466 48.8744 24.8018 50.0061 28.514C51.3014 32.7637 52.5165 39.1632 53.0206 42.8888C54.6397 54.9334 54.2792 59.2932 49.3251 60.1245C46.778 60.5518 44.0272 57.327 44.4879 54.3325C46.3206 42.4348 46.6411 41.2697 44.4478 29.7959C43.68 25.7799 40.512 31.0912 37.4908 30.3935C34.71 29.7525 30.3969 24.1809 35.7849 22.0611Z" fill="#720328" stroke="#720328" stroke-width="0.371687" stroke-miterlimit="10"/>
|
||||
<path d="M62.9724 12.3799C60.1782 13.6552 60.1548 17.6778 61.2364 20.719C62.7754 25.0488 64.1107 26.925 63.3062 35.9117C62.5017 44.9218 65.2224 49.5687 69.195 51.8421C74.0789 54.6329 81.3898 53.3176 85.0386 45.6028C92.6599 29.4921 84.5645 24.4112 85.3924 14.0925C85.6295 11.1414 84.9785 8.54422 82.2645 8.07018C79.7407 7.62953 77.6609 9.3087 77.247 11.8491C76.3824 17.1537 78.0649 18.8729 80.0545 24.0139C82.8119 31.1446 85.0386 42.0775 76.2121 43.5197C70.4902 44.4544 67.9264 37.1302 70.8842 28.3437C74.109 18.7628 70.4836 8.9515 62.9724 12.3833V12.3799Z" fill="#720328" stroke="#720328" stroke-width="0.371687" stroke-miterlimit="10"/>
|
||||
<path d="M102.334 51.6118C95.5106 53.6916 89.111 50.4868 90.8036 44.4411C95.8144 26.5211 91.8685 28.7077 90.6667 16.0488C90.0324 9.37889 92.1322 8.78801 94.7761 8.78467C98.8055 8.78467 105.379 15.1675 108.717 17.6112C113.194 20.886 118.351 25.2525 116.822 34.7801C115.874 40.6822 112.823 48.4137 102.337 51.6118H102.334ZM96.3051 20.886C97.2098 24.7284 100.461 30.5137 98.6119 37.1269C97.2064 42.1544 103.249 41.5702 107.318 35.6013C111.852 28.9514 107.011 23.0927 102.588 20.4955C99.2361 18.5292 95.3904 16.9836 96.3084 20.8827L96.3051 20.886Z" fill="#720328"/>
|
||||
<path d="M98.2181 52.4125C95.6943 52.4125 93.4476 51.6648 91.9921 50.2426C90.4799 48.7638 89.9992 46.6873 90.6401 44.3939C94.0452 32.2124 93.3141 29.4149 92.1056 24.7847C91.5314 22.5881 90.8805 20.101 90.4966 16.065C90.1861 12.7835 90.4966 10.7471 91.488 9.66216C92.2726 8.79754 93.3708 8.62061 94.7763 8.62061C97.9377 8.62061 102.638 12.4797 106.07 15.2939C107.131 16.1652 108.049 16.9196 108.814 17.4771C113.558 20.949 118.492 25.4357 116.983 34.8063C115.577 43.5393 110.667 49.2445 102.381 51.7716C100.965 52.2022 99.5534 52.4125 98.2181 52.4125ZM94.7763 8.9511C93.461 8.9511 92.4395 9.11134 91.7351 9.88249C90.8271 10.8806 90.5333 12.8903 90.8304 16.0317C91.211 20.0443 91.8586 22.518 92.4294 24.7012C93.6546 29.3882 94.3924 32.2191 90.9639 44.4874C90.3497 46.6873 90.787 48.5969 92.2291 50.0056C94.369 52.0954 98.3182 52.6629 102.287 51.4544C110.44 48.9674 115.273 43.349 116.656 34.7562C118.131 25.5725 113.494 21.3195 108.617 17.7475C107.842 17.1834 106.924 16.4256 105.856 15.5509C102.464 12.7668 97.8175 8.9511 94.7763 8.9511ZM100.391 40.6917C99.9974 40.6917 99.6402 40.6216 99.3397 40.4814C98.3049 40.0007 97.9811 38.7622 98.4484 37.083C99.8438 32.0956 98.3182 27.6122 97.0931 24.0069C96.7158 22.8986 96.3586 21.8503 96.1416 20.9256C95.8646 19.7572 95.9948 18.966 96.5222 18.5754C97.4369 17.9011 99.5067 18.4987 102.668 20.3548C106.01 22.3177 108.413 25.2955 109.091 28.3233C109.648 30.807 109.098 33.2874 107.452 35.701C105.182 39.0293 102.314 40.6951 100.391 40.6951V40.6917ZM97.4937 18.6389C97.1665 18.6389 96.9095 18.7056 96.7225 18.8425C96.3119 19.1463 96.2251 19.8206 96.4655 20.8455C96.6791 21.7535 97.033 22.7951 97.4069 23.8967C98.6487 27.5455 100.191 32.0856 98.7689 37.1698C98.3416 38.6954 98.5953 39.7637 99.48 40.1743C101.136 40.9454 104.624 39.2529 107.178 35.504C108.767 33.1705 109.301 30.777 108.767 28.3901C108.106 25.4524 105.766 22.5514 102.501 20.6352C100.258 19.3165 98.5319 18.6322 97.4937 18.6322V18.6389Z" fill="#720328"/>
|
||||
<path d="M124.908 28.3435C128.08 28.3435 130.55 25.0586 128.941 22.6283C127.332 20.198 123.553 20.4116 121.56 22.6283C119.567 24.8449 121.733 28.3435 124.905 28.3435H124.908Z" fill="#720328" stroke="#720328" stroke-width="0.371687" stroke-miterlimit="10"/>
|
||||
<path d="M123.863 31.5546C122.681 31.9552 120.875 33.4675 120.795 36.6122C120.431 50.4528 116.609 51.067 116.609 55.5304C116.609 61.8431 127.899 62.0334 127.131 53.0033C126.176 41.7465 129.354 41.4727 129.922 37.3867C130.489 33.3006 128.59 29.9489 123.866 31.5513L123.863 31.5546Z" fill="#720328" stroke="#720328" stroke-width="0.371687" stroke-miterlimit="10"/>
|
||||
<path d="M131.217 63.2359C130.182 50.1965 135.033 39.347 140.141 36.7364C145.248 34.1258 151.634 33.5617 153.921 41.0628C155.697 46.8882 151.998 48.3671 156.862 58.2017C161.165 66.9047 155.76 79.0228 145.996 78.8258C140.471 78.7156 132.062 73.8083 131.221 63.2359H131.217ZM149.181 42.3281C141.86 33.802 137.383 48.9613 138.588 55.8415C139.243 59.5671 141.012 62.842 143.96 63.957C147.902 65.4459 153.26 64.895 150.816 56.3256C148.837 49.3886 153.06 46.8415 149.181 42.3281Z" fill="#720328" stroke="#720328" stroke-width="0.371687" stroke-miterlimit="10"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2028_4438">
|
||||
<rect width="169" height="141" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="30" height="25" viewBox="0 0 30 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.50049 19.2085C3.45546 20.0651 8.99546 22.066 15.5157 23.2177M7.70036 5.64159C12.1164 8.59943 16.6618 12.0402 21.5955 16.9299M23.1552 1.50049L27.8393 13.1863" stroke="#FF6915" stroke-width="3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 332 B |
@@ -8,8 +8,6 @@ import {
|
||||
} from "@/components/general/GenericPageView";
|
||||
import { getSeoMetadata } from "@/lib/seo";
|
||||
|
||||
export const dynamicParams = false;
|
||||
|
||||
function getWagtailUrlPath(url: string[]): string {
|
||||
// for the page /foo/bar we need to look for `/home/foo/bar/`
|
||||
return `/home/${url.join("/")}/`;
|
||||
|
||||
@@ -1,36 +1,14 @@
|
||||
import { Metadata, ResolvingMetadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getClient } from "@/app/client";
|
||||
import {
|
||||
NewsPageView,
|
||||
loadNewsPageProps,
|
||||
} from "@/components/news/NewsPageView";
|
||||
import { graphql } from "@/gql";
|
||||
import { getSeoMetadata } from "@/lib/seo";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allNewsSlugsQuery = graphql(`
|
||||
query allNewsSlugs {
|
||||
pages(contentType: "news.NewsPage") {
|
||||
id
|
||||
slug
|
||||
}
|
||||
}
|
||||
`);
|
||||
const { data, error } = await getClient().query(allNewsSlugsQuery, {});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
if (!data?.pages) {
|
||||
throw new Error(
|
||||
"Failed to generate static params for subpages of /aktuelt"
|
||||
);
|
||||
}
|
||||
|
||||
return data.pages.map((page: any) => ({
|
||||
slug: page.slug,
|
||||
}));
|
||||
// Prerender nothing at build time; render and cache on first request
|
||||
return [];
|
||||
}
|
||||
|
||||
type Params = Promise<{ slug: string }>;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("next/cache", () => ({ revalidateTag: vi.fn() }));
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { CMS_CACHE_TAG } from "@/lib/revalidation";
|
||||
import { POST } from "./route.ts";
|
||||
|
||||
const request = (headers: Record<string, string> = {}) =>
|
||||
new Request("http://localhost/api/revalidate", {
|
||||
method: "POST",
|
||||
headers,
|
||||
}) as unknown as NextRequest;
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("POST /api/revalidate", () => {
|
||||
it("returns 503 when no secret is configured", async () => {
|
||||
vi.stubEnv("REVALIDATE_WEBHOOK_SECRET", undefined);
|
||||
|
||||
const res = await POST(request({ "x-revalidate-secret": "hemmelig" }));
|
||||
|
||||
expect(res.status).toBe(503);
|
||||
expect(revalidateTag).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 401 on missing or wrong secret", async () => {
|
||||
vi.stubEnv("REVALIDATE_WEBHOOK_SECRET", "hemmelig");
|
||||
|
||||
expect((await POST(request())).status).toBe(401);
|
||||
expect(
|
||||
(await POST(request({ "x-revalidate-secret": "feil" }))).status
|
||||
).toBe(401);
|
||||
expect(revalidateTag).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("purges the cms tag on the correct secret", async () => {
|
||||
vi.stubEnv("REVALIDATE_WEBHOOK_SECRET", "hemmelig");
|
||||
|
||||
const res = await POST(request({ "x-revalidate-secret": "hemmelig" }));
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ revalidated: true, tag: CMS_CACHE_TAG });
|
||||
expect(revalidateTag).toHaveBeenCalledWith(CMS_CACHE_TAG, { expire: 0 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { timingSafeEqual } from "node:crypto";
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { CMS_CACHE_TAG } from "@/lib/revalidation";
|
||||
|
||||
function secretMatches(provided: string, expected: string): boolean {
|
||||
const a = Buffer.from(provided);
|
||||
const b = Buffer.from(expected);
|
||||
return a.length === b.length && timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const secret = process.env.REVALIDATE_WEBHOOK_SECRET;
|
||||
if (!secret) {
|
||||
return NextResponse.json(
|
||||
{ error: "revalidation is not configured" },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
const provided = req.headers.get("x-revalidate-secret") ?? "";
|
||||
if (!secretMatches(provided, secret)) {
|
||||
return NextResponse.json({ error: "invalid secret" }, { status: 401 });
|
||||
}
|
||||
// expire: 0 purges immediately; the next request renders fresh
|
||||
revalidateTag(CMS_CACHE_TAG, { expire: 0 });
|
||||
return NextResponse.json({ revalidated: true, tag: CMS_CACHE_TAG });
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getClient } from "@/app/client";
|
||||
import { cachedUntilOsloMidnight } from "@/lib/revalidation";
|
||||
import {
|
||||
eventsOverviewQuery,
|
||||
getSingularEvents,
|
||||
@@ -45,7 +46,11 @@ export async function GET(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const { data, error } = await getClient().query(eventsOverviewQuery, {});
|
||||
const { data, error } = await getClient().query(
|
||||
eventsOverviewQuery,
|
||||
{},
|
||||
cachedUntilOsloMidnight()
|
||||
);
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,14 @@
|
||||
import { Metadata, ResolvingMetadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getClient } from "@/app/client";
|
||||
import {
|
||||
EventPageView,
|
||||
loadEventPageProps,
|
||||
} from "@/components/events/EventPageView";
|
||||
import { graphql } from "@/gql";
|
||||
import { getSeoMetadata } from "@/lib/seo";
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const allEventSlugsQuery = graphql(`
|
||||
query allEventSlugs {
|
||||
pages(contentType: "events.EventPage") {
|
||||
id
|
||||
slug
|
||||
}
|
||||
}
|
||||
`);
|
||||
const { data, error } = await getClient().query(allEventSlugsQuery, {});
|
||||
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
if (!data?.pages) {
|
||||
throw new Error(
|
||||
"Failed to generate static params for subpages of /arrangementer"
|
||||
);
|
||||
}
|
||||
|
||||
return data.pages.map((page: any) => ({
|
||||
slug: page.slug,
|
||||
}));
|
||||
// Prerender nothing at build time; render and cache on first request
|
||||
return [];
|
||||
}
|
||||
|
||||
type Params = Promise<{ slug: string }>;
|
||||
|
||||
@@ -3,6 +3,8 @@ import "server-only";
|
||||
import { cacheExchange, createClient, fetchExchange } from "@urql/core";
|
||||
import { registerUrql } from "@urql/next/rsc";
|
||||
|
||||
import { CMS_CACHE_TAG } from "@/lib/revalidation";
|
||||
|
||||
const wagtailBaseUrl = process.env.WAGTAIL_BASE_URL;
|
||||
if (!wagtailBaseUrl) {
|
||||
throw new Error("WAGTAIL_BASE_URL is not set");
|
||||
@@ -13,8 +15,9 @@ const makeClient = () => {
|
||||
return createClient({
|
||||
url: graphqlEndpoint,
|
||||
exchanges: [cacheExchange, fetchExchange],
|
||||
// requestPolicy: "network-only",
|
||||
fetchOptions: { next: { revalidate: 0 } },
|
||||
// Cache all queries in the Data Cache until revalidateTag(CMS_CACHE_TAG);
|
||||
// override per operation with the contexts in @/lib/revalidation
|
||||
fetchOptions: { cache: "force-cache", next: { tags: [CMS_CACHE_TAG] } },
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getClient } from "@/app/client";
|
||||
import { uncached } from "@/lib/revalidation";
|
||||
import { PreviewBanner } from "@/components/general/PreviewBanner";
|
||||
import {
|
||||
AssociationIndexView,
|
||||
@@ -149,7 +150,11 @@ export default async function PreviewRender() {
|
||||
return <ExpiredPreview />;
|
||||
}
|
||||
|
||||
const { data, error } = await getClient().query(previewPageQuery, { token });
|
||||
const { data, error } = await getClient().query(
|
||||
previewPageQuery,
|
||||
{ token },
|
||||
uncached
|
||||
);
|
||||
if (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getClient } from "@/app/client";
|
||||
import { uncached } from "@/lib/revalidation";
|
||||
import {
|
||||
type SearchResult,
|
||||
SearchResults,
|
||||
@@ -65,7 +66,7 @@ export default async function Page({
|
||||
}
|
||||
`);
|
||||
|
||||
const { data } = await getClient().query(searchQuery, { query });
|
||||
const { data } = await getClient().query(searchQuery, { query }, uncached);
|
||||
const all = (data?.results ?? []) as SearchResult[];
|
||||
totalCount = all.length;
|
||||
results = all.slice(0, RESULT_LIMIT);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AssociationFragment } from "@/gql/graphql";
|
||||
import { Image } from "@/components/general/Image";
|
||||
import styles from './associationHeader.module.scss';
|
||||
import { Breadcrumb } from "../general/Breadcrumb";
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import { Icon } from "../general/Icon";
|
||||
|
||||
export const AssociationHeader = ({
|
||||
@@ -15,10 +16,9 @@ export const AssociationHeader = ({
|
||||
<Breadcrumb link="/foreninger" text={association.associationType ? association.associationType : "Foreninger"} />
|
||||
<h1>{association.title}</h1>
|
||||
{association.lead && (
|
||||
<div
|
||||
className="lead"
|
||||
dangerouslySetInnerHTML={{ __html: association.lead }}
|
||||
/>
|
||||
<div className="lead">
|
||||
<RichText content={association.lead} />
|
||||
</div>
|
||||
)}
|
||||
{association.websiteUrl && (
|
||||
<a className="button" href={association.websiteUrl} target="_blank">
|
||||
|
||||
@@ -1,84 +1,168 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { RichTextBlock } from "./RichTextBlock";
|
||||
import { ImageWithTextBlock } from "./ImageWithTextBlock";
|
||||
import { HorizontalRuleBlock } from "./HorizontalRuleBlock";
|
||||
|
||||
const ImageSliderBlock = dynamic(() =>
|
||||
import("./ImageSliderBlock").then((m) => m.ImageSliderBlock)
|
||||
);
|
||||
import { FeaturedBlock } from "./FeaturedBlock";
|
||||
import type {
|
||||
AccordionBlockFragment,
|
||||
ContactEntityBlockFragment,
|
||||
ContactListBlockFragment,
|
||||
ContactSectionBlockFragment,
|
||||
ContactSubsectionBlockFragment,
|
||||
EmbedBlockFragment,
|
||||
FactBoxBlockFragment,
|
||||
FeaturedBlockFragment,
|
||||
HorizontalRuleBlockFragment,
|
||||
ImageSliderBlockFragment,
|
||||
ImageWithTextBlockFragment,
|
||||
PageSectionBlockFragment,
|
||||
RichTextBlockFragment,
|
||||
ScheduleBlockFragment,
|
||||
} from "@/gql/graphql";
|
||||
import { AccordionBlock } from "./AccordionBlock";
|
||||
import { ContactEntityBlock } from "./ContactEntityBlock";
|
||||
import { ContactListBlock } from "./ContactListBlock";
|
||||
import { ContactSectionBlock, ContactSubsectionBlock } from "./ContactSection";
|
||||
import { EmbedBlock } from "./EmbedBlock";
|
||||
import { FactBoxBlock } from "./FactBoxBlock";
|
||||
import { ScheduleBlock } from "./ScheduleBlock";
|
||||
import { PageSectionBlock, PageSectionNavigationBlock } from "./PageSection";
|
||||
import { ContactSectionBlock, ContactSubsectionBlock } from "./ContactSection";
|
||||
import { ContactListBlock } from "./ContactListBlock";
|
||||
import { ContactEntityBlock } from "./ContactEntityBlock";
|
||||
import { FeaturedBlock } from "./FeaturedBlock";
|
||||
import { HorizontalRuleBlock } from "./HorizontalRuleBlock";
|
||||
import { ImageWithTextBlock } from "./ImageWithTextBlock";
|
||||
import { NeufAddressSectionBlock } from "./NeufAddressSectionBlock";
|
||||
import { OpeningHoursSectionBlock } from "./OpeningHoursSectionBlock";
|
||||
import { PageSectionBlock, PageSectionNavigationBlock } from "./PageSection";
|
||||
import { RichTextBlock } from "./RichTextBlock";
|
||||
import { ScheduleBlock } from "./ScheduleBlock";
|
||||
|
||||
export const Blocks = ({ blocks, pageContent }: { blocks: any, pageContent?: boolean }) => {
|
||||
const sections = blocks.filter(
|
||||
(block: any) => block?.__typename === "PageSectionBlock"
|
||||
);
|
||||
const ImageSliderBlock = dynamic(() =>
|
||||
import("./ImageSliderBlock").then((m) => m.ImageSliderBlock),
|
||||
);
|
||||
|
||||
return blocks.map((block: any) => {
|
||||
switch (block?.blockType) {
|
||||
export type StreamFieldBlock = {
|
||||
id?: string | null;
|
||||
blockType?: string;
|
||||
__typename?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export const Blocks = ({
|
||||
blocks,
|
||||
pageContent,
|
||||
}: {
|
||||
blocks?: Array<StreamFieldBlock | null> | null;
|
||||
pageContent?: boolean;
|
||||
}) => {
|
||||
const sections = (blocks ?? []).filter(
|
||||
(block) => block?.__typename === "PageSectionBlock",
|
||||
) as PageSectionBlockFragment[];
|
||||
|
||||
return (blocks ?? []).map((block) => {
|
||||
if (!block) {
|
||||
return null;
|
||||
}
|
||||
switch (block.blockType) {
|
||||
case "RichTextBlock":
|
||||
return <RichTextBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<RichTextBlock
|
||||
key={block.id}
|
||||
block={block as RichTextBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "ImageWithTextBlock":
|
||||
return <ImageWithTextBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<ImageWithTextBlock
|
||||
key={block.id}
|
||||
block={block as ImageWithTextBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "EmbedBlock":
|
||||
return <EmbedBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<EmbedBlock key={block.id} block={block as EmbedBlockFragment} />
|
||||
);
|
||||
case "ImageSliderBlock":
|
||||
return <ImageSliderBlock key={block.id} block={block} pageContent />;
|
||||
break;
|
||||
return (
|
||||
<ImageSliderBlock
|
||||
key={block.id}
|
||||
block={block as ImageSliderBlockFragment}
|
||||
pageContent
|
||||
/>
|
||||
);
|
||||
case "HorizontalRuleBlock":
|
||||
return <HorizontalRuleBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<HorizontalRuleBlock
|
||||
key={block.id}
|
||||
block={block as HorizontalRuleBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "FeaturedBlock":
|
||||
return <FeaturedBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<FeaturedBlock
|
||||
key={block.id}
|
||||
block={block as FeaturedBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "AccordionBlock":
|
||||
return <AccordionBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<AccordionBlock
|
||||
key={block.id}
|
||||
block={block as AccordionBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "FactBoxBlock":
|
||||
return <FactBoxBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<FactBoxBlock key={block.id} block={block as FactBoxBlockFragment} />
|
||||
);
|
||||
case "ScheduleBlock":
|
||||
return <ScheduleBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<ScheduleBlock
|
||||
key={block.id}
|
||||
block={block as ScheduleBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "PageSectionBlock":
|
||||
return <PageSectionBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<PageSectionBlock
|
||||
key={block.id}
|
||||
block={block as PageSectionBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "PageSectionNavigationBlock":
|
||||
return <PageSectionNavigationBlock sections={sections} />;
|
||||
break;
|
||||
return (
|
||||
<PageSectionNavigationBlock key={block.id} sections={sections} />
|
||||
);
|
||||
case "ContactSectionBlock":
|
||||
return <ContactSectionBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<ContactSectionBlock
|
||||
key={block.id}
|
||||
block={block as ContactSectionBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "ContactSubsectionBlock":
|
||||
return <ContactSubsectionBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<ContactSubsectionBlock
|
||||
key={block.id}
|
||||
block={block as ContactSubsectionBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "ContactListBlock":
|
||||
return <ContactListBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<ContactListBlock
|
||||
key={block.id}
|
||||
block={block as ContactListBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "ContactEntityBlock":
|
||||
return <ContactEntityBlock key={block.id} block={block} />;
|
||||
break;
|
||||
return (
|
||||
<ContactEntityBlock
|
||||
key={block.id}
|
||||
block={block as ContactEntityBlockFragment}
|
||||
/>
|
||||
);
|
||||
case "NeufAddressSectionBlock":
|
||||
return <NeufAddressSectionBlock />;
|
||||
break;
|
||||
return <NeufAddressSectionBlock key={block.id} />;
|
||||
case "OpeningHoursSectionBlock":
|
||||
return <OpeningHoursSectionBlock />;
|
||||
break;
|
||||
return <OpeningHoursSectionBlock key={block.id} />;
|
||||
default:
|
||||
console.log("unsupported block", block);
|
||||
return <div>Unsupported block type {block?.blockType}</div>;
|
||||
return (
|
||||
<div key={block.id}>Unsupported block type {block.blockType}</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
type ContactSectionBlockFragment,
|
||||
type ContactSubsectionBlockFragment,
|
||||
} from "@/gql/graphql";
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import styles from "./contactSection.module.scss";
|
||||
import { Blocks } from "./Blocks";
|
||||
|
||||
@@ -37,10 +38,9 @@ export const ContactSectionBlock = ({
|
||||
<section className={styles.contactSection}>
|
||||
<h2 className={styles.heading}>{block.title}</h2>
|
||||
{block.text && (
|
||||
<p
|
||||
className={styles.intro}
|
||||
dangerouslySetInnerHTML={{ __html: block.text }}
|
||||
/>
|
||||
<div className={styles.intro}>
|
||||
<RichText content={block.text} />
|
||||
</div>
|
||||
)}
|
||||
<Blocks blocks={block.blocks} />
|
||||
</section>
|
||||
@@ -56,10 +56,9 @@ export const ContactSubsectionBlock = ({
|
||||
<section className={styles.contactSubsection}>
|
||||
<h3 className={styles.heading}>{block.title}</h3>
|
||||
{block.text && (
|
||||
<p
|
||||
className={styles.intro}
|
||||
dangerouslySetInnerHTML={{ __html: block.text }}
|
||||
/>
|
||||
<div className={styles.intro}>
|
||||
<RichText content={block.text} />
|
||||
</div>
|
||||
)}
|
||||
<Blocks blocks={block.blocks} />
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { graphql } from "@/gql";
|
||||
import { type FactBoxBlockFragment } from "@/gql/graphql";
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import styles from "./factBoxBlock.module.scss";
|
||||
|
||||
const FactBoxBlockFragmentDefinition = graphql(`
|
||||
@@ -23,10 +24,9 @@ export const FactBoxBlock = ({
|
||||
className={styles.factBox}
|
||||
data-background-color={block.backgroundColor ?? ""}
|
||||
>
|
||||
<div
|
||||
className={styles.factBoxContent}
|
||||
dangerouslySetInnerHTML={{ __html: block.factBoxBody }}
|
||||
/>
|
||||
<div className={styles.factBoxContent}>
|
||||
<RichText content={block.factBoxBody} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,9 @@ import { graphql, unmaskFragment } from "@/gql";
|
||||
import { type FeaturedBlockFragment } from "@/gql/graphql";
|
||||
import Link from "next/link";
|
||||
import { Image } from "@/components/general/Image";
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import { ImageFragmentDefinition } from "@/lib/common";
|
||||
import { internalHref } from "@/lib/links";
|
||||
import styles from "./featuredBlock.module.scss";
|
||||
|
||||
const FeaturedBlockFragmentDefinition = graphql(`
|
||||
@@ -44,6 +46,9 @@ export const FeaturedBlock = ({
|
||||
);
|
||||
// TODO: fetch image from target page
|
||||
|
||||
const pageUrl = block.featuredPage.url;
|
||||
const featuredHref = pageUrl ? internalHref(pageUrl) ?? pageUrl : "#";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.featuredBlock}
|
||||
@@ -51,8 +56,10 @@ export const FeaturedBlock = ({
|
||||
>
|
||||
<div className={styles.text}>
|
||||
<h2>{block.title}</h2>
|
||||
<div dangerouslySetInnerHTML={{ __html: block.featuredBlockText }} />
|
||||
<Link href={block.featuredPage.url ?? "#"}>
|
||||
<div>
|
||||
<RichText content={block.featuredBlockText} />
|
||||
</div>
|
||||
<Link href={featuredHref}>
|
||||
{block.linkText} →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { graphql, unmaskFragment } from "@/gql";
|
||||
import { type ScheduleBlockFragment } from "@/gql/graphql";
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import styles from "./scheduleBlock.module.scss";
|
||||
|
||||
const ScheduleItemFragmentDefinition = graphql(`
|
||||
@@ -30,7 +31,7 @@ export const ScheduleBlock = ({
|
||||
return (
|
||||
<section className={styles.schedule}>
|
||||
{block.scheduleTitle && (
|
||||
<h2 className={styles.title}>{block.scheduleTitle}</h2>
|
||||
<h2 className={styles.heading}>{block.scheduleTitle}</h2>
|
||||
)}
|
||||
<dl className={styles.items}>
|
||||
{block.scheduleItems?.map((item, index) => {
|
||||
@@ -45,18 +46,13 @@ export const ScheduleBlock = ({
|
||||
<div key={index} className={styles.item}>
|
||||
<dt className={styles.time}>{scheduleItem.time}</dt>
|
||||
<dd className={styles.body}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: scheduleItem.scheduleItemTitle,
|
||||
}}
|
||||
/>
|
||||
<div className={styles.title}>
|
||||
<RichText content={scheduleItem.scheduleItemTitle} />
|
||||
</div>
|
||||
{scheduleItem.description && (
|
||||
<div
|
||||
className={styles.description}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: scheduleItem.description,
|
||||
}}
|
||||
/>
|
||||
<div className={styles.description}>
|
||||
<RichText content={scheduleItem.description} />
|
||||
</div>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
margin: 0 auto var(--spacing-section-bottom);
|
||||
}
|
||||
|
||||
.title {
|
||||
.heading {
|
||||
font-size: var(--font-size-h3);
|
||||
margin: 0 0 var(--spacing-s);
|
||||
}
|
||||
|
||||
@@ -14,7 +15,7 @@
|
||||
.item {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
padding: var(--spacing-xs) 0;
|
||||
padding: var(--spacing-s) 0;
|
||||
border-top: var(--border);
|
||||
|
||||
&:last-child {
|
||||
@@ -22,9 +23,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
|
||||
a {
|
||||
text-decoration-thickness: .1rem;
|
||||
-webkit-text-decoration-color: var(--color-goldenOrange);
|
||||
text-decoration-color: var(--color-goldenOrange);
|
||||
transition: text-decoration-color var(--transition-easing);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
flex: 0 0 8rem;
|
||||
font-weight: 600;
|
||||
font-family: var(--font-serif);
|
||||
line-height: 1.1rem;
|
||||
}
|
||||
|
||||
.body {
|
||||
@@ -35,3 +51,12 @@
|
||||
margin-top: var(--spacing-xs);
|
||||
font-size: var(--font-size-caption);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.item {
|
||||
display: block;
|
||||
}
|
||||
.time {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
@@ -304,14 +304,14 @@ const CalendarWeek = ({
|
||||
);
|
||||
};
|
||||
|
||||
function maybeYear(yearMonthString: string) {
|
||||
function srOnlyYear(yearMonthString: string) {
|
||||
// formatYearMonth only shows the year for non-current years; always expose it to screen readers
|
||||
const yearMonth = new Date(yearMonthString);
|
||||
const now = new Date();
|
||||
const isCurrentYear = yearMonth.getFullYear() == now.getFullYear();
|
||||
if (isCurrentYear) {
|
||||
return <span className="sr-only"> {yearMonth.getFullYear()}</span>;
|
||||
const isCurrentYear = yearMonth.getFullYear() === new Date().getFullYear();
|
||||
if (!isCurrentYear) {
|
||||
return null;
|
||||
}
|
||||
return ` ${yearMonth.getFullYear()}`;
|
||||
return <span className="sr-only"> {yearMonth.getFullYear()}</span>;
|
||||
}
|
||||
|
||||
const EventCalendar = ({ events }: { events: EventOverviewItemFragment[] }) => {
|
||||
@@ -354,7 +354,7 @@ const EventCalendar = ({ events }: { events: EventOverviewItemFragment[] }) => {
|
||||
>
|
||||
<h2 onClick={() => toggleYearMonth(yearMonth)}>
|
||||
{formatYearMonth(yearMonth)}
|
||||
{maybeYear(yearMonth)}{" "}
|
||||
{srOnlyYear(yearMonth)}{" "}
|
||||
<span className={styles.eventCounter}>({eventCount})</span>
|
||||
</h2>
|
||||
{eventCount === 0 && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { VenueFragment } from "@/gql/graphql";
|
||||
import { getClient } from "@/app/client";
|
||||
import { cachedUntilOsloMidnight } from "@/lib/revalidation";
|
||||
import { EventContainer } from "@/components/events/EventContainer";
|
||||
import { PageHeader } from "@/components/general/PageHeader";
|
||||
import {
|
||||
@@ -17,7 +18,11 @@ export type EventIndexViewProps = {
|
||||
};
|
||||
|
||||
export async function loadEventIndexProps(): Promise<EventIndexViewProps> {
|
||||
const { data, error } = await getClient().query(eventsOverviewQuery, {});
|
||||
const { data, error } = await getClient().query(
|
||||
eventsOverviewQuery,
|
||||
{},
|
||||
cachedUntilOsloMidnight()
|
||||
);
|
||||
if (error) throw new Error(error.message);
|
||||
if (
|
||||
!data?.index ||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { EventDetails } from "@/components/events/EventDetails";
|
||||
import { EventHeader } from "@/components/events/EventHeader";
|
||||
import { BgPig } from "@/components/general/BgPig";
|
||||
import { PageContent } from "@/components/general/PageContent";
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import { getEventPig } from "@/lib/event";
|
||||
|
||||
const eventBySlugQuery = graphql(`
|
||||
@@ -45,10 +46,9 @@ export function EventPageView({ event }: EventPageViewProps) {
|
||||
<EventHeader event={event} />
|
||||
<EventDetails event={event} />
|
||||
{event.lead && (
|
||||
<div
|
||||
className="lead event-lead"
|
||||
dangerouslySetInnerHTML={{ __html: event.lead }}
|
||||
/>
|
||||
<div className="lead event-lead">
|
||||
<RichText content={event.lead} />
|
||||
</div>
|
||||
)}
|
||||
<PageContent blocks={event.body} />
|
||||
</main>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Blocks } from "@/components/blocks/Blocks";
|
||||
import { Blocks, type StreamFieldBlock } from "@/components/blocks/Blocks";
|
||||
import styles from './pageContent.module.scss';
|
||||
|
||||
export const PageContent = ({ blocks, className }: { blocks: any; className?: string }) => {
|
||||
export const PageContent = ({
|
||||
blocks,
|
||||
className,
|
||||
}: {
|
||||
blocks?: Array<StreamFieldBlock | null> | null;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${styles.pageContent}${className ? ` ${className}` : ''}`}>
|
||||
<Blocks blocks={blocks} pageContent />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import styles from "./pageHeader.module.scss";
|
||||
|
||||
export const PageHeader = ({
|
||||
@@ -13,7 +14,9 @@ export const PageHeader = ({
|
||||
<div className={`${styles.pageHeader} ${align && styles[align]}`}>
|
||||
<h1 className={styles.title}>{heading}</h1>
|
||||
{lead && (
|
||||
<div className="lead" dangerouslySetInnerHTML={{ __html: lead }} />
|
||||
<div className="lead">
|
||||
<RichText content={lead} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,26 +4,39 @@ import parse, {
|
||||
type DOMNode,
|
||||
type HTMLReactParserOptions,
|
||||
} from "html-react-parser";
|
||||
import Link from "next/link";
|
||||
import { ButtonLink } from "@/components/general/ButtonLink";
|
||||
import { internalHref } from "@/lib/links";
|
||||
|
||||
/**
|
||||
* Renders CMS rich text (HTML) as React, swapping inline button markers
|
||||
* (`<a class="button">` from the button-link Draftail feature) for the
|
||||
* ButtonLink component. Everything else renders as plain HTML.
|
||||
* Renders CMS rich text (HTML) as React. Everything renders as plain HTML,
|
||||
* except:
|
||||
*
|
||||
* - inline button markers (`<a class="button">` from the button-link Draftail
|
||||
* feature) become the ButtonLink component
|
||||
* - links pointing at this site become next/link
|
||||
*/
|
||||
const options: HTMLReactParserOptions = {
|
||||
replace: (node) => {
|
||||
if (
|
||||
node instanceof Element &&
|
||||
node.name === "a" &&
|
||||
node.attribs.class?.split(/\s+/).includes("button")
|
||||
) {
|
||||
if (!(node instanceof Element) || node.name !== "a") {
|
||||
return;
|
||||
}
|
||||
const href = node.attribs.href ?? "#";
|
||||
const internal = internalHref(href);
|
||||
if (node.attribs.class?.split(/\s+/).includes("button")) {
|
||||
return (
|
||||
<ButtonLink href={node.attribs.href ?? "#"}>
|
||||
<ButtonLink href={internal ?? href}>
|
||||
{domToReact(node.children as DOMNode[], options)}
|
||||
</ButtonLink>
|
||||
);
|
||||
}
|
||||
if (internal) {
|
||||
return (
|
||||
<Link href={internal}>
|
||||
{domToReact(node.children as DOMNode[], options)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import Link from "next/link";
|
||||
import styles from "./sectionFooter.module.scss";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export const SectionFooter = ({ link, linkText }: { link: string, linkText: string }) => {
|
||||
return (
|
||||
<footer className={styles.sectionFooter}>
|
||||
{link && linkText && <Link href={link}>{linkText}</Link>}
|
||||
{link && linkText && <Link href={link} className={styles.link}>
|
||||
<span>{linkText}</span>
|
||||
<Icon type="arrowRight" />
|
||||
</Link>}
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import styles from "./sectionHeader.module.scss";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export const SectionHeader = ({ heading, link, linkText }: { heading: string, link?: string, linkText?: string }) => {
|
||||
return (
|
||||
@@ -10,7 +11,12 @@ export const SectionHeader = ({ heading, link, linkText }: { heading: string, li
|
||||
<span className="circle"></span>
|
||||
{heading}
|
||||
</h2>
|
||||
{link && linkText && <Link href={link}>{linkText}</Link>}
|
||||
{link && linkText &&
|
||||
<Link href={link} className={styles.link}>
|
||||
<span>{linkText}</span>
|
||||
<Icon type="arrowRight" />
|
||||
</Link>
|
||||
}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,21 +5,20 @@
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
font-family: var(--font-serif);
|
||||
position: relative;
|
||||
padding-right: 1.4rem;
|
||||
|
||||
&:after {
|
||||
content: "→";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
display: none;
|
||||
@media (max-width: 800px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
margin-bottom: var(--spacing-s);
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
font-family: var(--font-serif);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,21 @@
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
font-family: var(--font-serif);
|
||||
margin-bottom: var(--spacing-s);
|
||||
position: relative;
|
||||
padding-right: 1.4rem;
|
||||
|
||||
&:after {
|
||||
content: "→";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
a {
|
||||
.link {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
margin-bottom: var(--spacing-s);
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
font-family: var(--font-serif);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import Link from "next/link";
|
||||
import { graphql } from "@/gql";
|
||||
import { HomeFragment } from "@/gql/graphql";
|
||||
import { getClient } from "@/app/client";
|
||||
import { cachedUntilOsloMidnight } from "@/lib/revalidation";
|
||||
import { EventListItemFragment } from "@/lib/event";
|
||||
import { NewsListItemFragment } from "@/lib/news";
|
||||
import { FeaturedEvents } from "@/components/events/FeaturedEvents";
|
||||
@@ -12,6 +13,7 @@ import { Pig } from "@/components/general/Pig";
|
||||
import { SectionFooter } from "@/components/general/SectionFooter";
|
||||
import { SectionHeader } from "@/components/general/SectionHeader";
|
||||
import { NewsList } from "@/components/news/NewsList";
|
||||
import { StudioShortcut } from "../studio/StudioShortcut";
|
||||
|
||||
const HomeFragmentDefinition = graphql(`
|
||||
fragment Home on HomePage {
|
||||
@@ -55,7 +57,11 @@ export type HomePageViewProps = {
|
||||
export async function loadHomePageProps(overrides?: {
|
||||
homeOverride?: HomeFragment;
|
||||
}): Promise<HomePageViewProps> {
|
||||
const { data, error } = await getClient().query(homeQuery, {});
|
||||
const { data, error } = await getClient().query(
|
||||
homeQuery,
|
||||
{},
|
||||
cachedUntilOsloMidnight()
|
||||
);
|
||||
if (error) throw new Error(error.message);
|
||||
const home = overrides?.homeOverride ?? (data?.home as HomeFragment | undefined);
|
||||
if (!home) throw new Error("Failed to load /");
|
||||
@@ -74,6 +80,7 @@ export function HomePageView({ home, events, news }: HomePageViewProps) {
|
||||
return (
|
||||
<>
|
||||
<main className="site-main index" id="main">
|
||||
<StudioShortcut />
|
||||
<FeaturedEvents events={featuredEvents} />
|
||||
<UpcomingEvents events={events} />
|
||||
<div className="infoBlock">
|
||||
|
||||
@@ -53,12 +53,12 @@ export const Header = () => {
|
||||
// let's add a santa hat during December
|
||||
const isChristmas = new Date().getMonth() === 11;
|
||||
|
||||
// let's add the student hat during grisefestuka
|
||||
// let's add the student hat during STUDiO 2026
|
||||
const pigWearsHat =
|
||||
new Date().getFullYear() === 2025 &&
|
||||
new Date().getMonth() === 9 &&
|
||||
new Date().getDate() >= 6 &&
|
||||
new Date().getDate() <= 12;
|
||||
new Date().getFullYear() === 2026 &&
|
||||
new Date().getMonth() === 7 &&
|
||||
new Date().getDate() >= 10 &&
|
||||
new Date().getDate() <= 16;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -191,12 +191,8 @@ export const Header = () => {
|
||||
</div>
|
||||
</nav>
|
||||
<div className={styles.headerBar} aria-hidden>
|
||||
<Link href="/" aria-label="Hjem">
|
||||
{!isInView ? (
|
||||
<Link href="/" aria-label="Hjem" tabIndex={-1}>
|
||||
<LogoIcon face="left" christmas={isChristmas} />
|
||||
) : (
|
||||
<Logo christmas={isChristmas} studentHat={pigWearsHat} />
|
||||
)}
|
||||
</Link>
|
||||
<nav className={styles.siteMenu}>
|
||||
<ul className={styles.mainMenu}>
|
||||
@@ -204,22 +200,35 @@ export const Header = () => {
|
||||
<Link
|
||||
href="/arrangementer"
|
||||
data-active={pathname === "/arrangementer"}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Arrangementer
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/praktisk" data-active={pathname === "/praktisk"}>
|
||||
<Link
|
||||
href="/praktisk"
|
||||
data-active={pathname === "/praktisk"}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Praktisk info
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/utleie" data-active={pathname === "/utleie"}>
|
||||
<Link
|
||||
href="/utleie"
|
||||
data-active={pathname === "/utleie"}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Utleie
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/frivillig" data-active={pathname === "/frivillig"}>
|
||||
<Link
|
||||
href="/frivillig"
|
||||
data-active={pathname === "/frivillig"}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Bli frivillig
|
||||
</Link>
|
||||
</li>
|
||||
@@ -227,6 +236,7 @@ export const Header = () => {
|
||||
<Link
|
||||
href="/foreninger"
|
||||
data-active={pathname === "/foreninger"}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Foreninger
|
||||
</Link>
|
||||
@@ -236,6 +246,7 @@ export const Header = () => {
|
||||
className={styles.toggleMenu}
|
||||
aria-label="Vis meny"
|
||||
onClick={toggleMenu}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<MenuIcon showMenu={showMenu} />
|
||||
</button>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getClient } from "@/app/client";
|
||||
import { Breadcrumb } from "@/components/general/Breadcrumb";
|
||||
import { ImageFigure } from "@/components/general/Image";
|
||||
import { PageContent } from "@/components/general/PageContent";
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import { formatDate } from "@/lib/date";
|
||||
|
||||
const newsBySlugQuery = graphql(`
|
||||
@@ -47,10 +48,9 @@ export function NewsPageView({ news }: NewsPageViewProps) {
|
||||
/>
|
||||
<h1 className="news-title">{news.title}</h1>
|
||||
{news.lead && (
|
||||
<div
|
||||
className="lead"
|
||||
dangerouslySetInnerHTML={{ __html: news.lead }}
|
||||
/>
|
||||
<div className="lead">
|
||||
<RichText content={news.lead} />
|
||||
</div>
|
||||
)}
|
||||
{featuredImage && (
|
||||
<ImageFigure
|
||||
|
||||
@@ -2,6 +2,7 @@ import { graphql, unmaskFragment } from "@/gql";
|
||||
import { type SponsorFragment } from "@/gql/graphql";
|
||||
import { Image } from "../general/Image";
|
||||
import { ImageFragmentDefinition } from "@/lib/common";
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import styles from "./sponsorList.module.scss";
|
||||
|
||||
export const SponsorFragmentDefinition = graphql(`
|
||||
@@ -35,10 +36,9 @@ const SponsorItem = ({ sponsor }: { sponsor: SponsorFragment }) => {
|
||||
<div className={styles.text}>
|
||||
<h2>{name}</h2>
|
||||
{text && (
|
||||
<p
|
||||
className={styles.sponsorText}
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
/>
|
||||
<div className={styles.sponsorText}>
|
||||
<RichText content={text} />
|
||||
</div>
|
||||
)}
|
||||
{website && (
|
||||
<p className={styles.website}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RichText } from "@/components/general/RichText";
|
||||
import styles from "./studioHeader.module.scss";
|
||||
|
||||
export const StudioHeader = ({
|
||||
@@ -10,25 +11,53 @@ export const StudioHeader = ({
|
||||
return (
|
||||
<div className={styles.studioHeader}>
|
||||
<h1 className="sr-only">{title}</h1>
|
||||
<div className={styles.logos}>
|
||||
<div className={styles.banner}>
|
||||
<div className={styles.deco}>
|
||||
<img
|
||||
className={styles.decoLeft}
|
||||
src="/assets/graphics/studio-2026/studio-banner-left-deco.svg"
|
||||
/>
|
||||
<img
|
||||
className={styles.decoRight}
|
||||
src="/assets/graphics/studio-2026/studio-banner-right-deco.svg"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.logo}>
|
||||
<img
|
||||
className={styles.mainLogo}
|
||||
src="/assets/graphics/studio-2026/studio-logo-2026.svg"
|
||||
alt="STUDiO"
|
||||
/>
|
||||
<div className={styles.logoText}>
|
||||
<img
|
||||
className={styles.studentfestivalen}
|
||||
src="/assets/graphics/studio-2026/studio-studentfestivalen-i-oslo.svg"
|
||||
alt="Studentfestivalen i Oslo"
|
||||
/>
|
||||
<img
|
||||
className={styles.mainLogo}
|
||||
src="/assets/graphics/studio-2026/studio-logo-2026.svg"
|
||||
alt="STUDiO"
|
||||
/>
|
||||
<img
|
||||
className={styles.fadderuke}
|
||||
src="/assets/graphics/studio-2026/studio-hele-oslos-fadderuke.svg"
|
||||
alt="Hele oslos fadderuke"
|
||||
alt="Hele Oslos fadderuke"
|
||||
/>
|
||||
<img
|
||||
className={styles.datos}
|
||||
src="/assets/graphics/studio-2026/studio-datos.svg"
|
||||
alt="10–14. august"
|
||||
/>
|
||||
</div>
|
||||
<a href="https://www.instagram.com/studentfestivalenioslo/" className={styles.instagram} target="_blank">
|
||||
<img
|
||||
className={styles.icon}
|
||||
src="/assets/graphics/studio-2026/studio-instagram.svg"
|
||||
alt="Instagram"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{lead && (
|
||||
<div className="lead" dangerouslySetInnerHTML={{ __html: lead }} />
|
||||
<div className={`lead ${styles.lead}`}>
|
||||
<RichText content={lead} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { fromZonedTime } from "date-fns-tz";
|
||||
import Link from "next/link";
|
||||
import { Icon } from "../general/Icon";
|
||||
import styles from "./studioShortcut.module.scss";
|
||||
|
||||
// Hidden from this date
|
||||
// Note that home page revalidation happens at midnight
|
||||
const showUntil = fromZonedTime("2026-08-16T00:00:00", "Europe/Oslo");
|
||||
|
||||
export const StudioShortcut = () => {
|
||||
if (new Date() >= showUntil) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.studioShortcut}>
|
||||
<Link href="/studio" className={styles.button}>
|
||||
<img
|
||||
className={styles.logo}
|
||||
src="/assets/graphics/studio-2026/studio-logo-shortcut.svg"
|
||||
alt="STUDiO"
|
||||
/>
|
||||
<div className={styles.text}>
|
||||
<span className={styles.heading}>Alt om Studentfestivalen i Oslo</span><br />
|
||||
<span>Gratis arrangementer hele uka!</span>
|
||||
</div>
|
||||
<div className={styles.icon}>
|
||||
<Icon type="arrowRight" />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +1,95 @@
|
||||
.studioHeader {
|
||||
position: relative;
|
||||
width: var(--size-width-lead);
|
||||
max-width: 100%;
|
||||
margin: 0 auto var(--spacing-l);
|
||||
}
|
||||
|
||||
.logos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.banner {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
margin: calc(var(--spacing-sitepadding-block) * -1) calc(var(--spacing-sitepadding-inline) * -1) 2rem;
|
||||
background: #EEA971;
|
||||
}
|
||||
|
||||
.deco {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&Left,
|
||||
&Right {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&Left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&Right {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
grid-template-columns: auto 2fr .5fr;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
margin-bottom: var(--spacing-m);
|
||||
justify-content: center;
|
||||
gap: var(--spacing-m);
|
||||
padding: var(--spacing-xs);
|
||||
width: calc(var(--size-width-lead) + var(--spacing-sitepadding-inline));
|
||||
max-width: 100%;
|
||||
margin: 0 auto var(--spacing-l);
|
||||
}
|
||||
|
||||
.mainLogo {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 16rem;
|
||||
max-width: 16vw;
|
||||
height: auto;
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.logoText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 28rem;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.studentfestivalen {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 27rem;
|
||||
width: 83%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.fadderuke {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 30rem;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.datos {
|
||||
display: block;
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.instagram {
|
||||
width: var(--size-icon-circle);
|
||||
height: var(--size-icon-circle);
|
||||
border-radius: 10rem;
|
||||
background: #D68A56;
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
.icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.lead {
|
||||
width: var(--size-width-lead);
|
||||
max-width: 100%;
|
||||
margin: 0 auto var(--spacing-l);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
.studioShortcut {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin: calc(var(--spacing-sitepadding-block) * -1.8) 0 calc(var(--spacing-sitepadding-block) * .8) auto;
|
||||
max-width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
padding: 0 1.5rem 0 0;
|
||||
border-radius: 10rem;
|
||||
background: #EEA971;
|
||||
color: var(--color-deepBrick);
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
background: url("/assets/graphics/studio-2026/studio-shiny.svg") no-repeat center / contain;
|
||||
position: absolute;
|
||||
top: -2%;
|
||||
left: -2%;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 6rem;
|
||||
overflow: hidden;
|
||||
border-top-left-radius: 10rem;
|
||||
border-bottom-left-radius: 10rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
line-height: 1.3;
|
||||
font-size: var(--font-size-s);
|
||||
|
||||
span {
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-weight: 600;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.button {
|
||||
margin: calc(var(--spacing-sitepadding-block) * -1) 0 var(--spacing-sitepadding-block) auto;
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-s);
|
||||
padding: 0 1rem 0 0;
|
||||
|
||||
&:after {
|
||||
width: .7rem;
|
||||
height: .7rem;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.text {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.button {
|
||||
margin: calc(var(--spacing-sitepadding-block) * -.5) 0 var(--spacing-sitepadding-block) auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1800px) {
|
||||
.button {
|
||||
margin: calc(var(--spacing-sitepadding-block) * -1.6) 0 calc(var(--spacing-sitepadding-block) * .5) auto;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export const VenueInfo = ({ venue }: { venue: VenueFragment }) => {
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Lyd</th>
|
||||
<td>{venue.capabilityLighting}</td>
|
||||
<td>{venue.capabilityAudio}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Lys</th>
|
||||
|
||||
@@ -15,8 +15,6 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
|
||||
*/
|
||||
type Documents = {
|
||||
"\n query allGenericSlugs {\n pages(contentType: \"generic.GenericPage\") {\n id\n urlPath\n }\n }\n ": typeof types.AllGenericSlugsDocument,
|
||||
"\n query allNewsSlugs {\n pages(contentType: \"news.NewsPage\") {\n id\n slug\n }\n }\n ": typeof types.AllNewsSlugsDocument,
|
||||
"\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n ": typeof types.AllEventSlugsDocument,
|
||||
"\n query allAssociationSlugs {\n pages(contentType: \"associations.AssociationPage\") {\n id\n slug\n }\n }\n ": typeof types.AllAssociationSlugsDocument,
|
||||
"\n query allVenueSlugs {\n pages(contentType: \"venues.VenuePage\", limit: 100) {\n id\n slug\n }\n }\n ": typeof types.AllVenueSlugsDocument,
|
||||
"\n query previewPage($token: String!) {\n page: page(token: $token) {\n __typename\n ... on GenericPage {\n ...Generic\n }\n ... on StudioPage {\n ...Studio\n }\n ... on SponsorsPage {\n ...SponsorsPage\n }\n ... on HomePage {\n ...Home\n }\n ... on EventPage {\n ...Event\n }\n ... on NewsPage {\n ...News\n }\n ... on AssociationPage {\n ...Association\n }\n ... on VenuePage {\n ...Venue\n }\n ... on NewsIndex {\n ...NewsIndex\n }\n ... on AssociationIndex {\n ...AssociationIndex\n }\n ... on VenueIndex {\n ...VenueIndex\n }\n ... on VenueRentalIndex {\n ...VenueRentalIndex\n }\n ... on ContactIndex {\n ...ContactIndex\n }\n }\n }\n": typeof types.PreviewPageDocument,
|
||||
@@ -85,8 +83,6 @@ type Documents = {
|
||||
};
|
||||
const documents: Documents = {
|
||||
"\n query allGenericSlugs {\n pages(contentType: \"generic.GenericPage\") {\n id\n urlPath\n }\n }\n ": types.AllGenericSlugsDocument,
|
||||
"\n query allNewsSlugs {\n pages(contentType: \"news.NewsPage\") {\n id\n slug\n }\n }\n ": types.AllNewsSlugsDocument,
|
||||
"\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n ": types.AllEventSlugsDocument,
|
||||
"\n query allAssociationSlugs {\n pages(contentType: \"associations.AssociationPage\") {\n id\n slug\n }\n }\n ": types.AllAssociationSlugsDocument,
|
||||
"\n query allVenueSlugs {\n pages(contentType: \"venues.VenuePage\", limit: 100) {\n id\n slug\n }\n }\n ": types.AllVenueSlugsDocument,
|
||||
"\n query previewPage($token: String!) {\n page: page(token: $token) {\n __typename\n ... on GenericPage {\n ...Generic\n }\n ... on StudioPage {\n ...Studio\n }\n ... on SponsorsPage {\n ...SponsorsPage\n }\n ... on HomePage {\n ...Home\n }\n ... on EventPage {\n ...Event\n }\n ... on NewsPage {\n ...News\n }\n ... on AssociationPage {\n ...Association\n }\n ... on VenuePage {\n ...Venue\n }\n ... on NewsIndex {\n ...NewsIndex\n }\n ... on AssociationIndex {\n ...AssociationIndex\n }\n ... on VenueIndex {\n ...VenueIndex\n }\n ... on VenueRentalIndex {\n ...VenueRentalIndex\n }\n ... on ContactIndex {\n ...ContactIndex\n }\n }\n }\n": types.PreviewPageDocument,
|
||||
@@ -172,14 +168,6 @@ export function graphql(source: string): unknown;
|
||||
* 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 allGenericSlugs {\n pages(contentType: \"generic.GenericPage\") {\n id\n urlPath\n }\n }\n "): (typeof documents)["\n query allGenericSlugs {\n pages(contentType: \"generic.GenericPage\") {\n id\n urlPath\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 allNewsSlugs {\n pages(contentType: \"news.NewsPage\") {\n id\n slug\n }\n }\n "): (typeof documents)["\n query allNewsSlugs {\n pages(contentType: \"news.NewsPage\") {\n id\n slug\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 allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n "): (typeof documents)["\n query allEventSlugs {\n pages(contentType: \"events.EventPage\") {\n id\n slug\n }\n }\n "];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
formatHumanReadableList,
|
||||
formatNorwegianPhoneNumber,
|
||||
formatPhoneE164,
|
||||
getSearchPath,
|
||||
randomElement,
|
||||
stripHtml,
|
||||
stripWhitespace,
|
||||
unique,
|
||||
} from "./common.ts";
|
||||
|
||||
describe("getSearchPath", () => {
|
||||
it("builds an encoded query path", () => {
|
||||
expect(getSearchPath("konsert")).toBe("/sok?q=konsert");
|
||||
expect(getSearchPath("øl & mat")).toBe("/sok?q=%C3%B8l+%26+mat");
|
||||
});
|
||||
|
||||
it("handles an empty query", () => {
|
||||
expect(getSearchPath("")).toBe("/sok?");
|
||||
});
|
||||
});
|
||||
|
||||
describe("randomElement", () => {
|
||||
it("picks a member, undefined for empty", () => {
|
||||
expect(randomElement([])).toBeUndefined();
|
||||
expect(randomElement(["a"])).toBe("a");
|
||||
expect([1, 2, 3]).toContain(randomElement([1, 2, 3]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("unique", () => {
|
||||
it("dedupes preserving first-seen order", () => {
|
||||
expect(unique([1, 2, 2, 3, 1])).toEqual([1, 2, 3]);
|
||||
expect(unique([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripWhitespace", () => {
|
||||
it("removes all whitespace", () => {
|
||||
expect(stripWhitespace(" 22 85 32\t00\n")).toBe("22853200");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripHtml", () => {
|
||||
it("removes tags but keeps text", () => {
|
||||
expect(stripHtml("<p>Hei <b>du</b></p>")).toBe("Hei du");
|
||||
expect(stripHtml("ingen tagger")).toBe("ingen tagger");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatPhoneE164", () => {
|
||||
it("prefixes +47 on bare 8-digit numbers only", () => {
|
||||
expect(formatPhoneE164("22 85 32 00")).toBe("+4722853200");
|
||||
expect(formatPhoneE164("+47 22 85 32 00")).toBe("+4722853200");
|
||||
expect(formatPhoneE164("12345")).toBe("12345");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatNorwegianPhoneNumber", () => {
|
||||
it("groups +47 numbers, passes others through", () => {
|
||||
expect(formatNorwegianPhoneNumber("+4722853200")).toBe("228 53 200");
|
||||
expect(formatNorwegianPhoneNumber("22853200")).toBe("22853200");
|
||||
expect(formatNorwegianPhoneNumber("+4612345678")).toBe("+4612345678");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatHumanReadableList", () => {
|
||||
it("joins with og", () => {
|
||||
expect(formatHumanReadableList([])).toBe("");
|
||||
expect(formatHumanReadableList(["Neuf"])).toBe("Neuf");
|
||||
expect(formatHumanReadableList(["a", "b"])).toBe("a og b");
|
||||
expect(formatHumanReadableList(["a", "b", "c"])).toBe("a, b og c");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
compareDates,
|
||||
formatDate,
|
||||
formatDateRange,
|
||||
formatExtendedDateTime,
|
||||
formatOccurrenceMonths,
|
||||
formatYearMonth,
|
||||
groupConsecutiveDates,
|
||||
isConsecutiveDays,
|
||||
isTodayOrFuture,
|
||||
toLocalTime,
|
||||
} from "./date.ts";
|
||||
|
||||
// "now" is tirsdag 2026-07-07 12:00 in Oslo (CEST)
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-07-07T10:00:00Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("toLocalTime", () => {
|
||||
it("converts UTC to Oslo wall clock", () => {
|
||||
expect(toLocalTime("2026-07-07T18:00:00Z").getHours()).toBe(20);
|
||||
expect(toLocalTime("2026-01-07T18:00:00Z").getHours()).toBe(19);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDate", () => {
|
||||
it("formats in Oslo time with Norwegian locale", () => {
|
||||
expect(formatDate("2026-07-07T18:00:00Z", "dd.MM.yyyy 'kl.' HH:mm")).toBe(
|
||||
"07.07.2026 kl. 20:00"
|
||||
);
|
||||
expect(formatDate("2026-07-07T18:00:00Z", "EEEE")).toBe("tirsdag");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatYearMonth", () => {
|
||||
it("omits year for the current year", () => {
|
||||
expect(formatYearMonth("2026-07")).toBe("juli");
|
||||
});
|
||||
|
||||
it("includes year for other years", () => {
|
||||
expect(formatYearMonth("2025-11")).toBe("november 2025");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatExtendedDateTime", () => {
|
||||
it("omits year in the current year", () => {
|
||||
expect(formatExtendedDateTime("2026-07-07T18:00:00Z")).toBe(
|
||||
"tirsdag 7. juli kl. 20:00"
|
||||
);
|
||||
});
|
||||
|
||||
it("includes year for other years", () => {
|
||||
expect(formatExtendedDateTime("2025-11-01T12:00:00Z")).toBe(
|
||||
"lørdag 1. november 2025 kl. 13:00"
|
||||
);
|
||||
});
|
||||
|
||||
it("supports dateOnly and alwaysIncludeYear", () => {
|
||||
expect(formatExtendedDateTime("2026-07-07T18:00:00Z", true)).toBe(
|
||||
"tirsdag 7. juli"
|
||||
);
|
||||
expect(formatExtendedDateTime("2026-07-07T18:00:00Z", false, true)).toBe(
|
||||
"tirsdag 7. juli 2026 kl. 20:00"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isTodayOrFuture", () => {
|
||||
it("is false for yesterday, true for today and later", () => {
|
||||
expect(isTodayOrFuture("2026-07-06T10:00:00Z")).toBe(false);
|
||||
expect(isTodayOrFuture("2026-07-07T20:00:00Z")).toBe(true);
|
||||
expect(isTodayOrFuture("2026-08-01T00:00:00Z")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("compareDates", () => {
|
||||
it("sorts ascending", () => {
|
||||
expect(compareDates("2026-01-01", "2026-01-02")).toBe(-1);
|
||||
expect(compareDates("2026-01-02", "2026-01-01")).toBe(1);
|
||||
expect(compareDates("2026-01-01", "2026-01-01")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isConsecutiveDays", () => {
|
||||
it("is true only for adjacent days", () => {
|
||||
expect(
|
||||
isConsecutiveDays(new Date("2026-07-07"), new Date("2026-07-08"))
|
||||
).toBe(true);
|
||||
expect(
|
||||
isConsecutiveDays(new Date("2026-07-07"), new Date("2026-07-09"))
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("groupConsecutiveDates", () => {
|
||||
it("dedupes and splits on gaps", () => {
|
||||
expect(
|
||||
groupConsecutiveDates([
|
||||
"2026-07-04T20:00:00Z",
|
||||
"2026-07-03T20:00:00Z",
|
||||
"2026-07-04T12:00:00Z",
|
||||
"2026-07-06T20:00:00Z",
|
||||
])
|
||||
).toEqual([["2026-07-03", "2026-07-04"], ["2026-07-06"]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatOccurrenceMonths", () => {
|
||||
it("collapses consecutive months", () => {
|
||||
expect(formatOccurrenceMonths([])).toBe("");
|
||||
expect(formatOccurrenceMonths(["2026-07-01T12:00:00Z"])).toBe("juli 2026");
|
||||
expect(
|
||||
formatOccurrenceMonths([
|
||||
"2026-06-05T18:00:00Z",
|
||||
"2026-07-10T18:00:00Z",
|
||||
"2026-08-01T18:00:00Z",
|
||||
"2026-11-02T18:00:00Z",
|
||||
])
|
||||
).toBe("juni – august 2026, november 2026");
|
||||
});
|
||||
|
||||
it("spells out both years across a year boundary", () => {
|
||||
expect(
|
||||
formatOccurrenceMonths(["2026-12-05T12:00:00Z", "2027-01-05T12:00:00Z"])
|
||||
).toBe("desember 2026 – januar 2027");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDateRange", () => {
|
||||
it("formats single days, same-month and cross-month ranges", () => {
|
||||
expect(formatDateRange(["2026-07-03"])).toBe("3. juli");
|
||||
expect(formatDateRange(["2026-07-03", "2026-07-05", "2026-07-07"])).toBe(
|
||||
"3.—7. juli"
|
||||
);
|
||||
expect(formatDateRange(["2026-06-30", "2026-07-02"])).toBe(
|
||||
"30. juni—2. juli"
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -27,7 +27,7 @@ export function formatDate(date: Date | string | number, formatStr: string) {
|
||||
export function formatYearMonth(yearMonth: string) {
|
||||
// full name of month if year is current year, otherwise name of month + year
|
||||
const parsed = parse(yearMonth, "yyyy-MM", new Date());
|
||||
if (parsed.getFullYear === new Date().getFullYear) {
|
||||
if (parsed.getFullYear() === new Date().getFullYear()) {
|
||||
return formatDate(parsed, "MMMM");
|
||||
}
|
||||
return formatDate(parsed, "MMMM yyyy");
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
type EventFragment,
|
||||
getEventPig,
|
||||
getFutureOccurrences,
|
||||
getSingularEvents,
|
||||
organizeEventsByDate,
|
||||
organizeEventsInCalendar,
|
||||
sortSingularEvents,
|
||||
} from "./event.ts";
|
||||
|
||||
// "now" is tirsdag 2026-07-07 12:00 in Oslo
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-07-07T10:00:00Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const makeEvent = (id: string, starts: string[]) => ({
|
||||
id,
|
||||
occurrences: starts.map((start, i) => ({
|
||||
id: `${id}-${i}`,
|
||||
start,
|
||||
end: null,
|
||||
})),
|
||||
});
|
||||
|
||||
describe("getSingularEvents", () => {
|
||||
it("flattens one copy per occurrence", () => {
|
||||
const a = makeEvent("a", ["2026-07-07T18:00:00Z", "2026-07-08T18:00:00Z"]);
|
||||
const b = makeEvent("b", ["2026-07-09T18:00:00Z"]);
|
||||
|
||||
const singular = getSingularEvents([a, b]);
|
||||
|
||||
expect(singular).toHaveLength(3);
|
||||
expect(singular.map((e) => e.occurrence.id)).toEqual(["a-0", "a-1", "b-0"]);
|
||||
expect(singular[0].id).toBe("a");
|
||||
expect("occurrence" in a).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortSingularEvents", () => {
|
||||
it("sorts by occurrence start", () => {
|
||||
const events = getSingularEvents([
|
||||
makeEvent("late", ["2026-07-09T18:00:00Z"]),
|
||||
makeEvent("early", ["2026-07-07T18:00:00Z"]),
|
||||
makeEvent("mid", ["2026-07-08T18:00:00Z"]),
|
||||
]);
|
||||
|
||||
expect(sortSingularEvents(events).map((e) => e.id)).toEqual([
|
||||
"early",
|
||||
"mid",
|
||||
"late",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("organizeEventsInCalendar", () => {
|
||||
it("nests by yearMonth/week/day and seeds empty days", () => {
|
||||
const events = getSingularEvents([
|
||||
makeEvent("a", ["2026-07-07T18:00:00Z"]),
|
||||
]);
|
||||
|
||||
const calendar = organizeEventsInCalendar(events);
|
||||
|
||||
expect(Object.keys(calendar)).toEqual(["2026-07"]);
|
||||
const weeks = Object.values(calendar["2026-07"]);
|
||||
expect(weeks).toHaveLength(1);
|
||||
const week = weeks[0];
|
||||
// the whole week (man 6. – søn 12. juli) is pre-seeded
|
||||
expect(Object.keys(week)).toHaveLength(7);
|
||||
expect(week["2026-07-07"].map((e) => e.id)).toEqual(["a"]);
|
||||
expect(week["2026-07-06"]).toEqual([]);
|
||||
expect(week["2026-07-12"]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("organizeEventsByDate", () => {
|
||||
it("groups by Oslo date, sorted within the day", () => {
|
||||
const events = getSingularEvents([
|
||||
makeEvent("kveld", ["2026-07-07T20:00:00Z"]),
|
||||
makeEvent("torsdag", ["2026-07-09T18:00:00Z"]),
|
||||
makeEvent("ettermiddag", ["2026-07-07T16:00:00Z"]),
|
||||
]);
|
||||
|
||||
const byDate = organizeEventsByDate(events);
|
||||
|
||||
expect(Object.keys(byDate)).toEqual(["2026-07-07", "2026-07-09"]);
|
||||
expect(byDate["2026-07-07"].map((e) => e.id)).toEqual([
|
||||
"ettermiddag",
|
||||
"kveld",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFutureOccurrences", () => {
|
||||
it("drops past days and sorts ascending", () => {
|
||||
const event = makeEvent("a", [
|
||||
"2026-07-14T18:00:00Z",
|
||||
"2026-07-01T18:00:00Z",
|
||||
"2026-07-07T18:00:00Z",
|
||||
]);
|
||||
|
||||
expect(getFutureOccurrences(event).map((o) => o.start)).toEqual([
|
||||
"2026-07-07T18:00:00Z",
|
||||
"2026-07-14T18:00:00Z",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEventPig", () => {
|
||||
const pigEvent = (pig: unknown, categoryPigs: string[] = []) =>
|
||||
({
|
||||
pig,
|
||||
categories: categoryPigs.map((p) => ({ pig: p })),
|
||||
}) as unknown as EventFragment;
|
||||
|
||||
it("returns an explicit valid pig", () => {
|
||||
expect(getEventPig(pigEvent("dance"))).toBe("dance");
|
||||
});
|
||||
|
||||
it("returns null for empty, missing or unknown pig", () => {
|
||||
expect(getEventPig(pigEvent(""))).toBeNull();
|
||||
expect(getEventPig(pigEvent(null))).toBeNull();
|
||||
expect(getEventPig(pigEvent("notapig"))).toBeNull();
|
||||
});
|
||||
|
||||
it("picks a valid category pig for automatic", () => {
|
||||
expect(getEventPig(pigEvent("automatic", ["music", "bogus"]))).toBe(
|
||||
"music"
|
||||
);
|
||||
expect(["music", "drink"]).toContain(
|
||||
getEventPig(pigEvent("automatic", ["music", "drink"]))
|
||||
);
|
||||
expect(getEventPig(pigEvent("automatic"))).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { internalHref } from "./links.ts";
|
||||
|
||||
describe("internalHref", () => {
|
||||
it("passes through root-relative paths", () => {
|
||||
expect(internalHref("/arrangementer")).toBe("/arrangementer");
|
||||
});
|
||||
|
||||
it("strips the neuf.no origin, keeping query and hash", () => {
|
||||
expect(internalHref("https://neuf.no/arrangementer?side=2#program")).toBe(
|
||||
"/arrangementer?side=2#program"
|
||||
);
|
||||
expect(internalHref("https://neuf.no")).toBe("/");
|
||||
});
|
||||
|
||||
it("returns null for external URLs", () => {
|
||||
expect(internalHref("https://example.com/arrangementer")).toBeNull();
|
||||
expect(internalHref("https://neuf.no.evil.com/x")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
const INTERNAL_ORIGINS = [
|
||||
process.env.URL,
|
||||
"https://neuf.no",
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
/** Root-relative href if the URL points at this site, else null. */
|
||||
export function internalHref(href: string): string | null {
|
||||
if (href.startsWith("/")) {
|
||||
return href;
|
||||
}
|
||||
const origin = INTERNAL_ORIGINS.find(
|
||||
(o) => href === o || href.startsWith(o + "/"),
|
||||
);
|
||||
if (!origin) {
|
||||
return null;
|
||||
}
|
||||
const url = new URL(href);
|
||||
return url.pathname + url.search + url.hash;
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// sever the server-only @/app/client import chain; the network functions are not under test
|
||||
vi.mock("@/app/client", () => ({ getClient: vi.fn() }));
|
||||
|
||||
import type {
|
||||
OpeningHoursRangeBlockFragment,
|
||||
OpeningHoursSetFragment,
|
||||
} from "@/gql/graphql";
|
||||
import {
|
||||
getOpeningHoursForFunction,
|
||||
getPrettyOpeningHoursForFunction,
|
||||
getTodaysOpeningHoursForFunction,
|
||||
groupOpeningHours,
|
||||
} from "./openinghours.ts";
|
||||
|
||||
const range = (
|
||||
timeFrom: string | null,
|
||||
timeTo: string | null,
|
||||
custom: string | null = null
|
||||
) => ({ timeFrom, timeTo, custom });
|
||||
|
||||
const week = {
|
||||
__typename: "OpeningHoursWeekBlock",
|
||||
monday: range("16:00:00", "23:00:00"),
|
||||
tuesday: range("16:00:00", "23:00:00"),
|
||||
wednesday: range("16:00:00", "23:00:00"),
|
||||
thursday: range("16:00:00", "23:00:00"),
|
||||
friday: range("15:00:00", "01:00:00"),
|
||||
saturday: null,
|
||||
sunday: range(null, null, "Kun ved arrangement"),
|
||||
};
|
||||
|
||||
const openingHours = {
|
||||
name: "Vanlige åpningstider",
|
||||
effectiveFrom: "2026-01-01",
|
||||
effectiveTo: null,
|
||||
announcement: null,
|
||||
items: [{ id: "1", function: "bar", week: [week] }],
|
||||
} as unknown as OpeningHoursSetFragment;
|
||||
|
||||
const perDay = (days: Record<string, ReturnType<typeof range> | null>) =>
|
||||
days as unknown as Record<string, OpeningHoursRangeBlockFragment>;
|
||||
|
||||
describe("groupOpeningHours", () => {
|
||||
it("collapses adjacent days with identical hours", () => {
|
||||
const grouped = groupOpeningHours(
|
||||
perDay({
|
||||
monday: range("16:00:00", "23:00:00"),
|
||||
tuesday: range("16:00:00", "23:00:00"),
|
||||
wednesday: range("12:00:00", "20:00:00"),
|
||||
})
|
||||
);
|
||||
|
||||
expect(grouped).toEqual([
|
||||
{
|
||||
days: ["monday", "tuesday"],
|
||||
timeFrom: "16:00:00",
|
||||
timeTo: "23:00:00",
|
||||
custom: null,
|
||||
},
|
||||
{
|
||||
days: ["wednesday"],
|
||||
timeFrom: "12:00:00",
|
||||
timeTo: "20:00:00",
|
||||
custom: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("treats a null day as closed instead of crashing", () => {
|
||||
const grouped = groupOpeningHours(
|
||||
perDay({
|
||||
monday: range("16:00:00", "23:00:00"),
|
||||
tuesday: null,
|
||||
})
|
||||
);
|
||||
|
||||
expect(grouped[1]).toEqual({
|
||||
days: ["tuesday"],
|
||||
timeFrom: null,
|
||||
timeTo: null,
|
||||
custom: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips days missing from the record", () => {
|
||||
const grouped = groupOpeningHours(
|
||||
perDay({
|
||||
monday: range("16:00:00", "23:00:00"),
|
||||
wednesday: range("12:00:00", "20:00:00"),
|
||||
})
|
||||
);
|
||||
|
||||
expect(grouped.map((g) => g.days)).toEqual([["monday"], ["wednesday"]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOpeningHoursForFunction", () => {
|
||||
it("returns the week for a known function", () => {
|
||||
expect(getOpeningHoursForFunction(openingHours, "bar")).toEqual(week);
|
||||
});
|
||||
|
||||
it("returns undefined for unknown or malformed items", () => {
|
||||
expect(getOpeningHoursForFunction(openingHours, "kafé")).toBeUndefined();
|
||||
|
||||
const malformed = {
|
||||
...openingHours,
|
||||
items: [{ id: "1", function: "bar", week: [] }],
|
||||
} as unknown as OpeningHoursSetFragment;
|
||||
expect(getOpeningHoursForFunction(malformed, "bar")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPrettyOpeningHoursForFunction", () => {
|
||||
it("formats grouped Norwegian day ranges", () => {
|
||||
expect(getPrettyOpeningHoursForFunction(openingHours, "bar")).toEqual([
|
||||
{ range: "mandag—torsdag", time: "16:00—23:00" },
|
||||
{ range: "fredag", time: "15:00—01:00" },
|
||||
{ range: "lørdag" },
|
||||
{ range: "søndag", custom: "Kun ved arrangement" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns an empty list for unknown functions", () => {
|
||||
expect(getPrettyOpeningHoursForFunction(openingHours, "kafé")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTodaysOpeningHoursForFunction", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("returns today's hours", () => {
|
||||
vi.setSystemTime(new Date("2026-07-07T10:00:00Z")); // tirsdag
|
||||
expect(getTodaysOpeningHoursForFunction(openingHours, "bar")).toBe(
|
||||
"16:00—23:00"
|
||||
);
|
||||
});
|
||||
|
||||
it("returns Stengt for a null day", () => {
|
||||
vi.setSystemTime(new Date("2026-07-11T10:00:00Z")); // lørdag
|
||||
expect(getTodaysOpeningHoursForFunction(openingHours, "bar")).toBe(
|
||||
"Stengt"
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the custom text when set", () => {
|
||||
vi.setSystemTime(new Date("2026-07-12T10:00:00Z")); // søndag
|
||||
expect(getTodaysOpeningHoursForFunction(openingHours, "bar")).toBe(
|
||||
"Kun ved arrangement"
|
||||
);
|
||||
});
|
||||
|
||||
it("returns ? for unknown functions", () => {
|
||||
vi.setSystemTime(new Date("2026-07-07T10:00:00Z"));
|
||||
expect(getTodaysOpeningHoursForFunction(openingHours, "kafé")).toBe("?");
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "date-fns";
|
||||
|
||||
import { getClient } from "@/app/client";
|
||||
import { cachedUntilOsloMidnight } from "@/lib/revalidation";
|
||||
import { graphql, unmaskFragment } from "@/gql";
|
||||
import type {
|
||||
OpeningHoursRangeBlockFragment as OpeningHoursRangeBlock,
|
||||
@@ -50,7 +51,13 @@ const openingHoursQuery = graphql(`
|
||||
`);
|
||||
|
||||
export async function fetchOpeningHoursSets() {
|
||||
const { data, error } = await getClient().query(openingHoursQuery, {});
|
||||
const { data, error } = await getClient().query(
|
||||
openingHoursQuery,
|
||||
{},
|
||||
// The footer renders today's hours on every page, so this TTL doubles as
|
||||
// the sitewide midnight refresh for futureEvents rollover
|
||||
cachedUntilOsloMidnight()
|
||||
);
|
||||
const sets = (data?.openingHoursSets ?? []) as OpeningHoursSet[];
|
||||
return sets;
|
||||
}
|
||||
@@ -112,9 +119,9 @@ export function groupOpeningHours(
|
||||
) {
|
||||
grouped.push({
|
||||
days: [day],
|
||||
timeFrom: hours.timeFrom ?? null,
|
||||
timeTo: hours.timeTo ?? null,
|
||||
custom: hours.custom ?? null,
|
||||
timeFrom: hours?.timeFrom ?? null,
|
||||
timeTo: hours?.timeTo ?? null,
|
||||
custom: hours?.custom ?? null,
|
||||
});
|
||||
} else {
|
||||
grouped[grouped.length - 1].days.push(day);
|
||||
@@ -209,10 +216,10 @@ export function getTodaysOpeningHoursForFunction(
|
||||
const weekdayIndex = getISODay(startOfToday()) - 1;
|
||||
const weekday = WEEKDAYS[weekdayIndex];
|
||||
const hours = week[weekday];
|
||||
if (hours.timeFrom && hours.timeTo) {
|
||||
if (hours?.timeFrom && hours?.timeTo) {
|
||||
return `${hours.timeFrom.slice(0, 5)}—${hours.timeTo.slice(0, 5)}`;
|
||||
}
|
||||
if (hours.custom && hours.custom.length) {
|
||||
if (hours?.custom?.length) {
|
||||
return hours.custom;
|
||||
}
|
||||
return "Stengt";
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { secondsUntilOsloMidnight } from "./revalidation.ts";
|
||||
|
||||
describe("secondsUntilOsloMidnight", () => {
|
||||
it("is 12h from midnight at 12:00 CEST on a normal day", () => {
|
||||
const now = new Date("2026-07-07T10:00:00Z");
|
||||
expect(secondsUntilOsloMidnight(now)).toBe(12 * 3600);
|
||||
});
|
||||
|
||||
it("clamps to 60s just before midnight", () => {
|
||||
const now = new Date("2026-07-07T21:59:30Z");
|
||||
expect(secondsUntilOsloMidnight(now)).toBe(60);
|
||||
});
|
||||
|
||||
it("handles the 25h DST fall-back day (2026-10-25)", () => {
|
||||
const now = new Date("2026-10-24T22:00:00Z");
|
||||
expect(secondsUntilOsloMidnight(now)).toBe(25 * 3600);
|
||||
});
|
||||
|
||||
it("handles the 23h DST spring-forward day (2026-03-29)", () => {
|
||||
const now = new Date("2026-03-28T23:00:00Z");
|
||||
expect(secondsUntilOsloMidnight(now)).toBe(23 * 3600);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { addDays, startOfDay } from "date-fns";
|
||||
import { fromZonedTime, toZonedTime } from "date-fns-tz";
|
||||
import type { OperationContext } from "@urql/core";
|
||||
|
||||
const timeZone = "Europe/Oslo";
|
||||
|
||||
// Global cache tag: any CMS content change purges everything
|
||||
export const CMS_CACHE_TAG = "cms";
|
||||
|
||||
const MIN_REVALIDATE_SECONDS = 60;
|
||||
|
||||
// Clamped so a request just before midnight never yields revalidate ~0
|
||||
export function secondsUntilOsloMidnight(now: Date = new Date()): number {
|
||||
const osloNow = toZonedTime(now, timeZone);
|
||||
const nextOsloMidnight = startOfDay(addDays(osloNow, 1));
|
||||
const instant = fromZonedTime(nextOsloMidnight, timeZone);
|
||||
return Math.max(
|
||||
Math.ceil((instant.getTime() - now.getTime()) / 1000),
|
||||
MIN_REVALIDATE_SECONDS
|
||||
);
|
||||
}
|
||||
|
||||
// Per-operation contexts replace (not merge with) the client-default
|
||||
// fetchOptions, so both contexts below are self-contained
|
||||
|
||||
// For per-user (search) and token-scoped (preview) queries
|
||||
export const uncached: Partial<OperationContext> = {
|
||||
fetchOptions: { cache: "no-store" },
|
||||
};
|
||||
|
||||
// For queries whose results depend on "today" (futureEvents, opening hours)
|
||||
export function cachedUntilOsloMidnight(): Partial<OperationContext> {
|
||||
return {
|
||||
fetchOptions: {
|
||||
next: { revalidate: secondsUntilOsloMidnight(), tags: [CMS_CACHE_TAG] },
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { ResolvingMetadata } from "next";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getSeoDescription, getSeoMetadata } from "./seo.ts";
|
||||
|
||||
const parent = (openGraph?: object) =>
|
||||
Promise.resolve({ openGraph }) as unknown as ResolvingMetadata;
|
||||
|
||||
describe("getSeoDescription", () => {
|
||||
it("prefers searchDescription", () => {
|
||||
expect(getSeoDescription("Beskrivelse", "<p>Utdrag</p>", "Ingress")).toBe(
|
||||
"Beskrivelse"
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to stripped excerpt, then lead", () => {
|
||||
expect(getSeoDescription(null, "<p>Utdrag</p>", "Ingress")).toBe("Utdrag");
|
||||
expect(getSeoDescription("", "<p> </p>", " <b>Ingress</b> ")).toBe(
|
||||
"Ingress"
|
||||
);
|
||||
});
|
||||
|
||||
it("returns undefined when everything is empty", () => {
|
||||
expect(getSeoDescription(null, null, null)).toBeUndefined();
|
||||
expect(getSeoDescription("", "", "")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSeoMetadata", () => {
|
||||
it("merges parent openGraph and collects images", async () => {
|
||||
const page = {
|
||||
seoTitle: "SEO-tittel",
|
||||
title: "Tittel",
|
||||
searchDescription: "Beskrivelse",
|
||||
featuredImage: { url: "https://cms.neuf.no/img.jpg" },
|
||||
logo: { url: "https://cms.neuf.no/logo.png" },
|
||||
};
|
||||
|
||||
const metadata = await getSeoMetadata(page, parent({ siteName: "Neuf" }));
|
||||
|
||||
expect(metadata).toEqual({
|
||||
title: "SEO-tittel",
|
||||
description: "Beskrivelse",
|
||||
openGraph: {
|
||||
siteName: "Neuf",
|
||||
title: "SEO-tittel",
|
||||
description: "Beskrivelse",
|
||||
images: [
|
||||
"https://cms.neuf.no/img.jpg",
|
||||
"https://cms.neuf.no/logo.png",
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to title and tolerates missing parent openGraph", async () => {
|
||||
const metadata = await getSeoMetadata({ title: "Bare tittel" }, parent());
|
||||
|
||||
expect(metadata.title).toBe("Bare tittel");
|
||||
expect(metadata.description).toBeUndefined();
|
||||
expect(metadata.openGraph.images).toEqual([]);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ResolvingMetadata } from "next";
|
||||
import type { ResolvingMetadata } from "next";
|
||||
import { stripHtml } from "./common";
|
||||
|
||||
export function getSeoDescription(
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["src/**/*.test.{ts,tsx}"],
|
||||
// date/opening-hours logic is Oslo-tz-sensitive
|
||||
env: { TZ: "Europe/Oslo" },
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user