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 }) => ( + + + speaker portrait + + + {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 ( - - -
    - {title} -
    -
    - - {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={