diff --git a/backend/api/cms/page/blocks/dynamic_content_display_section.py b/backend/api/cms/page/blocks/dynamic_content_display_section.py
new file mode 100644
index 0000000000..072803a2a9
--- /dev/null
+++ b/backend/api/cms/page/blocks/dynamic_content_display_section.py
@@ -0,0 +1,25 @@
+from typing import Self
+import strawberry
+from api.cms.page.registry import register_page_block
+import enum
+
+
+@strawberry.enum
+class DynamicContentDisplaySectionSource(enum.Enum):
+ speakers = "speakers"
+ keynoters = "keynoters"
+ proposals = "proposals"
+
+
+@register_page_block()
+@strawberry.type
+class DynamicContentDisplaySection:
+ id: strawberry.ID
+ source: DynamicContentDisplaySectionSource
+
+ @classmethod
+ def from_block(cls, block) -> Self:
+ return cls(
+ id=block.id,
+ source=DynamicContentDisplaySectionSource(block.value["source"]),
+ )
diff --git a/backend/api/participants/queries.py b/backend/api/participants/queries.py
index 9edcd4d02d..4d62abd369 100644
--- a/backend/api/participants/queries.py
+++ b/backend/api/participants/queries.py
@@ -1,5 +1,4 @@
import json
-from typing import Optional
from django.conf import settings
import strawberry
from strawberry.tools import create_type
@@ -15,20 +14,13 @@
@strawberry.field
-def participant(
- info: Info, user_id: strawberry.ID, conference: str
-) -> Optional[Participant]:
- user = info.context.request.user
- decoded_id = decode_hashid(user_id, salt=settings.USER_ID_HASH_SALT, min_length=6)
+def participant(info: Info, id: strawberry.ID, conference: str) -> Participant | None:
+ decoded_id = decode_hashid(id, salt=settings.USER_ID_HASH_SALT, min_length=6)
participant = ParticipantModel.objects.filter(
- conference__code=conference, user_id=decoded_id
+ conference__code=conference, id=decoded_id
).first()
- if not participant or (
- not participant.public_profile and (not user or participant.user_id != user.id)
- ):
- # Profile doesn't exist, or
- # Profile is not public, and the person requesting it is not the owner
+ if not participant:
return None
return Participant.from_model(participant)
@@ -37,7 +29,7 @@ def participant(
@strawberry.field
def ticket_id_to_user_hashid(
ticket_id: strawberry.ID, conference_code: str
-) -> Optional[str]:
+) -> str | None:
conference = Conference.objects.filter(code=conference_code).first()
decoded_ticket_id = decode_hashid(ticket_id)
order_position = pretix.get_order_position(conference, decoded_ticket_id)
diff --git a/backend/api/participants/tests/__init__.py b/backend/api/participants/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/api/participants/tests/test_queries.py b/backend/api/participants/tests/test_queries.py
new file mode 100644
index 0000000000..b170d9eda9
--- /dev/null
+++ b/backend/api/participants/tests/test_queries.py
@@ -0,0 +1,103 @@
+from conferences.tests.factories import ConferenceFactory
+from participants.tests.factories import ParticipantFactory
+
+
+def test_query_participant(graphql_client):
+ participant = ParticipantFactory()
+
+ response = graphql_client.query(
+ """
+ query Participant($id: ID!, $conference: String!) {
+ participant(id: $id, conference: $conference) {
+ id
+ fullname
+ }
+ }
+ """,
+ variables={"id": participant.hashid, "conference": participant.conference.code},
+ )
+
+ assert response["data"]["participant"]["id"] == participant.hashid
+ assert response["data"]["participant"]["fullname"] == participant.user.fullname
+
+
+def test_query_private_fields_of_own_user(graphql_client, user):
+ graphql_client.force_login(user)
+ participant = ParticipantFactory(
+ user=user,
+ speaker_availabilities={"test": "test"},
+ previous_talk_video="test",
+ speaker_level="level",
+ )
+
+ response = graphql_client.query(
+ """query Participant($id: ID!, $conference: String!) {
+ participant(id: $id, conference: $conference) {
+ id
+ speakerAvailabilities
+ previousTalkVideo
+ speakerLevel
+ }
+ }
+ """,
+ variables={"id": participant.hashid, "conference": participant.conference.code},
+ )
+
+ assert response["data"]["participant"]["speakerAvailabilities"] == {"test": "test"}
+ assert response["data"]["participant"]["previousTalkVideo"] == "test"
+ assert response["data"]["participant"]["speakerLevel"] == "level"
+
+
+def test_cannot_query_private_fields_of_other_user(graphql_client):
+ participant = ParticipantFactory()
+
+ response = graphql_client.query(
+ """query Participant($id: ID!, $conference: String!) {
+ participant(id: $id, conference: $conference) {
+ id
+ speakerAvailabilities
+ previousTalkVideo
+ speakerLevel
+ }
+ }
+ """,
+ variables={"id": participant.hashid, "conference": participant.conference.code},
+ )
+
+ assert response["data"]["participant"]["speakerAvailabilities"] is None
+ assert response["data"]["participant"]["previousTalkVideo"] is None
+ assert response["data"]["participant"]["speakerLevel"] is None
+
+
+def test_query_participant_with_wrong_conference(graphql_client):
+ participant = ParticipantFactory()
+
+ response = graphql_client.query(
+ """
+ query Participant($id: ID!, $conference: String!) {
+ participant(id: $id, conference: $conference) {
+ id
+ fullname
+ }
+ }
+ """,
+ variables={"id": participant.hashid, "conference": ConferenceFactory().code},
+ )
+
+ assert response["data"]["participant"] is None
+
+
+def test_query_participant_with_non_existent_id(graphql_client):
+ response = graphql_client.query(
+ """
+ query Participant($id: ID!, $conference: String!) {
+ participant(id: $id, conference: $conference) {
+ id
+ fullname
+ }
+ }
+ """,
+ variables={"id": "abcabc", "conference": ConferenceFactory().code},
+ )
+
+ assert response["data"]["participant"] is None
diff --git a/backend/api/participants/types.py b/backend/api/participants/types.py
index c1228e6797..2d2b240483 100644
--- a/backend/api/participants/types.py
+++ b/backend/api/participants/types.py
@@ -1,16 +1,19 @@
-from typing import Optional
+from typing import TYPE_CHECKING, Annotated
+from submissions.models import Submission as SubmissionModel
from strawberry.scalars import JSON
import strawberry
from strawberry import ID
from api.submissions.permissions import CanSeeSubmissionPrivateFields
+if TYPE_CHECKING:
+ from api.submissions.types import Submission
+
@strawberry.type
class Participant:
id: ID
- user_id: ID
bio: str
website: str
photo: str | None
@@ -21,22 +24,39 @@ class Participant:
linkedin_url: str
facebook_url: str
mastodon_handle: str
- speaker_id: strawberry.Private[int]
fullname: str
- speaker_availabilities: JSON
_speaker_level: strawberry.Private[str]
_previous_talk_video: strawberry.Private[str]
+ _conference_id: strawberry.Private[int]
+ _user_id: strawberry.Private[int]
+ _speaker_availabilities: strawberry.Private[int]
+
+ @strawberry.field
+ def proposals(
+ self, info
+ ) -> list[Annotated["Submission", strawberry.lazy("api.submissions.types")]]:
+ return SubmissionModel.objects.for_conference(self._conference_id).filter(
+ speaker_id=self._user_id,
+ status=SubmissionModel.STATUS.accepted,
+ )
@strawberry.field
- def speaker_level(self, info) -> Optional[str]:
+ def speaker_availabilities(self, info) -> JSON | None:
+ if not CanSeeSubmissionPrivateFields().has_permission(self, info):
+ return None
+
+ return self._speaker_availabilities
+
+ @strawberry.field
+ def speaker_level(self, info) -> str | None:
if not CanSeeSubmissionPrivateFields().has_permission(self, info):
return None
return self._speaker_level
@strawberry.field
- def previous_talk_video(self, info) -> Optional[str]:
+ def previous_talk_video(self, info) -> str | None:
if not CanSeeSubmissionPrivateFields().has_permission(self, info):
return None
@@ -46,20 +66,20 @@ def previous_talk_video(self, info) -> Optional[str]:
def from_model(cls, instance):
return cls(
id=instance.hashid,
- user_id=instance.user_id,
- speaker_id=instance.user_id,
fullname=instance.user.fullname,
photo=instance.photo_url,
photo_id=instance.photo_file_id,
bio=instance.bio,
website=instance.website,
public_profile=instance.public_profile,
- _speaker_level=instance.speaker_level,
- _previous_talk_video=instance.previous_talk_video,
twitter_handle=instance.twitter_handle,
instagram_handle=instance.instagram_handle,
linkedin_url=instance.linkedin_url,
facebook_url=instance.facebook_url,
mastodon_handle=instance.mastodon_handle,
- speaker_availabilities=instance.speaker_availabilities or {},
+ _speaker_availabilities=instance.speaker_availabilities or {},
+ _conference_id=instance.conference_id,
+ _user_id=instance.user_id,
+ _speaker_level=instance.speaker_level,
+ _previous_talk_video=instance.previous_talk_video,
)
diff --git a/backend/api/permissions.py b/backend/api/permissions.py
index 3d4e503a69..09f213c413 100644
--- a/backend/api/permissions.py
+++ b/backend/api/permissions.py
@@ -38,12 +38,18 @@ def has_permission(self, source, info, **kwargs):
class CanSeeSubmissions(BasePermission):
message = "You need to have a ticket to see submissions"
- def has_permission(self, conference, info):
+ def has_permission(self, conference, info, *args, **kwargs):
user = info.context.request.user
+ if kwargs.get("only_accepted", False):
+ return True
+
if not user.is_authenticated:
return False
+ if info.context._user_can_vote is not None:
+ return info.context._user_can_vote
+
return check_if_user_can_vote(user, conference)
diff --git a/backend/api/schedule/types/slot.py b/backend/api/schedule/types/slot.py
index 9b402fe88a..caec44899d 100644
--- a/backend/api/schedule/types/slot.py
+++ b/backend/api/schedule/types/slot.py
@@ -16,10 +16,10 @@ class ScheduleSlotType(Enum):
@strawberry.type
class ScheduleSlot:
+ id: strawberry.ID
hour: time
duration: int
type: ScheduleSlotType
- id: strawberry.ID
@strawberry.field
def is_live(self) -> bool:
diff --git a/backend/api/submissions/permissions.py b/backend/api/submissions/permissions.py
index 837f215170..56856757bb 100644
--- a/backend/api/submissions/permissions.py
+++ b/backend/api/submissions/permissions.py
@@ -1,3 +1,4 @@
+from submissions.models import Submission
from strawberry.permission import BasePermission
from api.permissions import HasTokenPermission
@@ -13,6 +14,9 @@ def has_permission(self, source, info, **kwargs):
if HasTokenPermission().has_permission(source, info):
return True
+ if source.status == Submission.STATUS.accepted:
+ return True
+
if source.schedule_items.exists(): # pragma: no cover
return True
@@ -48,12 +52,21 @@ class CanSeeSubmissionPrivateFields(BasePermission):
message = "You can't see the private fields for this submission"
def has_permission(self, source, info):
+ from api.participants.types import Participant
+
user = info.context.request.user
if not user.is_authenticated:
return False
- return user.is_staff or source.speaker_id == user.id
+ if isinstance(source, Submission):
+ source_user_id = source.speaker_id
+ elif isinstance(source, Participant):
+ source_user_id = source._user_id
+ else:
+ raise ValueError("Invalid source type")
+
+ return user.is_staff or source_user_id == user.id
class IsSubmissionSpeakerOrStaff(BasePermission):
diff --git a/backend/api/submissions/schema.py b/backend/api/submissions/schema.py
index f9589f6e81..16370d7cf4 100644
--- a/backend/api/submissions/schema.py
+++ b/backend/api/submissions/schema.py
@@ -2,6 +2,7 @@
from api.context import Info
from api.submissions.permissions import CanSeeSubmissionRestrictedFields
+from voting.helpers import check_if_user_can_vote
import strawberry
from api.permissions import CanSeeSubmissions, IsAuthenticated
@@ -34,7 +35,7 @@ def submission(self, info: Info, id: strawberry.ID) -> Submission | None:
return submission
- @strawberry.field(permission_classes=[IsAuthenticated])
+ @strawberry.field()
def submissions(
self,
info: Info,
@@ -46,9 +47,10 @@ def submissions(
audience_levels: list[str] | None = None,
page: int | None = 1,
page_size: int | None = 50,
+ only_accepted: bool = False,
) -> Paginated[Submission] | None:
- if page_size > 150:
- raise ValueError("Page size cannot be greater than 150")
+ if page_size > 300:
+ raise ValueError("Page size cannot be greater than 300")
if page_size < 1:
raise ValueError("Page size must be greater than 0")
@@ -60,10 +62,17 @@ def submissions(
user = request.user
conference = ConferenceModel.objects.filter(code=code).first()
- if not conference or not CanSeeSubmissions().has_permission(conference, info):
- raise PermissionError("You need to have a ticket to see submissions")
+ if not only_accepted and not IsAuthenticated().has_permission(conference, info):
+ raise PermissionError("User not logged in")
- info.context._user_can_vote = True
+ info.context._user_can_vote = (
+ check_if_user_can_vote(user, conference) if user.is_authenticated else False
+ )
+
+ if not conference or not CanSeeSubmissions().has_permission(
+ conference, info, only_accepted=only_accepted
+ ):
+ raise PermissionError("You need to have a ticket to see submissions")
qs = conference.submissions.prefetch_related(
"type",
@@ -72,7 +81,12 @@ def submissions(
"languages",
"audience_level",
"tags",
- ).filter(status=SubmissionModel.STATUS.proposed)
+ )
+
+ if only_accepted:
+ qs = qs.filter(status=SubmissionModel.STATUS.accepted)
+ else:
+ qs = qs.filter(status=SubmissionModel.STATUS.proposed)
if languages:
qs = qs.filter(languages__code__in=languages)
diff --git a/backend/api/submissions/tests/test_submissions.py b/backend/api/submissions/tests/test_submissions.py
index 4f97876dda..933dae3efd 100644
--- a/backend/api/submissions/tests/test_submissions.py
+++ b/backend/api/submissions/tests/test_submissions.py
@@ -1,3 +1,4 @@
+from participants.tests.factories import ParticipantFactory
from submissions.models import Submission
from users.tests.factories import UserFactory
from voting.tests.factories.vote import VoteFactory
@@ -105,6 +106,51 @@ def test_returns_submissions_paginated(graphql_client, user):
)
+def test_accepted_submissions_are_public(graphql_client):
+ submission = SubmissionFactory(id=1, status=Submission.STATUS.accepted)
+ SubmissionFactory(
+ id=2, conference=submission.conference, status=Submission.STATUS.proposed
+ )
+ participant = ParticipantFactory(
+ user_id=submission.speaker_id, conference_id=submission.conference_id
+ )
+ ParticipantFactory(user_id=submission.speaker_id)
+
+ query = """query Submissions($code: String!, $page: Int) {
+ submissions(code: $code, page: $page, pageSize: 5, onlyAccepted: true) {
+ pageInfo {
+ totalPages
+ totalItems
+ }
+ items {
+ id
+ speaker {
+ id
+ participant {
+ id
+ }
+ }
+ }
+ }
+ }"""
+ resp = graphql_client.query(
+ query,
+ variables={"code": submission.conference.code},
+ )
+
+ assert not resp.get("errors")
+ assert len(resp["data"]["submissions"]["items"]) == 1
+ assert resp["data"]["submissions"]["items"][0]["id"] == submission.hashid
+ assert resp["data"]["submissions"]["items"][0]["speaker"]["id"] == str(
+ submission.speaker_id
+ )
+ assert (
+ resp["data"]["submissions"]["items"][0]["speaker"]["participant"]["id"]
+ == participant.hashid
+ )
+ assert resp["data"]["submissions"]["pageInfo"] == {"totalPages": 1, "totalItems": 1}
+
+
def test_canceled_submissions_are_excluded(graphql_client, user, mock_has_ticket):
graphql_client.force_login(user)
@@ -181,7 +227,7 @@ def test_max_allowed_page_size(graphql_client, user):
variables={"code": submission.conference.code},
)
- assert resp["errors"][0]["message"] == "Page size cannot be greater than 150"
+ assert resp["errors"][0]["message"] == "Page size cannot be greater than 300"
assert resp["data"]["submissions"] is None
diff --git a/backend/api/submissions/types.py b/backend/api/submissions/types.py
index 3f863240bb..86aca23917 100644
--- a/backend/api/submissions/types.py
+++ b/backend/api/submissions/types.py
@@ -1,3 +1,5 @@
+from typing import Annotated
+from participants.models import Participant as ParticipantModel
import strawberry
from strawberry.types.field import StrawberryField
from strawberry.types import Info
@@ -9,11 +11,12 @@
from voting.models import Vote
from .permissions import CanSeeSubmissionPrivateFields, CanSeeSubmissionRestrictedFields
-from typing import TYPE_CHECKING, Annotated
+from typing import TYPE_CHECKING
if TYPE_CHECKING:
from api.conferences.types import Conference, Topic, Duration, AudienceLevel
from api.schedule.types import ScheduleItem
+ from api.participants.types import Participant
def private_field() -> StrawberryField:
@@ -44,6 +47,20 @@ class SubmissionSpeaker:
id: strawberry.ID
full_name: str
gender: str
+ _conference_id: strawberry.Private[str]
+
+ @strawberry.field
+ def participant(
+ self, info: Info
+ ) -> Annotated["Participant", strawberry.lazy("api.participants.types")] | None:
+ from api.participants.types import Participant
+
+ participant = (
+ ParticipantModel.objects.for_conference(self._conference_id)
+ .filter(user_id=self.id)
+ .first()
+ )
+ return Participant.from_model(participant) if participant else None
@strawberry.type
@@ -136,6 +153,7 @@ def speaker(self, info: Info) -> SubmissionSpeaker | None:
id=self.speaker_id,
full_name=self.speaker.full_name,
gender=self.speaker.gender,
+ _conference_id=self.conference_id,
)
@strawberry.field
diff --git a/backend/api/users/tests/test_participant.py b/backend/api/users/tests/test_participant.py
index 94c77f70c5..d8d7c97e7b 100644
--- a/backend/api/users/tests/test_participant.py
+++ b/backend/api/users/tests/test_participant.py
@@ -1,5 +1,7 @@
from conferences.tests.factories import ConferenceFactory
from participants.tests.factories import ParticipantFactory
+from submissions.tests.factories import SubmissionFactory
+from submissions.models import Submission
import pytest
@@ -49,6 +51,56 @@ def test_user_participant(user, graphql_client):
assert participant_type["previousTalkVideo"] == ""
+def test_user_participant_proposals(user, graphql_client):
+ graphql_client.force_login(user)
+ participant = ParticipantFactory(
+ user_id=user.id,
+ bio="biiiiio",
+ photo="https://marcopycontest.blob.core.windows.net/participants-avatars/blob.jpg",
+ website="https://google.it",
+ twitter_handle="marco",
+ speaker_level="intermediate",
+ previous_talk_video="",
+ )
+ proposal_1 = SubmissionFactory(
+ speaker_id=participant.user_id,
+ conference=participant.conference,
+ status=Submission.STATUS.accepted,
+ )
+ SubmissionFactory(
+ speaker_id=participant.user_id,
+ conference=participant.conference,
+ status=Submission.STATUS.rejected,
+ )
+ SubmissionFactory(speaker_id=participant.user_id, status=Submission.STATUS.accepted)
+
+ response = graphql_client.query(
+ """query($conference: String!) {
+ me {
+ participant(conference: $conference) {
+ id
+ bio
+ photo
+ website
+ twitterHandle
+ speakerLevel
+ previousTalkVideo
+ proposals {
+ id
+ }
+ }
+ }
+ }""",
+ variables={"conference": participant.conference.code},
+ )
+
+ participant_type = response["data"]["me"]["participant"]
+
+ assert participant_type["id"] == participant.hashid
+ assert len(participant_type["proposals"]) == 1
+ assert participant_type["proposals"][0]["id"] == proposal_1.hashid
+
+
def test_user_participant_when_it_doesnt_exist(user, graphql_client):
graphql_client.force_login(user)
conference_code = ConferenceFactory().code
diff --git a/backend/cms/components/page/blocks/dynamic_content_display_section.py b/backend/cms/components/page/blocks/dynamic_content_display_section.py
new file mode 100644
index 0000000000..fc87d47799
--- /dev/null
+++ b/backend/cms/components/page/blocks/dynamic_content_display_section.py
@@ -0,0 +1,11 @@
+from wagtail import blocks
+
+
+class DynamicContentDisplaySection(blocks.StructBlock):
+ source = blocks.ChoiceBlock(
+ choices=[
+ ("speakers", "Speakers"),
+ ("keynoters", "Keynoters"),
+ ("proposals", "Proposals"),
+ ],
+ )
diff --git a/backend/cms/components/page/migrations/0005_alter_genericpage_body.py b/backend/cms/components/page/migrations/0005_alter_genericpage_body.py
new file mode 100644
index 0000000000..3cb98e4356
--- /dev/null
+++ b/backend/cms/components/page/migrations/0005_alter_genericpage_body.py
@@ -0,0 +1,20 @@
+# Generated by Django 5.1.4 on 2025-02-09 23:56
+
+import cms.components.base.blocks.accordion
+import wagtail.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('page', '0004_alter_genericpage_body'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='genericpage',
+ name='body',
+ field=wagtail.fields.StreamField([('text_section', 7), ('map', 11), ('slider_cards_section', 18), ('sponsors_section', 21), ('home_intro_section', 22), ('keynoters_section', 23), ('schedule_preview_section', 26), ('socials_section', 27), ('special_guest_section', 30), ('information_section', 33), ('news_grid_section', 34), ('checkout_section', 35), ('live_streaming_section', 34), ('homepage_hero', 37), ('dynamic_content_display_section', 39)], block_lookup={0: ('wagtail.blocks.CharBlock', (), {'required': False}), 1: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'required': False}), 2: ('wagtail.blocks.RichTextBlock', (), {'required': False}), 3: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('text-1', 'Text-1'), ('text-2', 'Text-2')], 'required': False}), 4: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('cathedral', 'Cathedral'), ('florence', 'Florence'), ('florence2', 'Florence2'), ('handWithSnakeInside', 'Hand With Snake Inside'), ('snake1', 'Snake1'), ('snake2', 'Snake2'), ('snake4', 'Snake4'), ('snake5', 'Snake5'), ('snakeBody', 'Snake Body'), ('snakeCouple', 'Snake Couple'), ('snakeDNA', 'Snake D N A'), ('snakeHead', 'Snake Head'), ('snakeInDragon', 'Snake In Dragon'), ('snakeInDragonInverted', 'Snake In Dragon Inverted'), ('snakeLetter', 'Snake Letter'), ('snakeLongNeck', 'Snake Long Neck'), ('snakePencil', 'Snake Pencil'), ('snakeTail', 'Snake Tail'), ('snakeWithBalloon', 'Snake With Balloon'), ('snakeWithContacts', 'Snake With Contacts'), ('snakesWithBanner', 'Snakes With Banner'), ('snakesWithCocktail', 'Snakes With Cocktail'), ('snakesWithDirections', 'Snakes With Directions'), ('snakesWithOutlines', 'Snakes With Outlines'), ('tripleSnakes', 'Triple Snakes')], 'required': False}), 5: ('wagtail.blocks.ListBlock', (cms.components.base.blocks.accordion.Accordion,), {}), 6: ('wagtail.blocks.StructBlock', [[('label', 0), ('link', 0)]], {}), 7: ('wagtail.blocks.StructBlock', [[('title', 0), ('is_main_title', 1), ('subtitle', 0), ('body', 2), ('body_text_size', 3), ('illustration', 4), ('accordions', 5), ('cta', 6)]], {}), 8: ('wagtail.blocks.DecimalBlock', (), {'decimal_places': 6, 'max_digits': 9}), 9: ('wagtail.blocks.IntegerBlock', (), {'default': 15}), 10: ('wagtail.blocks.URLBlock', (), {}), 11: ('wagtail.blocks.StructBlock', [[('longitude', 8), ('latitude', 8), ('zoom', 9), ('link', 10)]], {}), 12: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('xl', 'Extra Large'), ('3xl', '3 Extra Large')]}), 13: ('wagtail.blocks.CharBlock', (), {}), 14: ('wagtail.blocks.RichTextBlock', (), {}), 15: ('wagtail.blocks.StructBlock', [[('title', 13), ('body', 14), ('cta', 6)]], {}), 16: ('wagtail.blocks.StructBlock', [[('title', 13), ('body', 14), ('price', 13), ('price_tier', 13), ('cta', 6)]], {}), 17: ('wagtail.blocks.StreamBlock', [[('simple_text_card', 15), ('price_card', 16)]], {}), 18: ('wagtail.blocks.StructBlock', [[('title', 0), ('spacing', 12), ('snake_background', 1), ('cards', 17)]], {}), 19: ('wagtail.blocks.CharBlock', (), {'required': True}), 20: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('side-by-side', 'Side-by-side'), ('vertical', 'Vertical')], 'required': False}), 21: ('wagtail.blocks.StructBlock', [[('title', 19), ('body', 19), ('cta', 6), ('layout', 20)]], {}), 22: ('wagtail.blocks.StructBlock', [[('pretitle', 0), ('title', 0)]], {}), 23: ('wagtail.blocks.StructBlock', [[('title', 19), ('cta', 6)]], {}), 24: ('wagtail.blocks.StructBlock', [[('label', 0), ('link', 0)]], {'label': 'Primary CTA'}), 25: ('wagtail.blocks.StructBlock', [[('label', 0), ('link', 0)]], {'label': 'Secondary CTA'}), 26: ('wagtail.blocks.StructBlock', [[('title', 19), ('primary_cta', 24), ('secondary_cta', 25)]], {}), 27: ('wagtail.blocks.StructBlock', [[('label', 19), ('hashtag', 19)]], {}), 28: ('wagtail.images.blocks.ImageChooserBlock', (), {'required': True}), 29: ('wagtail.blocks.DateBlock', (), {'required': True}), 30: ('wagtail.blocks.StructBlock', [[('title', 19), ('guest_name', 19), ('guest_photo', 28), ('guest_job_title', 19), ('event_date', 29), ('cta', 6)]], {}), 31: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('coral', 'coral'), ('caramel', 'caramel'), ('cream', 'cream'), ('yellow', 'yellow'), ('green', 'green'), ('purple', 'purple'), ('pink', 'pink'), ('blue', 'blue'), ('red', 'red'), ('success', 'success'), ('warning', 'warning'), ('neutral', 'neutral'), ('error', 'error'), ('black', 'black'), ('grey', 'grey'), ('white', 'white'), ('milk', 'milk')]}), 32: ('wagtail.blocks.DateTimeBlock', (), {'required': False}), 33: ('wagtail.blocks.StructBlock', [[('title', 19), ('body', 2), ('illustration', 4), ('background_color', 31), ('countdown_to_datetime', 32), ('countdown_to_deadline', 0), ('cta', 6)]], {}), 34: ('wagtail.blocks.StructBlock', [[]], {}), 35: ('wagtail.blocks.StructBlock', [[('show_conference_tickets_products', 1), ('show_social_events_products', 1), ('show_tours_products', 1), ('show_gadgets_products', 1), ('show_membership_products', 1)]], {}), 36: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('florence', 'Florence'), ('bologna', 'Bologna')]}), 37: ('wagtail.blocks.StructBlock', [[('city', 36)]], {}), 38: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('speakers', 'Speakers'), ('keynoters', 'Keynoters'), ('accepted_proposals', 'Accepted Proposals')]}), 39: ('wagtail.blocks.StructBlock', [[('source', 38)]], {})}),
+ ),
+ ]
diff --git a/backend/cms/components/page/migrations/0006_alter_genericpage_body.py b/backend/cms/components/page/migrations/0006_alter_genericpage_body.py
new file mode 100644
index 0000000000..738401c6ee
--- /dev/null
+++ b/backend/cms/components/page/migrations/0006_alter_genericpage_body.py
@@ -0,0 +1,20 @@
+# Generated by Django 5.1.4 on 2025-02-15 17:47
+
+import cms.components.base.blocks.accordion
+import wagtail.fields
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('page', '0005_alter_genericpage_body'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='genericpage',
+ name='body',
+ field=wagtail.fields.StreamField([('text_section', 7), ('map', 11), ('slider_cards_section', 18), ('sponsors_section', 21), ('home_intro_section', 22), ('keynoters_section', 23), ('schedule_preview_section', 26), ('socials_section', 27), ('special_guest_section', 30), ('information_section', 33), ('news_grid_section', 34), ('checkout_section', 35), ('live_streaming_section', 34), ('homepage_hero', 37), ('dynamic_content_display_section', 39)], block_lookup={0: ('wagtail.blocks.CharBlock', (), {'required': False}), 1: ('wagtail.blocks.BooleanBlock', (), {'default': False, 'required': False}), 2: ('wagtail.blocks.RichTextBlock', (), {'required': False}), 3: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('text-1', 'Text-1'), ('text-2', 'Text-2')], 'required': False}), 4: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('cathedral', 'Cathedral'), ('florence', 'Florence'), ('florence2', 'Florence2'), ('handWithSnakeInside', 'Hand With Snake Inside'), ('snake1', 'Snake1'), ('snake2', 'Snake2'), ('snake4', 'Snake4'), ('snake5', 'Snake5'), ('snakeBody', 'Snake Body'), ('snakeCouple', 'Snake Couple'), ('snakeDNA', 'Snake D N A'), ('snakeHead', 'Snake Head'), ('snakeInDragon', 'Snake In Dragon'), ('snakeInDragonInverted', 'Snake In Dragon Inverted'), ('snakeLetter', 'Snake Letter'), ('snakeLongNeck', 'Snake Long Neck'), ('snakePencil', 'Snake Pencil'), ('snakeTail', 'Snake Tail'), ('snakeWithBalloon', 'Snake With Balloon'), ('snakeWithContacts', 'Snake With Contacts'), ('snakesWithBanner', 'Snakes With Banner'), ('snakesWithCocktail', 'Snakes With Cocktail'), ('snakesWithDirections', 'Snakes With Directions'), ('snakesWithOutlines', 'Snakes With Outlines'), ('tripleSnakes', 'Triple Snakes')], 'required': False}), 5: ('wagtail.blocks.ListBlock', (cms.components.base.blocks.accordion.Accordion,), {}), 6: ('wagtail.blocks.StructBlock', [[('label', 0), ('link', 0)]], {}), 7: ('wagtail.blocks.StructBlock', [[('title', 0), ('is_main_title', 1), ('subtitle', 0), ('body', 2), ('body_text_size', 3), ('illustration', 4), ('accordions', 5), ('cta', 6)]], {}), 8: ('wagtail.blocks.DecimalBlock', (), {'decimal_places': 6, 'max_digits': 9}), 9: ('wagtail.blocks.IntegerBlock', (), {'default': 15}), 10: ('wagtail.blocks.URLBlock', (), {}), 11: ('wagtail.blocks.StructBlock', [[('longitude', 8), ('latitude', 8), ('zoom', 9), ('link', 10)]], {}), 12: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('xl', 'Extra Large'), ('3xl', '3 Extra Large')]}), 13: ('wagtail.blocks.CharBlock', (), {}), 14: ('wagtail.blocks.RichTextBlock', (), {}), 15: ('wagtail.blocks.StructBlock', [[('title', 13), ('body', 14), ('cta', 6)]], {}), 16: ('wagtail.blocks.StructBlock', [[('title', 13), ('body', 14), ('price', 13), ('price_tier', 13), ('cta', 6)]], {}), 17: ('wagtail.blocks.StreamBlock', [[('simple_text_card', 15), ('price_card', 16)]], {}), 18: ('wagtail.blocks.StructBlock', [[('title', 0), ('spacing', 12), ('snake_background', 1), ('cards', 17)]], {}), 19: ('wagtail.blocks.CharBlock', (), {'required': True}), 20: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('side-by-side', 'Side-by-side'), ('vertical', 'Vertical')], 'required': False}), 21: ('wagtail.blocks.StructBlock', [[('title', 19), ('body', 19), ('cta', 6), ('layout', 20)]], {}), 22: ('wagtail.blocks.StructBlock', [[('pretitle', 0), ('title', 0)]], {}), 23: ('wagtail.blocks.StructBlock', [[('title', 19), ('cta', 6)]], {}), 24: ('wagtail.blocks.StructBlock', [[('label', 0), ('link', 0)]], {'label': 'Primary CTA'}), 25: ('wagtail.blocks.StructBlock', [[('label', 0), ('link', 0)]], {'label': 'Secondary CTA'}), 26: ('wagtail.blocks.StructBlock', [[('title', 19), ('primary_cta', 24), ('secondary_cta', 25)]], {}), 27: ('wagtail.blocks.StructBlock', [[('label', 19), ('hashtag', 19)]], {}), 28: ('wagtail.images.blocks.ImageChooserBlock', (), {'required': True}), 29: ('wagtail.blocks.DateBlock', (), {'required': True}), 30: ('wagtail.blocks.StructBlock', [[('title', 19), ('guest_name', 19), ('guest_photo', 28), ('guest_job_title', 19), ('event_date', 29), ('cta', 6)]], {}), 31: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('coral', 'coral'), ('caramel', 'caramel'), ('cream', 'cream'), ('yellow', 'yellow'), ('green', 'green'), ('purple', 'purple'), ('pink', 'pink'), ('blue', 'blue'), ('red', 'red'), ('success', 'success'), ('warning', 'warning'), ('neutral', 'neutral'), ('error', 'error'), ('black', 'black'), ('grey', 'grey'), ('white', 'white'), ('milk', 'milk')]}), 32: ('wagtail.blocks.DateTimeBlock', (), {'required': False}), 33: ('wagtail.blocks.StructBlock', [[('title', 19), ('body', 2), ('illustration', 4), ('background_color', 31), ('countdown_to_datetime', 32), ('countdown_to_deadline', 0), ('cta', 6)]], {}), 34: ('wagtail.blocks.StructBlock', [[]], {}), 35: ('wagtail.blocks.StructBlock', [[('show_conference_tickets_products', 1), ('show_social_events_products', 1), ('show_tours_products', 1), ('show_gadgets_products', 1), ('show_membership_products', 1)]], {}), 36: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('florence', 'Florence'), ('bologna', 'Bologna')]}), 37: ('wagtail.blocks.StructBlock', [[('city', 36)]], {}), 38: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('speakers', 'Speakers'), ('keynoters', 'Keynoters'), ('proposals', 'Proposals')]}), 39: ('wagtail.blocks.StructBlock', [[('source', 38)]], {})}),
+ ),
+ ]
diff --git a/backend/cms/components/page/models.py b/backend/cms/components/page/models.py
index 7beba42f32..b3bf3e9a2f 100644
--- a/backend/cms/components/page/models.py
+++ b/backend/cms/components/page/models.py
@@ -1,3 +1,6 @@
+from cms.components.page.blocks.dynamic_content_display_section import (
+ DynamicContentDisplaySection,
+)
from cms.components.page.blocks.homepage_hero import HomepageHero
from cms.components.page.blocks.sponsors_section import SponsorsSection
from cms.components.home.blocks.home_intro_section import HomeIntroSection
@@ -37,6 +40,7 @@ class BodyBlock(blocks.StreamBlock):
checkout_section = CheckoutSection()
live_streaming_section = LiveStreamingSection()
homepage_hero = HomepageHero()
+ dynamic_content_display_section = DynamicContentDisplaySection()
class GenericPage(CustomHeadlessMixin, Page):
diff --git a/backend/custom_admin/src/components/schedule-builder/item.tsx b/backend/custom_admin/src/components/schedule-builder/item.tsx
index ba6b80cdd4..19e44f10c0 100644
--- a/backend/custom_admin/src/components/schedule-builder/item.tsx
+++ b/backend/custom_admin/src/components/schedule-builder/item.tsx
@@ -1,5 +1,6 @@
import { useDrag } from "react-dnd";
+import { Button } from "@radix-ui/themes";
import { useDjangoAdminEditor } from "../shared/django-admin-editor-modal/context";
import { convertHoursToMinutes } from "../utils/time";
@@ -82,9 +83,7 @@ export const ScheduleItemCard = ({ item, duration }) => {
)}
-
+
);
diff --git a/backend/custom_admin/src/components/schedule-builder/slot-creation.tsx b/backend/custom_admin/src/components/schedule-builder/slot-creation.tsx
index 75a41e1cb9..da30c22c6b 100644
--- a/backend/custom_admin/src/components/schedule-builder/slot-creation.tsx
+++ b/backend/custom_admin/src/components/schedule-builder/slot-creation.tsx
@@ -1,3 +1,4 @@
+import { Button } from "@radix-ui/themes";
import { useCurrentConference } from "../utils/conference";
import { useCreateScheduleSlotMutation } from "./create-schedule-slot.generated";
@@ -47,9 +48,5 @@ const AddSlotButton = ({ children, duration, type, dayId }) => {
});
};
- return (
-
- );
+ return ;
};
diff --git a/frontend/package.json b/frontend/package.json
index fc90523dd5..3117f09054 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -24,7 +24,7 @@
"@graphql-codegen/typescript-operations": "^2.5.2",
"@graphql-codegen/typescript-react-apollo": "^4.3.2",
"@neshca/cache-handler": "^1.9.0",
- "@python-italia/pycon-styleguide": "0.1.201",
+ "@python-italia/pycon-styleguide": "0.1.210",
"@sentry/nextjs": "^8.45.0",
"@vercel/analytics": "^1.1.1",
"@vercel/og": "^0.6.2",
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml
index 965669b8be..a956892e37 100644
--- a/frontend/pnpm-lock.yaml
+++ b/frontend/pnpm-lock.yaml
@@ -43,8 +43,8 @@ importers:
specifier: ^1.9.0
version: 1.9.0(next@15.1.0(@babel/core@7.24.6)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0)
'@python-italia/pycon-styleguide':
- specifier: 0.1.201
- version: 0.1.201(@emotion/is-prop-valid@1.3.1)(clsx@1.2.1)(date-fns@4.1.0)(react-dom@19.0.0(react@19.0.0))(react-use@17.5.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.5.4)
+ specifier: 0.1.210
+ version: 0.1.210(@emotion/is-prop-valid@1.3.1)(clsx@1.2.1)(date-fns@4.1.0)(react-dom@19.0.0(react@19.0.0))(react-use@17.5.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.5.4)
'@sentry/nextjs':
specifier: ^8.45.0
version: 8.45.0(@opentelemetry/core@1.29.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.29.0(@opentelemetry/api@1.9.0))(next@15.1.0(@babel/core@7.24.6)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(webpack@5.95.0)
@@ -216,7 +216,7 @@ importers:
version: 1.0.0
ts-jest:
specifier: ^27.0.4
- version: 27.1.5(@babel/core@7.24.6)(@types/jest@29.5.12)(babel-jest@29.7.0(@babel/core@7.24.6))(jest@29.7.0(@types/node@14.18.63)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.5.4)))(typescript@5.5.4)
+ version: 27.1.5(@babel/core@7.24.6)(@types/jest@29.5.12)(jest@29.7.0(@types/node@14.18.63)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.5.4)))(typescript@5.5.4)
typescript:
specifier: ^5.5.4
version: 5.5.4
@@ -1557,8 +1557,8 @@ packages:
'@python-italia/eslint-config@1.0.13':
resolution: {integrity: sha512-5H+vykXms8OnVBs4nn6Yu5n1+/EfmJObIhVneMLbIwBPvzsGs6rtnSw3Fnu0e+rZip8BYj2l7QUBn82Ohvxo2Q==}
- '@python-italia/pycon-styleguide@0.1.201':
- resolution: {integrity: sha512-/+t/Ipv01DNtcyEjoYlLNIL1oglEEJ77fT5h2W9VQ9qvuIGuLx+/Q2BQyWS6WuoUXIu2xJMPHuDYk9boBj7pdA==}
+ '@python-italia/pycon-styleguide@0.1.210':
+ resolution: {integrity: sha512-dfpzw2hnPwCzzYTRt+FLm7OzBxQZQ/K70asJkkGoMf0upFnUfN1bYNH8FWdXPAEGen+ZsFTZ0epOHgQjBMLR+Q==}
peerDependencies:
clsx: ^1.1.1
date-fns: ^2.28.0
@@ -7814,7 +7814,7 @@ snapshots:
'@python-italia/eslint-config@1.0.13(next@15.1.0(@babel/core@7.24.6)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))':
dependencies:
'@typescript-eslint/eslint-plugin': 4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@4.9.5))(eslint@7.32.0)(typescript@4.9.5)
- '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.5.4)
+ '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@4.9.5)
eslint: 7.32.0
eslint-config-next: 11.1.4(eslint@7.32.0)(next@15.1.0(@babel/core@7.24.6)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@4.9.5)
eslint-config-prettier: 8.10.0(eslint@7.32.0)
@@ -7827,7 +7827,7 @@ snapshots:
- next
- supports-color
- '@python-italia/pycon-styleguide@0.1.201(@emotion/is-prop-valid@1.3.1)(clsx@1.2.1)(date-fns@4.1.0)(react-dom@19.0.0(react@19.0.0))(react-use@17.5.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.5.4)':
+ '@python-italia/pycon-styleguide@0.1.210(@emotion/is-prop-valid@1.3.1)(clsx@1.2.1)(date-fns@4.1.0)(react-dom@19.0.0(react@19.0.0))(react-use@17.5.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.5.4)':
dependencies:
clsx: 1.2.1
date-fns: 4.1.0
@@ -8383,7 +8383,7 @@ snapshots:
'@typescript-eslint/eslint-plugin@4.33.0(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@4.9.5))(eslint@7.32.0)(typescript@4.9.5)':
dependencies:
'@typescript-eslint/experimental-utils': 4.33.0(eslint@7.32.0)(typescript@4.9.5)
- '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.5.4)
+ '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@4.9.5)
'@typescript-eslint/scope-manager': 4.33.0
debug: 4.3.4
eslint: 7.32.0
@@ -8431,7 +8431,7 @@ snapshots:
dependencies:
'@typescript-eslint/scope-manager': 4.33.0
'@typescript-eslint/types': 4.33.0
- '@typescript-eslint/typescript-estree': 4.33.0(typescript@5.5.4)
+ '@typescript-eslint/typescript-estree': 4.33.0(typescript@4.9.5)
debug: 4.3.4
eslint: 7.32.0
optionalDependencies:
@@ -8439,18 +8439,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.5.4)':
- dependencies:
- '@typescript-eslint/scope-manager': 4.33.0
- '@typescript-eslint/types': 4.33.0
- '@typescript-eslint/typescript-estree': 4.33.0(typescript@5.5.4)
- debug: 4.3.4
- eslint: 7.32.0
- optionalDependencies:
- typescript: 5.5.4
- transitivePeerDependencies:
- - supports-color
-
'@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4)':
dependencies:
'@typescript-eslint/scope-manager': 6.21.0
@@ -8510,20 +8498,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/typescript-estree@4.33.0(typescript@5.5.4)':
- dependencies:
- '@typescript-eslint/types': 4.33.0
- '@typescript-eslint/visitor-keys': 4.33.0
- debug: 4.3.4
- globby: 11.1.0
- is-glob: 4.0.3
- semver: 7.6.2
- tsutils: 3.21.0(typescript@5.5.4)
- optionalDependencies:
- typescript: 5.5.4
- transitivePeerDependencies:
- - supports-color
-
'@typescript-eslint/typescript-estree@6.21.0(typescript@5.5.4)':
dependencies:
'@typescript-eslint/types': 6.21.0
@@ -9829,7 +9803,7 @@ snapshots:
eslint: 7.32.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.29.1)(eslint@7.32.0)
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
eslint-plugin-jsx-a11y: 6.8.0(eslint@7.32.0)
eslint-plugin-react: 7.34.1(eslint@7.32.0)
eslint-plugin-react-hooks: 4.6.2(eslint@7.32.0)
@@ -9875,7 +9849,7 @@ snapshots:
dependencies:
debug: 4.3.4
eslint: 7.32.0
- eslint-plugin-import: 2.29.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0)
+ eslint-plugin-import: 2.29.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
glob: 7.2.3
is-glob: 4.0.3
resolve: 1.22.8
@@ -9888,7 +9862,7 @@ snapshots:
debug: 4.3.6
enhanced-resolve: 5.17.1
eslint: 7.32.0
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
fast-glob: 3.3.2
get-tsconfig: 4.7.5
@@ -9900,7 +9874,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -9911,17 +9885,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.8.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@7.32.0):
+ eslint-module-utils@2.8.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0):
dependencies:
debug: 3.2.7
optionalDependencies:
- '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.5.4)
+ '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@4.9.5)
eslint: 7.32.0
eslint-import-resolver-node: 0.3.9
+ eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.29.1)(eslint@7.32.0)
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.29.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.5.4))(eslint@7.32.0):
+ eslint-plugin-import@2.29.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0):
dependencies:
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5
@@ -9931,7 +9906,7 @@ snapshots:
doctrine: 2.1.0
eslint: 7.32.0
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.8.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint@7.32.0)
+ eslint-module-utils: 2.8.1(@typescript-eslint/parser@4.33.0(eslint@7.32.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@2.7.1)(eslint@7.32.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
@@ -9942,7 +9917,7 @@ snapshots:
semver: 6.3.1
tsconfig-paths: 3.15.0
optionalDependencies:
- '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@5.5.4)
+ '@typescript-eslint/parser': 4.33.0(eslint@7.32.0)(typescript@4.9.5)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
@@ -9959,7 +9934,7 @@ snapshots:
doctrine: 2.1.0
eslint: 7.32.0
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@7.32.0))(eslint@7.32.0)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@7.32.0)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -12732,7 +12707,7 @@ snapshots:
dependencies:
tslib: 2.8.1
- ts-jest@27.1.5(@babel/core@7.24.6)(@types/jest@29.5.12)(babel-jest@29.7.0(@babel/core@7.24.6))(jest@29.7.0(@types/node@14.18.63)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.5.4)))(typescript@5.5.4):
+ ts-jest@27.1.5(@babel/core@7.24.6)(@types/jest@29.5.12)(jest@29.7.0(@types/node@14.18.63)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.5.4)))(typescript@5.5.4):
dependencies:
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
@@ -12747,7 +12722,6 @@ snapshots:
optionalDependencies:
'@babel/core': 7.24.6
'@types/jest': 29.5.12
- babel-jest: 29.7.0(@babel/core@7.24.6)
ts-log@2.2.5: {}
@@ -12791,11 +12765,6 @@ snapshots:
tslib: 1.14.1
typescript: 4.9.5
- tsutils@3.21.0(typescript@5.5.4):
- dependencies:
- tslib: 1.14.1
- typescript: 5.5.4
-
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
diff --git a/frontend/src/components/blocks-renderer/index.tsx b/frontend/src/components/blocks-renderer/index.tsx
index de31e1f3b0..f3a3d6ddf5 100644
--- a/frontend/src/components/blocks-renderer/index.tsx
+++ b/frontend/src/components/blocks-renderer/index.tsx
@@ -5,6 +5,7 @@ import { TextSection } from "~/components/blocks/text-section";
import type { Block } from "~/types";
import { CheckoutSection } from "../blocks/checkout-section";
+import { DynamicContentDisplaySection } from "../blocks/dynamic-content-display-section";
import { HomeIntroSection } from "../blocks/home-intro-section";
import { HomepageHero } from "../blocks/homepage-hero";
import { InformationSection } from "../blocks/information-section";
@@ -36,6 +37,7 @@ const REGISTRY: Registry = {
CheckoutSection,
LiveStreamingSection,
HomepageHero,
+ DynamicContentDisplaySection,
};
type Props = {
@@ -63,7 +65,7 @@ export const BlocksRenderer = ({ blocks, blocksProps }: Props) => {
export const blocksDataFetching = (client, blocks, language) => {
const promises = [];
- let staticProps = {};
+ const staticProps = {};
for (const block of blocks) {
const component = REGISTRY[block.__typename];
@@ -74,15 +76,14 @@ export const blocksDataFetching = (client, blocks, language) => {
const dataFetching = component.dataFetching;
if (dataFetching) {
- promises.push(...dataFetching(client, language));
+ promises.push(...dataFetching(client, language, block));
}
const getStaticProps = component.getStaticProps;
+
+ staticProps[block.id] = {};
if (getStaticProps) {
- staticProps = {
- ...staticProps,
- [block.id]: getStaticProps(block),
- };
+ staticProps[block.id] = getStaticProps(block);
}
}
diff --git a/frontend/src/components/blocks/dynamic-content-display-section/dynamic-content-display-section-proposals.graphql b/frontend/src/components/blocks/dynamic-content-display-section/dynamic-content-display-section-proposals.graphql
new file mode 100644
index 0000000000..0dd1c5e75b
--- /dev/null
+++ b/frontend/src/components/blocks/dynamic-content-display-section/dynamic-content-display-section-proposals.graphql
@@ -0,0 +1,22 @@
+#import "../../fragments/submission-accordion.graphql"
+
+query DynamicContentDisplaySectionProposals(
+ $code: String!
+ $language: String!
+) {
+ submissions(code: $code, onlyAccepted: true, pageSize: 300) {
+ items {
+ ...submissionAccordion
+ id
+ speaker {
+ id
+ fullName
+ participant {
+ id
+ photo
+ bio
+ }
+ }
+ }
+ }
+}
diff --git a/frontend/src/components/blocks/dynamic-content-display-section/index.tsx b/frontend/src/components/blocks/dynamic-content-display-section/index.tsx
new file mode 100644
index 0000000000..37f9bb3fa1
--- /dev/null
+++ b/frontend/src/components/blocks/dynamic-content-display-section/index.tsx
@@ -0,0 +1,62 @@
+import React, { Fragment } from "react";
+import {
+ DynamicContentDisplaySectionSource,
+ queryDynamicContentDisplaySectionProposals,
+ queryKeynotesSection,
+} from "~/types";
+import { KeynotersContent } from "./keynoters-content";
+import { ProposalsContent } from "./proposals-content";
+import { SpeakersContent } from "./speakers-content";
+
+export const DynamicContentDisplaySection = ({
+ source,
+}: {
+ source: string;
+}) => {
+ return (
+
+ {source === DynamicContentDisplaySectionSource.Keynoters && (
+
+ )}
+ {source === DynamicContentDisplaySectionSource.Speakers && (
+
+ )}
+ {source === DynamicContentDisplaySectionSource.Proposals && (
+
+ )}
+
+ );
+};
+
+DynamicContentDisplaySection.dataFetching = (client, language, block) => {
+ const source = block.source;
+
+ switch (source) {
+ case DynamicContentDisplaySectionSource.Keynoters: {
+ return [
+ queryKeynotesSection(client, {
+ code: process.env.conferenceCode,
+ language,
+ }),
+ ];
+ }
+ case DynamicContentDisplaySectionSource.Speakers: {
+ return [
+ queryDynamicContentDisplaySectionProposals(client, {
+ code: process.env.conferenceCode,
+ language,
+ }),
+ ];
+ }
+ case DynamicContentDisplaySectionSource.Proposals: {
+ return [
+ queryDynamicContentDisplaySectionProposals(client, {
+ code: process.env.conferenceCode,
+ language,
+ }),
+ ];
+ }
+ }
+
+ return [];
+};
diff --git a/frontend/src/components/blocks/dynamic-content-display-section/keynoters-content.tsx b/frontend/src/components/blocks/dynamic-content-display-section/keynoters-content.tsx
new file mode 100644
index 0000000000..be9bd31f5c
--- /dev/null
+++ b/frontend/src/components/blocks/dynamic-content-display-section/keynoters-content.tsx
@@ -0,0 +1,23 @@
+import { Section } from "@python-italia/pycon-styleguide";
+import React from "react";
+import { KeynotesSpeakersCards } from "~/components/keynotes-speakers-cards";
+import { useCurrentLanguage } from "~/locale/context";
+import { useKeynotesSectionQuery } from "~/types";
+
+export const KeynotersContent = () => {
+ const language = useCurrentLanguage();
+ const { data } = useKeynotesSectionQuery({
+ variables: {
+ code: process.env.conferenceCode,
+ language,
+ },
+ });
+ return (
+
+ );
+};
diff --git a/frontend/src/components/blocks/dynamic-content-display-section/proposals-content.tsx b/frontend/src/components/blocks/dynamic-content-display-section/proposals-content.tsx
new file mode 100644
index 0000000000..5b0ba78f53
--- /dev/null
+++ b/frontend/src/components/blocks/dynamic-content-display-section/proposals-content.tsx
@@ -0,0 +1,95 @@
+import {
+ CardPart,
+ Heading,
+ HorizontalStack,
+ MultiplePartsCard,
+ MultiplePartsCardCollection,
+ Section,
+ Text,
+ VerticalStack,
+} from "@python-italia/pycon-styleguide";
+import React from "react";
+import { FormattedMessage } from "react-intl";
+import { VotingCard } from "~/components/voting-card";
+import { useCurrentLanguage } from "~/locale/context";
+import { useDynamicContentDisplaySectionProposalsQuery } from "~/types";
+
+export const ProposalsContent = () => {
+ const [filterBy, setFilterBy] = React.useState(null);
+ const language = useCurrentLanguage();
+ let {
+ data: {
+ submissions: { items: submissions },
+ },
+ } = useDynamicContentDisplaySectionProposalsQuery({
+ variables: {
+ code: process.env.conferenceCode,
+ language,
+ },
+ });
+
+ if (filterBy === "talks") {
+ submissions = submissions.filter(
+ (submission) => submission.type.name.toLowerCase() === "talk",
+ );
+ } else if (filterBy === "workshops") {
+ submissions = submissions.filter(
+ (submission) => submission.type.name.toLowerCase() === "workshop",
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setFilterBy(null)}
+ >
+
+
+ setFilterBy("talks")}
+ >
+
+
+ setFilterBy("workshops")}
+ >
+
+
+
+
+
+
+
+ {submissions.map((submission) => (
+
+ ))}
+
+
+ );
+};
diff --git a/frontend/src/components/blocks/dynamic-content-display-section/speakers-content.tsx b/frontend/src/components/blocks/dynamic-content-display-section/speakers-content.tsx
new file mode 100644
index 0000000000..1d366e6609
--- /dev/null
+++ b/frontend/src/components/blocks/dynamic-content-display-section/speakers-content.tsx
@@ -0,0 +1,74 @@
+import {
+ CardPart,
+ Grid,
+ Heading,
+ Link,
+ MultiplePartsCard,
+ Section,
+ Text,
+} from "@python-italia/pycon-styleguide";
+
+import { useCurrentLanguage } from "~/locale/context";
+import { useDynamicContentDisplaySectionProposalsQuery } from "~/types";
+
+export const SpeakersContent = () => {
+ const language = useCurrentLanguage();
+ const {
+ data: {
+ submissions: { items: submissions },
+ },
+ } = useDynamicContentDisplaySectionProposalsQuery({
+ variables: {
+ code: process.env.conferenceCode,
+ language,
+ },
+ });
+
+ const submissionsBySpeaker = Object.groupBy(
+ submissions.toSorted((a, b) =>
+ a.speaker.fullName.localeCompare(b.speaker.fullName),
+ ),
+ (submission) => submission.speaker.participant.id,
+ );
+
+ return (
+
+
+ {Object.entries(submissionsBySpeaker).map(
+ ([speakerId, submissions]) => (
+
+ submission.title)
+ .join(", ")}
+ />
+
+ ),
+ )}
+
+
+ );
+};
+
+const SpeakerCard = ({ portraitUrl, speakerName, sessions }) => (
+
+
+
+
+
+ {speakerName}
+
+
+ {sessions}
+
+
+);
diff --git a/frontend/src/components/blocks/keynotes-section/index.tsx b/frontend/src/components/blocks/keynotes-section/index.tsx
index 438515defa..032c3e95a3 100644
--- a/frontend/src/components/blocks/keynotes-section/index.tsx
+++ b/frontend/src/components/blocks/keynotes-section/index.tsx
@@ -19,6 +19,7 @@ import {
useKeynotesSectionQuery,
} from "~/types";
+import { KeynotesSpeakersCards } from "~/components/keynotes-speakers-cards";
import { createHref } from "../../link";
type Props = {
@@ -33,11 +34,6 @@ export const KeynotersSection = ({ title, cta }: Props) => {
language,
},
});
- const englishText = useTranslatedMessage("global.english");
- const dateFormatter = new Intl.DateTimeFormat(language, {
- day: "numeric",
- month: "long",
- });
if (!data) {
return null;
@@ -59,30 +55,8 @@ export const KeynotersSection = ({ title, cta }: Props) => {
-
- {keynotes.map((keynote) => (
-
-
-
- ))}
-
+
+
{cta && (
diff --git a/frontend/src/components/keynotes-list-page-handler/index.tsx b/frontend/src/components/keynotes-list-page-handler/index.tsx
deleted file mode 100644
index 96960b8522..0000000000
--- a/frontend/src/components/keynotes-list-page-handler/index.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import {
- Button,
- Container,
- Grid,
- Heading,
- Link,
- Page,
- Section,
- Spacer,
- SpeakerCard,
- VerticalStack,
-} from "@python-italia/pycon-styleguide";
-import React from "react";
-import { FormattedMessage } from "react-intl";
-
-import { useTranslatedMessage } from "~/helpers/use-translated-message";
-import { useCurrentLanguage } from "~/locale/context";
-import { useKeynotesPageQuery } from "~/types";
-
-import { createHref } from "../link";
-import { MetaTags } from "../meta-tags";
-
-export const KeynotesListPageHandler = () => {
- const language = useCurrentLanguage();
- const {
- data: {
- conference: { title, subtitle, keynotes },
- },
- } = useKeynotesPageQuery({
- variables: {
- conference: process.env.conferenceCode,
- language,
- },
- });
- const englishText = useTranslatedMessage("global.english");
- const dateFormatter = new Intl.DateTimeFormat(language, {
- day: "numeric",
- month: "long",
- });
-
- return (
-
-
-
-
-
- {subtitle}
-
-
-
- {keynotes.map((keynote) => (
-
- speaker.participant.fullname)
- .join(", ")}
- />
-
- ))}
-
-
-
-
-
-
-
- );
-};
diff --git a/frontend/src/components/keynotes-speakers-cards/index.tsx b/frontend/src/components/keynotes-speakers-cards/index.tsx
new file mode 100644
index 0000000000..c1029140d2
--- /dev/null
+++ b/frontend/src/components/keynotes-speakers-cards/index.tsx
@@ -0,0 +1,58 @@
+import {
+ Button,
+ Container,
+ Heading,
+ Link,
+ Section,
+ SliderGrid,
+ Spacer,
+ SpeakerCard,
+ VerticalStack,
+} from "@python-italia/pycon-styleguide";
+import React from "react";
+import { useTranslatedMessage } from "~/helpers/use-translated-message";
+import { useCurrentLanguage } from "~/locale/context";
+import type { KeynotesSectionQueryResult } from "~/types";
+import { createHref } from "../link";
+
+export const KeynotesSpeakersCards = ({
+ keynotes,
+ justifyContent = "center",
+}: {
+ keynotes: KeynotesSectionQueryResult["data"]["conference"]["keynotes"];
+ justifyContent?: "left" | "center" | "right";
+}) => {
+ const language = useCurrentLanguage();
+ const englishText = useTranslatedMessage("global.english");
+ const dateFormatter = new Intl.DateTimeFormat(language, {
+ day: "numeric",
+ month: "long",
+ });
+
+ return (
+
+ {keynotes.map((keynote) => (
+
+
+
+ ))}
+
+ );
+};
diff --git a/frontend/src/components/public-profile-page-handler/fetch-participant.graphql b/frontend/src/components/public-profile-page-handler/fetch-participant.graphql
index c7022944e5..3202ab4849 100644
--- a/frontend/src/components/public-profile-page-handler/fetch-participant.graphql
+++ b/frontend/src/components/public-profile-page-handler/fetch-participant.graphql
@@ -1,5 +1,11 @@
-query ParticipantPublicProfile($userId: ID!, $conference: String!) {
- participant(userId: $userId, conference: $conference) {
+#import "../../fragments/schedule-item.graphql"
+
+query ParticipantPublicProfile(
+ $id: ID!
+ $conference: String!
+ $language: String!
+) {
+ participant(id: $id, conference: $conference) {
id
fullname
publicProfile
@@ -11,5 +17,24 @@ query ParticipantPublicProfile($userId: ID!, $conference: String!) {
linkedinUrl
facebookUrl
mastodonHandle
+ proposals {
+ id
+ title(language: $language)
+ type {
+ id
+ name
+ }
+ audienceLevel {
+ id
+ name
+ }
+ duration {
+ id
+ duration
+ }
+ scheduleItems {
+ ...ScheduleItemFragment
+ }
+ }
}
}
diff --git a/frontend/src/components/public-profile-page-handler/index.tsx b/frontend/src/components/public-profile-page-handler/index.tsx
index 450d602764..84339ea3d6 100644
--- a/frontend/src/components/public-profile-page-handler/index.tsx
+++ b/frontend/src/components/public-profile-page-handler/index.tsx
@@ -1,19 +1,39 @@
-import { Page, Section } from "@python-italia/pycon-styleguide";
+import {
+ CardPart,
+ Heading,
+ Link,
+ MultiplePartsCard,
+ MultiplePartsCardCollection,
+ Page,
+ Section,
+ Spacer,
+ Text,
+} from "@python-italia/pycon-styleguide";
import { useRouter } from "next/router";
-import { useParticipantPublicProfileQuery } from "~/types";
+import {
+ Participant,
+ type ParticipantPublicProfileQueryResult,
+ useParticipantPublicProfileQuery,
+} from "~/types";
+import { FormattedMessage } from "react-intl";
+import { useCurrentLanguage } from "~/locale/context";
+import { createHref } from "../link";
import { ParticipantInfoSection } from "../participant-info-section";
+import { ScheduleItemList } from "../schedule-view/schedule-list";
export const PublicProfilePageHandler = () => {
const router = useRouter();
+ const language = useCurrentLanguage();
const {
data: { participant },
} = useParticipantPublicProfileQuery({
variables: {
- userId: router.query.hashid as string,
+ id: router.query.hashid as string,
conference: process.env.conferenceCode,
+ language,
},
});
@@ -25,6 +45,60 @@ export const PublicProfilePageHandler = () => {
participant={participant}
/>
+ {participant.proposals.length > 0 && (
+
+
+
+
+
+
+ {participant.proposals.map((proposal) => (
+
+ ))}
+
+
+ )}
);
};
+
+const ProposalCard = ({
+ proposal,
+}: {
+ proposal: ParticipantPublicProfileQueryResult["data"]["participant"]["proposals"][0];
+}) => {
+ const language = useCurrentLanguage();
+ return (
+
+
+
+
+
+ , {proposal.audienceLevel.name}
+
+
+
+
+ 0
+ ? `/event/${proposal.scheduleItems[0].slug}`
+ : `/submission/${proposal.id}`,
+ locale: language,
+ })}
+ >
+
+ {proposal.title}
+
+
+
+
+ );
+};
diff --git a/frontend/src/components/schedule-event-detail/index.tsx b/frontend/src/components/schedule-event-detail/index.tsx
index 775e1d38f1..79e8b3eaff 100644
--- a/frontend/src/components/schedule-event-detail/index.tsx
+++ b/frontend/src/components/schedule-event-detail/index.tsx
@@ -21,11 +21,16 @@ import { useCurrentLanguage } from "~/locale/context";
import { Fragment } from "react";
import { TableItemHeader } from "~/components/table-item-header";
-import type { ProposalMaterial, TalkQueryResult } from "~/types";
+import type { ProposalMaterial, TalkQuery } from "~/types";
import { ParticipantInfoSection } from "../participant-info-section";
import { EventTag } from "./event-tag";
import { Sidebar } from "./sidebar";
+type Speaker = Omit<
+ TalkQuery["conference"]["talk"]["speakers"][0],
+ "__typename"
+>;
+
type Props = {
id?: string;
slug?: string;
@@ -33,10 +38,7 @@ type Props = {
eventTitle: string;
elevatorPitch?: string;
abstract?: string;
- speakers: {
- fullName: string;
- participant?: TalkQueryResult["data"]["conference"]["talk"]["speakers"][0]["participant"];
- }[];
+ speakers?: Speaker[];
tags?: string[];
language: string;
audienceLevel?: string;
diff --git a/frontend/src/components/schedule-view/events.tsx b/frontend/src/components/schedule-view/events.tsx
index d8058c0b09..7795446e96 100644
--- a/frontend/src/components/schedule-view/events.tsx
+++ b/frontend/src/components/schedule-view/events.tsx
@@ -22,13 +22,7 @@ import { useCurrentLanguage } from "~/locale/context";
import { createHref } from "../link";
import { EventTag } from "../schedule-event-detail/event-tag";
-import {
- type Item,
- ItemTypes,
- type Room,
- type Slot,
- Submission as SubmissionType,
-} from "./types";
+import type { Item, Room, Slot } from "./types";
export const getItemUrl = (item: Item) => {
if (item.linkTo) {
@@ -107,7 +101,7 @@ export const ScheduleEntry = ({
item.language.code === "en" ? "talk.language.en" : "talk.language.it",
);
const isCustomItem = item.type === "custom" || item.type === "break";
- const speakersNames = item.speakers.map((s) => s.fullName).join(", ");
+ const speakersNames = item.speakers.map((s) => s.fullname).join(", ");
const allRoomsText = useTranslatedMessage("scheduleView.allRooms");
const roomText =
@@ -259,9 +253,9 @@ export const ScheduleEntry = ({
{item.speakers.map((speaker) => (
`${acc} ${speaker.fullName}`, "")
+ .reduce((acc, speaker) => `${acc} ${speaker.fullname}`, "")
.toLowerCase();
if (
diff --git a/frontend/src/components/schedule-view/schedule-list.tsx b/frontend/src/components/schedule-view/schedule-list.tsx
index c56eb0aa46..ad5dfc12e7 100644
--- a/frontend/src/components/schedule-view/schedule-list.tsx
+++ b/frontend/src/components/schedule-view/schedule-list.tsx
@@ -57,7 +57,7 @@ export const ScheduleList = ({
}
return (
- 0 && (
{item.speakers
- .map((speaker) => speaker.fullName)
+ .map((speaker) => speaker.fullname)
.join(", ")}
)}
diff --git a/frontend/src/components/schedule-view/types.ts b/frontend/src/components/schedule-view/types.ts
index 3c570d3080..88800243a2 100644
--- a/frontend/src/components/schedule-view/types.ts
+++ b/frontend/src/components/schedule-view/types.ts
@@ -1,69 +1,5 @@
-export const ItemTypes = {
- ALL_TRACKS_EVENT: "all_tracks_event",
- TRAINING: "training",
- TALK: "talk",
- CUSTOM: "custom",
- KEYNOTE: "keynote",
- ROOM_CHANGE: "room_change",
-};
+import type { ScheduleQuery } from "~/types";
-export type Submission = {
- id: string;
- title: string;
- type?: { name: string } | null;
- duration?: { duration: number } | null;
- audienceLevel?: { name: string; id: string } | null;
- speaker?: { fullName: string } | null;
- tags?: { name: string }[] | null;
-};
-
-export type Keynote = {
- id: string;
- title: string;
- slug: string;
- speakers: ({ fullName: string } | null)[];
-};
-
-type Participant = {
- photo?: string;
-};
-
-export type Item = {
- id: string;
- title: string;
- slug: string;
- language: { code: string };
- type: string;
- rooms: Room[];
- linkTo: string;
- duration?: number | null;
- submission?: Submission | null;
- keynote?: Keynote | null;
- audienceLevel?: { name: string; id: string } | null;
- speakers: { fullName: string; participant?: Participant }[];
- hasLimitedCapacity: boolean;
- userHasSpot: boolean;
- hasSpacesLeft: boolean;
- spacesLeft: number;
-};
-
-export type Slot = {
- id: string;
- duration: number;
- hour: string;
- endHour: string;
- type: "DEFAULT" | "FREE_TIME" | "BREAK";
- items: Item[];
-};
-
-export type ScheduleItem = {
- title: string;
- trackSpan?: number;
- allTracks?: boolean;
-};
-
-export type Room = {
- id: string;
- name: string;
- type: string;
-};
+export type Slot = ScheduleQuery["conference"]["days"][0]["slots"][0];
+export type Item = Slot["items"][0];
+export type Room = ScheduleQuery["conference"]["days"][0]["rooms"][0];
diff --git a/frontend/src/components/submission/submission.graphql b/frontend/src/components/submission/submission.graphql
index 32b6dd7551..2219becc2e 100644
--- a/frontend/src/components/submission/submission.graphql
+++ b/frontend/src/components/submission/submission.graphql
@@ -28,5 +28,20 @@ query Submission($id: ID!, $language: String!) {
id
name
}
+ speaker {
+ id
+ fullName
+ participant {
+ id
+ photo
+ bio
+ twitterHandle
+ instagramHandle
+ linkedinUrl
+ facebookUrl
+ mastodonHandle
+ website
+ }
+ }
}
}
diff --git a/frontend/src/components/voting-card/index.tsx b/frontend/src/components/voting-card/index.tsx
index 1f4e8c9fae..c39878fe63 100644
--- a/frontend/src/components/voting-card/index.tsx
+++ b/frontend/src/components/voting-card/index.tsx
@@ -17,10 +17,12 @@ import { type SubmissionAccordionFragment, useSendVoteMutation } from "~/types";
type Props = {
submission: SubmissionAccordionFragment;
+ showVotingUI?: boolean;
};
export const VotingCard = ({
submission,
+ showVotingUI = true,
submission: {
id,
title,
@@ -29,6 +31,7 @@ export const VotingCard = ({
audienceLevel,
duration,
languages,
+ speaker,
},
}: Props) => {
const [sendVote, { loading, error, data: submissionData }] =
@@ -99,42 +102,44 @@ export const VotingCard = ({
>
{title}
-
- ,
- },
- {
- value: 2,
- label: ,
- },
- {
- value: 3,
- label: ,
- },
- {
- value: 4,
- label: ,
- },
- ]}
- value={submission?.myVote?.value}
- onClick={onSubmitVote}
- />
-
-
- {error?.message}
- {submissionData &&
- submissionData.sendVote.__typename === "SendVoteErrors" && (
- <>
- {submissionData.sendVote.errors.nonFieldErrors}{" "}
- {submissionData.sendVote.errors.validationSubmission}{" "}
- {submissionData.sendVote.errors.validationValue}
- >
- )}
-
-
+ {showVotingUI && (
+
+ ,
+ },
+ {
+ value: 2,
+ label: ,
+ },
+ {
+ value: 3,
+ label: ,
+ },
+ {
+ value: 4,
+ label: ,
+ },
+ ]}
+ value={submission?.myVote?.value}
+ onClick={onSubmitVote}
+ />
+
+
+ {error?.message}
+ {submissionData &&
+ submissionData.sendVote.__typename === "SendVoteErrors" && (
+ <>
+ {submissionData.sendVote.errors.nonFieldErrors}{" "}
+ {submissionData.sendVote.errors.validationSubmission}{" "}
+ {submissionData.sendVote.errors.validationValue}
+ >
+ )}
+
+
+ )}
@@ -211,7 +216,21 @@ export const VotingCard = ({
-
+ {speaker && (
+
+
+
+
+
+
+
+
+ {speaker.fullName}
+
+
+
+ )}
+
diff --git a/frontend/src/fragments/submission-accordion.graphql b/frontend/src/fragments/submission-accordion.graphql
index ac0cd98cd4..cac28a72f6 100644
--- a/frontend/src/fragments/submission-accordion.graphql
+++ b/frontend/src/fragments/submission-accordion.graphql
@@ -33,4 +33,9 @@ fragment submissionAccordion on Submission {
id
value
}
+
+ speaker {
+ id
+ fullName
+ }
}
diff --git a/frontend/src/helpers/get-color-for-submission.ts b/frontend/src/helpers/get-color-for-submission.ts
index fc0a25ed80..44bc1c20fe 100644
--- a/frontend/src/helpers/get-color-for-submission.ts
+++ b/frontend/src/helpers/get-color-for-submission.ts
@@ -1,11 +1,4 @@
-import type { Keynote, Submission } from "~/components/schedule-view/types";
-
-type Item = {
- id: string;
- submission?: Submission | null;
- keynote?: Keynote | null;
- audienceLevel?: { name: string } | null;
-};
+import type { Item } from "~/components/schedule-view/types";
const COLOR_MAP = {
beginner: "blue",
diff --git a/frontend/src/locale/index.ts b/frontend/src/locale/index.ts
index 0624d60b9a..66c22a2c9a 100644
--- a/frontend/src/locale/index.ts
+++ b/frontend/src/locale/index.ts
@@ -11,6 +11,8 @@ export const messages = {
"We have groups discounts for groups with 6+ people, contact us to know more. After you have purchased your tickets, you will be able to access the {page} with our discounted codes.",
"tickets.description.page": "hotels page",
+ "global.sessions": "Sessions",
+
"input.placeholder": "Type here...",
"global.accordion.close": "Close",
"global.accordion.readMore": "Read more",
@@ -24,6 +26,11 @@ export const messages = {
"checkout.billing.businessInvoice.description":
"Turn this option on if your company, university or similar is paying for you or if you need an invoice.",
+ "voting.talks": "Talks",
+ "voting.workshops": "Workshops",
+ "voting.filterBy": "Filter by",
+ "voting.all": "All",
+
"sponsorLeadModal.title": "Download our brochure",
"sponsorLeadModal.submit": "Submit",
"sponsorLeadModal.body": `Our packages gives you an idea of what we offer and how you can optimize your presence at PyCon Italia, but are not set in stone!
@@ -894,6 +901,7 @@ reflects what everyone wants to see!`,
"voting.audienceLevel": "Audience level",
"talk.audienceLevel": "Audience level",
"voting.length": "Length",
+ "voting.speaker": "Speaker",
"voting.languages": "Languages",
"voting.minutes": "{type} ({duration} minutes)",
"voting.submissionType": "Category",
@@ -1935,6 +1943,12 @@ Affrettati a comprare il biglietto!`,
"scheduleEventDetail.materials.open": "Apri ({hostname})",
"scheduleEventDetail.materials.download": "Scarica ({mimeType})",
+ "voting.speaker": "Speaker",
+ "voting.talks": "Talks",
+ "voting.workshops": "Workshops",
+
+ "global.sessions": "Sessioni",
+
"scheduleEventDetail.eventTime": "{start} - {end}",
"voting.minutes": "{type} ({duration} minuti)",
"voting.pagination":
@@ -2272,6 +2286,9 @@ Clicca sulla casella per cambiare. Se lasciato vuoto, presumeremo che tu sia dis
"Il form aprirà il {date}. Segui i nostri socials per aggiornamenti e cambiamenti.",
"requestInvitationLetter.formClosed":
"Il form è chiuso. Se hai domande, contattaci.",
+
+ "voting.filterBy": "Filtra per",
+ "voting.all": "Tutti",
},
};
diff --git a/frontend/src/pages/keynotes/index.tsx b/frontend/src/pages/keynotes/index.tsx
deleted file mode 100644
index 85612984f1..0000000000
--- a/frontend/src/pages/keynotes/index.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { GetStaticProps } from "next";
-
-import { addApolloState, getApolloClient } from "~/apollo/client";
-import { prefetchSharedQueries } from "~/helpers/prefetch";
-import { queryKeynotesPage } from "~/types";
-
-export const getStaticProps: GetStaticProps = async ({ locale }) => {
- const client = getApolloClient();
-
- await Promise.all([
- prefetchSharedQueries(client, locale),
- queryKeynotesPage(client, {
- conference: process.env.conferenceCode,
- language: locale,
- }),
- ]);
-
- return addApolloState(client, {
- props: {},
- });
-};
-
-export { KeynotesListPageHandler as default } from "~/components/keynotes-list-page-handler";
diff --git a/frontend/src/pages/keynotes/keynotes-page.graphql b/frontend/src/pages/keynotes/keynotes-page.graphql
deleted file mode 100644
index 558bf64e6e..0000000000
--- a/frontend/src/pages/keynotes/keynotes-page.graphql
+++ /dev/null
@@ -1,27 +0,0 @@
-query KeynotesPage($conference: String!, $language: String!) {
- conference(code: $conference) {
- id
- title: copy(key: "keynotes-title", language: $language)
- subtitle: copy(key: "keynotes-subtitle", language: $language)
- description: copy(key: "keynotes-description", language: $language)
- keynotes {
- id
- title(language: $language)
- slug(language: $language)
- start
- speakers {
- id
- participant {
- id
- photo
- fullname
- twitterHandle
- instagramHandle
- linkedinUrl
- facebookUrl
- mastodonHandle
- }
- }
- }
- }
-}
diff --git a/frontend/src/pages/profile/[hashid].tsx b/frontend/src/pages/profile/[hashid].tsx
index 827df42d10..c91d625a25 100644
--- a/frontend/src/pages/profile/[hashid].tsx
+++ b/frontend/src/pages/profile/[hashid].tsx
@@ -16,7 +16,8 @@ export const getServerSideProps: GetServerSideProps = async ({
prefetchSharedQueries(client, locale),
queryParticipantPublicProfile(client, {
conference: process.env.conferenceCode,
- userId: params.hashid as string,
+ id: params.hashid as string,
+ language: locale,
}),
]);
diff --git a/frontend/src/pages/schedule/fragments/blocks.graphql b/frontend/src/pages/schedule/fragments/blocks.graphql
index b1d298d8e7..d96579185f 100644
--- a/frontend/src/pages/schedule/fragments/blocks.graphql
+++ b/frontend/src/pages/schedule/fragments/blocks.graphql
@@ -130,6 +130,11 @@ fragment Blocks on Block {
id
city
}
+
+ ... on DynamicContentDisplaySection {
+ id
+ source
+ }
}
fragment CTAInfo on CTA {
diff --git a/frontend/src/pages/schedule/fragments/schedule-item.graphql b/frontend/src/pages/schedule/fragments/schedule-item.graphql
new file mode 100644
index 0000000000..7b7f632b13
--- /dev/null
+++ b/frontend/src/pages/schedule/fragments/schedule-item.graphql
@@ -0,0 +1,48 @@
+fragment ScheduleItemFragment on ScheduleItem {
+ id
+ title
+ slug
+ type
+
+ duration
+
+ hasLimitedCapacity
+ userHasSpot
+ hasSpacesLeft
+ spacesLeft
+ linkTo
+
+ audienceLevel {
+ id
+ name
+ }
+
+ language {
+ id
+ name
+ code
+ }
+
+ submission {
+ ...SubmissionFragment
+ }
+
+ keynote {
+ ...KeynoteFragment
+ }
+
+ speakers {
+ id
+ fullname
+ participant {
+ id
+ photo
+ }
+ }
+
+ rooms {
+ id
+ name
+ type
+ }
+}
diff --git a/frontend/src/pages/schedule/fragments/schedule-slot.graphql b/frontend/src/pages/schedule/fragments/schedule-slot.graphql
index c49ced0223..5be4bbc94f 100644
--- a/frontend/src/pages/schedule/fragments/schedule-slot.graphql
+++ b/frontend/src/pages/schedule/fragments/schedule-slot.graphql
@@ -1,5 +1,6 @@
#import "./submission.graphql"
#import "./keynote.graphql"
+#import "./schedule-item.graphql"
fragment ScheduleSlotFragment on ScheduleSlot {
id
@@ -9,51 +10,6 @@ fragment ScheduleSlotFragment on ScheduleSlot {
type
items {
- id
- title
- slug
- type
-
- duration
-
- hasLimitedCapacity
- userHasSpot
- hasSpacesLeft
- spacesLeft
- linkTo
-
- audienceLevel {
- id
- name
- }
-
- language {
- id
- name
- code
- }
-
- submission {
- ...SubmissionFragment
- }
-
- keynote {
- ...KeynoteFragment
- }
-
- speakers {
- id
- fullName
- participant {
- id
- photo
- }
- }
-
- rooms {
- id
- name
- type
- }
+ ...ScheduleItemFragment
}
}
diff --git a/frontend/src/pages/submission/[id]/index.tsx b/frontend/src/pages/submission/[id]/index.tsx
index ddce6ebda7..8d17fb947b 100644
--- a/frontend/src/pages/submission/[id]/index.tsx
+++ b/frontend/src/pages/submission/[id]/index.tsx
@@ -76,7 +76,7 @@ export const SubmissionPage = () => {
audienceLevel={submission?.audienceLevel.name}
startTime={null}
endTime={null}
- speakers={[]}
+ speakers={submission?.speaker ? [submission.speaker] : null}
bookable={false}
spacesLeft={0}
sidebarExtras={