From 15b6891146788e33d0cf102d82c4611219d49665 Mon Sep 17 00:00:00 2001 From: Kneesal Date: Fri, 13 Mar 2026 11:50:45 +1300 Subject: [PATCH 01/16] feat(cms): add quiz-button section component schema Add quiz-button component with buttonText and iframeSrc attributes. Register it in the section content dynamic zone and update the GraphQL schema with the new type and union memberships. Made-with: Cursor --- apps/cms/schema.graphql | 24 +++++++++++++++++-- .../src/components/sections/quiz-button.json | 19 +++++++++++++++ apps/cms/src/components/sections/section.json | 3 ++- 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 apps/cms/src/components/sections/quiz-button.json diff --git a/apps/cms/schema.graphql b/apps/cms/schema.graphql index f50643c3..940ee6c7 100644 --- a/apps/cms/schema.graphql +++ b/apps/cms/schema.graphql @@ -392,6 +392,26 @@ input ComponentSectionsPromoBannerInput { widthPercent: Int } +type ComponentSectionsQuizButton { + buttonText: String! + id: ID! + iframeSrc: String! +} + +input ComponentSectionsQuizButtonFiltersInput { + and: [ComponentSectionsQuizButtonFiltersInput] + buttonText: StringFilterInput + iframeSrc: StringFilterInput + not: ComponentSectionsQuizButtonFiltersInput + or: [ComponentSectionsQuizButtonFiltersInput] +} + +input ComponentSectionsQuizButtonInput { + buttonText: String + id: ID + iframeSrc: String +} + type ComponentSectionsRelatedQuestionItem { answer: String! id: ID! @@ -787,7 +807,7 @@ input FloatFilterInput { startsWith: Float } -union GenericMorph = ComponentSectionsBibleQuoteItem | ComponentSectionsBibleQuotesCarousel | ComponentSectionsCard | ComponentSectionsContainer | ComponentSectionsContainerSlot | ComponentSectionsCta | ComponentSectionsEasterDates | ComponentSectionsInfoBlock | ComponentSectionsInfoBlocks | ComponentSectionsMediaCollection | ComponentSectionsMediaCollectionItem | ComponentSectionsPromoBanner | ComponentSectionsRelatedQuestionItem | ComponentSectionsRelatedQuestions | ComponentSectionsSection | ComponentSectionsText | ComponentSectionsVideo | ComponentSectionsVideoHero | Experience | I18NLocale | ReviewWorkflowsWorkflow | ReviewWorkflowsWorkflowStage | UploadFile | UsersPermissionsPermission | UsersPermissionsRole | UsersPermissionsUser | Video +union GenericMorph = ComponentSectionsBibleQuoteItem | ComponentSectionsBibleQuotesCarousel | ComponentSectionsCard | ComponentSectionsContainer | ComponentSectionsContainerSlot | ComponentSectionsCta | ComponentSectionsEasterDates | ComponentSectionsInfoBlock | ComponentSectionsInfoBlocks | ComponentSectionsMediaCollection | ComponentSectionsMediaCollectionItem | ComponentSectionsPromoBanner | ComponentSectionsQuizButton | ComponentSectionsRelatedQuestionItem | ComponentSectionsRelatedQuestions | ComponentSectionsSection | ComponentSectionsText | ComponentSectionsVideo | ComponentSectionsVideoHero | Experience | I18NLocale | ReviewWorkflowsWorkflow | ReviewWorkflowsWorkflowStage | UploadFile | UsersPermissionsPermission | UsersPermissionsRole | UsersPermissionsUser | Video type I18NLocale { code: String @@ -1190,7 +1210,7 @@ type ReviewWorkflowsWorkflowStageRelationResponseCollection { nodes: [ReviewWorkflowsWorkflowStage!]! } -union SectionContentDynamicZone = ComponentSectionsBibleQuotesCarousel | ComponentSectionsCard | ComponentSectionsContainer | ComponentSectionsCta | ComponentSectionsInfoBlocks | ComponentSectionsMediaCollection | ComponentSectionsPromoBanner | ComponentSectionsRelatedQuestions | ComponentSectionsText | ComponentSectionsVideo | Error +union SectionContentDynamicZone = ComponentSectionsBibleQuotesCarousel | ComponentSectionsCard | ComponentSectionsContainer | ComponentSectionsCta | ComponentSectionsInfoBlocks | ComponentSectionsMediaCollection | ComponentSectionsPromoBanner | ComponentSectionsQuizButton | ComponentSectionsRelatedQuestions | ComponentSectionsText | ComponentSectionsVideo | Error scalar SectionContentDynamicZoneInput diff --git a/apps/cms/src/components/sections/quiz-button.json b/apps/cms/src/components/sections/quiz-button.json new file mode 100644 index 00000000..3874fcb9 --- /dev/null +++ b/apps/cms/src/components/sections/quiz-button.json @@ -0,0 +1,19 @@ +{ + "collectionName": "components_sections_quiz_buttons", + "info": { + "displayName": "Quiz Button", + "icon": "question-circle", + "description": "Interactive quiz button that opens a modal with an embedded quiz iframe" + }, + "options": {}, + "attributes": { + "buttonText": { + "type": "string", + "required": true + }, + "iframeSrc": { + "type": "string", + "required": true + } + } +} diff --git a/apps/cms/src/components/sections/section.json b/apps/cms/src/components/sections/section.json index a464afd9..49aa8cec 100644 --- a/apps/cms/src/components/sections/section.json +++ b/apps/cms/src/components/sections/section.json @@ -43,7 +43,8 @@ "sections.related-questions", "sections.bible-quotes-carousel", "sections.card", - "sections.video" + "sections.video", + "sections.quiz-button" ], "required": false } From 281baf140e43655e7fb3c620aa45f10adb58bd56 Mon Sep 17 00:00:00 2001 From: Kneesal Date: Fri, 13 Mar 2026 11:50:52 +1300 Subject: [PATCH 02/16] feat(web): add QuizButton section component with modal Create the QuizButton component with animated gradient button that opens a fullscreen modal with an embedded iframe. Wire the GraphQL fragment, register in section renderer, and add mesh gradient keyframe animation to globals.css. Made-with: Cursor --- apps/web/src/app/globals.css | 21 ++++ .../src/components/sections/QuizButton.tsx | 108 ++++++++++++++++++ apps/web/src/components/sections/Section.tsx | 8 ++ apps/web/src/lib/fragments/index.ts | 1 + .../src/lib/fragments/quiz-button-section.ts | 9 ++ apps/web/src/lib/fragments/section.ts | 3 + packages/graphql/src/graphql-env.d.ts | 7 +- 7 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/sections/QuizButton.tsx create mode 100644 apps/web/src/lib/fragments/quiz-button-section.ts diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index 36a1b614..bec9cc21 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -133,3 +133,24 @@ @apply font-sans; } } + +@layer utilities { + .animate-mesh-gradient { + animation: mesh-gradient 6s ease infinite; + } + .animate-mesh-gradient-fast { + animation: mesh-gradient 2s ease infinite; + } +} + +@keyframes mesh-gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} diff --git a/apps/web/src/components/sections/QuizButton.tsx b/apps/web/src/components/sections/QuizButton.tsx new file mode 100644 index 00000000..c1898986 --- /dev/null +++ b/apps/web/src/components/sections/QuizButton.tsx @@ -0,0 +1,108 @@ +"use client" + +import { type ReactElement, useCallback, useState } from "react" +import type { FragmentOf } from "@forge/graphql" +import { quizButtonSectionFragment } from "@/lib/fragments/quiz-button-section" + +export { quizButtonSectionFragment } + +type QuizButtonProps = { + data: FragmentOf +} + +export function QuizButton({ data }: QuizButtonProps): ReactElement { + const { buttonText, iframeSrc } = data + const [modalOpen, setModalOpen] = useState(false) + + const handleOpen = useCallback(() => setModalOpen(true), []) + const handleClose = useCallback(() => setModalOpen(false), []) + + return ( + <> +
+ +
+ + {modalOpen && } + + ) +} + +function QuizModal({ + iframeSrc, + onClose, +}: { + iframeSrc: string + onClose: () => void +}): ReactElement { + return ( +
+
{ + if (e.key === "Enter" || e.key === " ") onClose() + }} + role="button" + tabIndex={0} + aria-label="Close quiz" + /> +
+ +