Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/cms/config/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ const config = ({
config: {
endpoint: "/graphql",
shadowCRUD: true,
// graphql-depth-limit@1.1.0 crashes on fragment spreads within
// dynamic-zone unions (reads .kind on undefined nodes). Set high
// to avoid the crash path in the library's recursive traversal.
depthLimit: 100,
Comment thread
Kneesal marked this conversation as resolved.
landingPage: env("NODE_ENV") !== "production",
generateArtifacts: true,
artifacts: {
Expand Down
24 changes: 22 additions & 2 deletions apps/cms/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions apps/cms/scripts/seed-easter.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,12 @@ async function main() {
],
}

const quizButtonBlock = {
__component: "sections.quiz-button",
buttonText: "What's your next step of faith?",
iframeSrc: "https://your.nextstep.is/embed/easter2025?expand=false",
}

const sectionBlock = {
__component: "sections.section",
sectionKey: "easter-meaning",
Expand All @@ -358,6 +364,7 @@ async function main() {
easterExplainedBlock,
textAndQuestionsContainer,
bibleQuotesBlock,
quizButtonBlock,
],
}

Expand Down
20 changes: 20 additions & 0 deletions apps/cms/src/components/sections/quiz-button.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"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,
"regex": "^https://[\\w.-]+\\.nextstep\\.is/.*$"
}
}
}
3 changes: 2 additions & 1 deletion apps/cms/src/components/sections/section.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"sections.related-questions",
"sections.bible-quotes-carousel",
"sections.card",
"sections.video"
"sections.video",
"sections.quiz-button"
],
"required": false
}
Expand Down
21 changes: 21 additions & 0 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
}
}
78 changes: 78 additions & 0 deletions apps/web/src/components/sections/QuizButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use client"

import { type ReactElement, useState } from "react"
import { Loader2, XIcon } from "lucide-react"
import { Dialog, DialogClose, DialogContent } from "@/components/ui/dialog"

type QuizButtonData = {
id: string
buttonText: string
iframeSrc: string
}

type QuizButtonProps = {
data: QuizButtonData
}

export function QuizButton({ data }: QuizButtonProps): ReactElement {
const { buttonText, iframeSrc } = data
const [open, setOpen] = useState(false)

return (
<>
<div className="mx-auto w-full px-6 pt-12 sm:w-auto lg:w-1/2 lg:px-8 xl:w-1/2 2xl:w-2xl">
<button
onClick={() => setOpen(true)}
className="animate-mesh-gradient hover:animate-mesh-gradient-fast group relative w-full overflow-hidden rounded-lg bg-linear-to-tr from-yellow-500 via-amber-500 to-red-700 bg-size-[400%_400%] bg-blend-multiply text-white shadow-lg hover:bg-orange-500"
aria-label="Open faith quiz"
type="button"
>
<div className="flex cursor-pointer items-center justify-between p-4 xl:p-6">
<div className="absolute inset-0 bg-[url(/assets/overlay.svg)] bg-repeat opacity-50 mix-blend-multiply" />
<div className="relative z-1 flex w-full items-center leading-[1.2] font-semibold md:text-xl xl:text-2xl">
<span className="mr-4 flex-none rounded-lg border-2 border-white px-2 py-1 text-xs font-extrabold tracking-wider uppercase">
Quiz
</span>
<div className="flex-auto text-center">{buttonText}</div>
</div>
<span className="transition">
<svg fill="none" height="24" width="24" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 5l7 7m0 0l-7 7m7-7H6"
/>
</svg>
</span>
</div>
</button>
</div>

<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
overlayClassName="bg-black/80 backdrop-blur-sm"
showCloseButton={false}
className="top-0 left-0 h-dvh w-dvw max-w-none translate-x-0 translate-y-0 gap-0 rounded-none border-0 bg-transparent p-2 pt-14 ring-0 sm:max-w-none md:p-14 md:pt-0"
>
<DialogClose className="absolute top-4 right-4 z-10 rounded-full p-2 text-white transition-colors hover:bg-white/20 [&_svg]:size-8">
<XIcon />
<span className="sr-only">Close</span>
</DialogClose>
<div className="absolute inset-0 -z-1 flex items-center justify-center">
<div className="scale-200 text-white">
<Loader2 className="animate-spin" />
</div>
</div>
<iframe
src={iframeSrc}
sandbox="allow-forms allow-scripts allow-same-origin"
referrerPolicy="strict-origin-when-cross-origin"
className="z-1 h-full w-full border-0"
title="Next Step of Faith Quiz"
/>
</DialogContent>
</Dialog>
</>
)
}
13 changes: 13 additions & 0 deletions apps/web/src/components/sections/Section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { BibleQuotesCarousel } from "./BibleQuotesCarousel"
import { Container } from "./Container"
import { DynamicBackground } from "./DynamicBackground"
import { MediaCollection } from "./MediaCollection"
import { QuizButton } from "./QuizButton"
import { RelatedQuestions } from "./RelatedQuestions"
import { Video } from "./Video"

Expand Down Expand Up @@ -145,6 +146,18 @@ function SectionContentRenderer({ item }: { item: SectionContentItem }) {
data={item as unknown as FragmentOf<typeof mediaCollectionFragment>}
/>
)
case "ComponentSectionsQuizButton":
return (
<QuizButton
data={
item as unknown as {
id: string
buttonText: string
iframeSrc: string
}
}
/>
)
default: {
if (process.env.NODE_ENV === "development") {
console.warn("[Section] Unhandled content type:", typename)
Expand Down
158 changes: 158 additions & 0 deletions apps/web/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"use client"

import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"

function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}

function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}

function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}

function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}

function DialogOverlay({
className,
...props
}: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className,
)}
{...props}
/>
)
}

function DialogContent({
className,
children,
showCloseButton = true,
overlayClassName,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean
overlayClassName?: string
}) {
return (
<DialogPortal>
<DialogOverlay className={overlayClassName} />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-background p-4 text-sm ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
/>
}
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
)
}

function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}

function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close render={<Button variant="outline" />}>
Close
</DialogPrimitive.Close>
)}
</div>
)
}

function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-base leading-none font-medium", className)}
{...props}
/>
)
}

function DialogDescription({
className,
...props
}: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className,
)}
{...props}
/>
)
}

export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}
Loading
Loading