diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7995913..bd63f3a 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -59,4 +59,4 @@ jobs:
npm run start &
npm run test
env:
- ORY_KRATOS_URL: http://localhost:4455
+ ORY_KRATOS_URL: http://localhost:4433
diff --git a/cypress/integration/pages.spec.js b/cypress/integration/pages.spec.js
index dae8152..2e256ec 100644
--- a/cypress/integration/pages.spec.js
+++ b/cypress/integration/pages.spec.js
@@ -15,20 +15,10 @@ context("Ory Kratos pages", () => {
cy.get('[name="method"]').should("exist")
})
- it("can load the registration page", () => {
+ it("can load the registration page if eligibile", () => {
+ sessionStorage.setItem("eligible", "")
cy.visit("/registration")
- cy.get('[name="traits.email"]').type(email)
- cy.get('[name="password"]').type(password)
- cy.get('[name="method"]').click()
- cy.location("pathname").should("eq", "/verification")
-
- cy.visit("/")
- cy.get('[data-testid="logout"]').should(
- "have.attr",
- "aria-disabled",
- "false",
- )
- cy.get('[data-testid="session-content"]').should("contain.text", email)
+ cy.get('[name="traits.email"]').should("exist")
})
it("can load the verification page", () => {
diff --git a/data/eligibility-questionnaire.ts b/data/eligibility-questionnaire.ts
index 1b4204e..cc9a47b 100644
--- a/data/eligibility-questionnaire.ts
+++ b/data/eligibility-questionnaire.ts
@@ -1,87 +1,86 @@
export const eligibilityQuestions = [
- {
- field_name: "age",
- form_name: "eligibility",
- section_header: "",
- field_type: "text",
- field_label: "How old are you?",
- select_choices_or_calculations: "",
- field_note: "",
- text_validation_type_or_show_slider_number: "",
- text_validation_min: "",
- text_validation_max: "",
- identifier: "",
- branching_logic: "",
- required_field: "yes",
- custom_alignment: "",
- question_number: "",
- matrix_group_name: "",
- matrix_ranking: "",
- field_annotation: "",
- evaluated_logic: ""
- },
- {
- field_name: "city",
- form_name: "eligibility",
- section_header: "",
- field_type: "text",
- field_label: "What city do you live in?",
- select_choices_or_calculations: "",
- field_note: "",
- text_validation_type_or_show_slider_number: "",
- text_validation_min: "",
- text_validation_max: "",
- identifier: "",
- branching_logic: "",
- required_field: "yes",
- custom_alignment: "",
- question_number: "",
- matrix_group_name: "",
- matrix_ranking: "",
- field_annotation: "",
- evaluated_logic: ""
- },
- {
- field_name: "has_fitbit",
- form_name: "eligibility",
- section_header: "",
- field_type: "text",
- field_label: "Do you have a Fitbit?",
- select_choices_or_calculations: "",
- field_note: "",
- text_validation_type_or_show_slider_number: "",
- text_validation_min: "",
- text_validation_max: "",
- identifier: "",
- branching_logic: "",
- required_field: "yes",
- custom_alignment: "",
- question_number: "",
- matrix_group_name: "",
- matrix_ranking: "",
- field_annotation: "",
- evaluated_logic: ""
- },
- {
- field_name: "is_eligible",
- form_name: "eligibility",
- section_header: "",
- field_type: "text",
- field_label: "Eligible",
- select_choices_or_calculations: "",
- field_note: "",
- text_validation_type_or_show_slider_number: "",
- text_validation_min: "",
- text_validation_max: "",
- identifier: "",
- branching_logic: "",
- required_field: "",
- custom_alignment: "",
- question_number: "",
- matrix_group_name: "",
- matrix_ranking: "",
- field_annotation: "",
- evaluated_logic: ""
- }
- ]
-
\ No newline at end of file
+ {
+ field_name: "age",
+ form_name: "eligibility",
+ section_header: "",
+ field_type: "text",
+ field_label: "How old are you?",
+ select_choices_or_calculations: "",
+ field_note: "",
+ text_validation_type_or_show_slider_number: "",
+ text_validation_min: "",
+ text_validation_max: "",
+ identifier: "",
+ branching_logic: "",
+ required_field: "yes",
+ custom_alignment: "",
+ question_number: "",
+ matrix_group_name: "",
+ matrix_ranking: "",
+ field_annotation: "",
+ evaluated_logic: "",
+ },
+ {
+ field_name: "city",
+ form_name: "eligibility",
+ section_header: "",
+ field_type: "text",
+ field_label: "What city do you live in?",
+ select_choices_or_calculations: "",
+ field_note: "",
+ text_validation_type_or_show_slider_number: "",
+ text_validation_min: "",
+ text_validation_max: "",
+ identifier: "",
+ branching_logic: "",
+ required_field: "yes",
+ custom_alignment: "",
+ question_number: "",
+ matrix_group_name: "",
+ matrix_ranking: "",
+ field_annotation: "",
+ evaluated_logic: "",
+ },
+ {
+ field_name: "has_fitbit",
+ form_name: "eligibility",
+ section_header: "",
+ field_type: "text",
+ field_label: "Do you have a Fitbit?",
+ select_choices_or_calculations: "",
+ field_note: "",
+ text_validation_type_or_show_slider_number: "",
+ text_validation_min: "",
+ text_validation_max: "",
+ identifier: "",
+ branching_logic: "",
+ required_field: "yes",
+ custom_alignment: "",
+ question_number: "",
+ matrix_group_name: "",
+ matrix_ranking: "",
+ field_annotation: "",
+ evaluated_logic: "",
+ },
+ {
+ field_name: "is_eligible",
+ form_name: "eligibility",
+ section_header: "",
+ field_type: "text",
+ field_label: "Eligible",
+ select_choices_or_calculations: "",
+ field_note: "",
+ text_validation_type_or_show_slider_number: "",
+ text_validation_min: "",
+ text_validation_max: "",
+ identifier: "",
+ branching_logic: "",
+ required_field: "",
+ custom_alignment: "",
+ question_number: "",
+ matrix_group_name: "",
+ matrix_ranking: "",
+ field_annotation: "",
+ evaluated_logic: "",
+ },
+]
diff --git a/pages/eligibility.tsx b/pages/eligibility.tsx
index 9c20896..41e7bb0 100644
--- a/pages/eligibility.tsx
+++ b/pages/eligibility.tsx
@@ -5,16 +5,21 @@ import Head from "next/head"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
import { toast } from "react-toastify"
-import { eligibilityQuestions } from "../data/eligibility-questionnaire"
+import { eligibilityQuestions } from "../data/eligibility-questionnaire"
// Import render helpers
import { MarginCard, CardTitle, TextCenterButton } from "../pkg"
+interface EligibilityFormProps {
+ questions: any[]
+ onSubmit: (event: React.FormEvent) => void
+}
+
// Renders the eligibility page
const Eligibility: NextPage = () => {
const IS_ELIGIBLE = "yes"
const router = useRouter()
- const [eligibility, setEligibility] = useState(null)
+ const [eligibility, setEligibility] = useState()
const questions: any[] = eligibilityQuestions
const checkEligibility = async (values: any) => {
@@ -69,7 +74,10 @@ const NotEligibleMessage = () => (
)
-const EligibilityForm = ({ questions, onSubmit }) => (
+const EligibilityForm: React.FC = ({
+ questions,
+ onSubmit,
+}) => (
Eligibility Screening
-
+
diff --git a/pages/login.tsx b/pages/login.tsx
index e926352..2552da3 100644
--- a/pages/login.tsx
+++ b/pages/login.tsx
@@ -94,7 +94,7 @@ const Login: NextPage = () => {
// If the previous handler did not catch the error it's most likely a form validation error
if (err.response?.status === 400) {
// Yup, it is!
- setFlow(err.response?.data)
+ setFlow(err.response?.data as LoginFlow)
return
}
diff --git a/pages/profile.tsx b/pages/profile.tsx
index d7f2f00..58e0e8a 100644
--- a/pages/profile.tsx
+++ b/pages/profile.tsx
@@ -1,12 +1,17 @@
-import { SettingsFlow } from "@ory/client"
+import {
+ SettingsFlow,
+ UiNode,
+ UiNodeInputAttributes,
+ UpdateSettingsFlowBody,
+} from "@ory/client"
import { AxiosError } from "axios"
import type { NextPage } from "next"
import Head from "next/head"
import Link from "next/link"
import { useRouter } from "next/router"
import { ReactNode, useEffect, useState } from "react"
-import { profileQuestions } from "../data/profile-questionnaire"
+import { profileQuestions } from "../data/profile-questionnaire"
import {
ActionCard,
CenterLink,
@@ -24,12 +29,19 @@ interface Props {
only?: Methods
}
-function ProfileCard({
- flow,
- only,
- children,
-}: Props & { children: ReactNode }) {
- return {children}
+interface ProfileFormProps {
+ questions: any[]
+ onSubmit: (event: React.FormEvent) => void
+ handleChange: (event: React.ChangeEvent) => void
+ profile: any
+}
+
+function ProfileCard({ children }: Props & { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
}
const Profile: NextPage = () => {
@@ -66,9 +78,11 @@ const Profile: NextPage = () => {
})
.then(({ data }) => {
setFlow(data)
- const csrfTokenFromHeaders = data.ui.nodes.find(
- (node: any) => node.attributes.name === "csrf_token",
- )?.attributes.value
+ const csrfTokenFromHeaders = (
+ data.ui.nodes.find(
+ (node: any) => node.attributes.name === "csrf_token",
+ )?.attributes as UiNodeInputAttributes
+ ).value
const traits = data.identity.traits
setCsrfToken(csrfTokenFromHeaders || "")
setTraits(traits)
@@ -86,7 +100,7 @@ const Profile: NextPage = () => {
const onSubmit = (event: React.FormEvent) => {
event.preventDefault()
- const updatedValues = {
+ const updatedValues: UpdateSettingsFlowBody = {
csrf_token: csrfToken,
method: "profile",
traits: {
@@ -115,12 +129,12 @@ const Profile: NextPage = () => {
return
}
})
- .catch(handleFlowError(router, "profile", setFlow))
+ .catch(handleFlowError(router, "settings", setFlow))
.catch(async (err: AxiosError) => {
// If the previous handler did not catch the error it's most likely a form validation error
if (err.response?.status === 400) {
// Yup, it is!
- setFlow(err.response?.data)
+ setFlow(err.response?.data as SettingsFlow)
return
}
@@ -136,8 +150,7 @@ const Profile: NextPage = () => {
Profile Page
-
-
+
User Information
{
)
}
-const ProfileForm: React.FC = ({
+const ProfileForm: React.FC = ({
questions,
onSubmit,
handleChange,
diff --git a/pages/recovery.tsx b/pages/recovery.tsx
index de0cd49..cfdad09 100644
--- a/pages/recovery.tsx
+++ b/pages/recovery.tsx
@@ -47,7 +47,7 @@ const Recovery: NextPage = () => {
// If the previous handler did not catch the error it's most likely a form validation error
if (err.response?.status === 400) {
// Yup, it is!
- setFlow(err.response?.data)
+ setFlow(err.response?.data as RecoveryFlow)
return
}
@@ -75,7 +75,7 @@ const Recovery: NextPage = () => {
switch (err.response?.status) {
case 400:
// Status code 400 implies the form validation had an error
- setFlow(err.response?.data)
+ setFlow(err.response?.data as RecoveryFlow)
return
}
diff --git a/pages/registration.tsx b/pages/registration.tsx
index 28bf884..5676d02 100644
--- a/pages/registration.tsx
+++ b/pages/registration.tsx
@@ -10,6 +10,7 @@ import { ActionCard, CenterLink, Flow, MarginCard, CardTitle } from "../pkg"
import { handleFlowError } from "../pkg/errors"
// Import the SDK
import ory from "../pkg/sdk"
+import { parseObject } from "../pkg/ui/helpers"
// Renders the registration page
const Registration: NextPage = () => {
@@ -19,16 +20,19 @@ const Registration: NextPage = () => {
// information about the form we need to render (e.g. username + password)
const [flow, setFlow] = useState()
const [eligibility, setEligibility] = useState()
+ const [projectId, setProjectId] = useState()
// Get ?flow=... from the URL
const { flow: flowId, return_to: returnTo } = router.query
useEffect(() => {
const eligible = sessionStorage.getItem("eligible")
+ const projectId = sessionStorage.getItem("project_id")
if (eligible == null) {
router.push("/eligibility")
}
setEligibility(eligible)
+ setProjectId(projectId)
}, [])
// In this effect we either initiate a new registration flow, or we fetch an existing registration flow.
@@ -62,8 +66,12 @@ const Registration: NextPage = () => {
const onSubmit = async (values: UpdateRegistrationFlowBody) => {
const updatedValues = {
- ...values,
- traits: { eligibility: JSON.parse(eligibility) },
+ ...parseObject(values),
+ transient_payload: { project_id: projectId },
+ traits: {
+ ...parseObject(values).traits,
+ eligibility: JSON.parse(eligibility),
+ },
}
await router
// On submission, add the flow ID to the URL but do not navigate. This prevents the user loosing
@@ -101,7 +109,7 @@ const Registration: NextPage = () => {
// If the previous handler did not catch the error it's most likely a form validation error
if (err.response?.status === 400) {
// Yup, it is!
- setFlow(err.response?.data)
+ setFlow(err.response?.data as RegistrationFlow)
return
}
diff --git a/pages/settings.tsx b/pages/settings.tsx
index 7ade8fd..0ea0d98 100644
--- a/pages/settings.tsx
+++ b/pages/settings.tsx
@@ -84,7 +84,7 @@ const Settings: NextPage = () => {
.catch(handleFlowError(router, "settings", setFlow))
}, [flowId, router, router.isReady, returnTo, flow])
- const onSubmit = (values: UpdateSettingsFlowWithProfileMethod) =>
+ const onSubmit = (values: UpdateSettingsFlowBody) =>
router
// On submission, add the flow ID to the URL but do not navigate. This prevents the user loosing
// his data when she/he reloads the page.
@@ -121,7 +121,7 @@ const Settings: NextPage = () => {
// If the previous handler did not catch the error it's most likely a form validation error
if (err.response?.status === 400) {
// Yup, it is!
- setFlow(err.response?.data)
+ setFlow(err.response?.data as SettingsFlow)
return
}
@@ -142,23 +142,13 @@ const Settings: NextPage = () => {
Profile Management and Security Settings
-
+ Profile Settings
+
Change Password
diff --git a/pages/study-consent.tsx b/pages/study-consent.tsx
index a9166f4..11a82c1 100644
--- a/pages/study-consent.tsx
+++ b/pages/study-consent.tsx
@@ -1,5 +1,6 @@
import {
SettingsFlow,
+ UiNodeInputAttributes,
UpdateSettingsFlowBody,
UpdateSettingsFlowWithProfileMethod,
} from "@ory/client"
@@ -10,8 +11,8 @@ import Head from "next/head"
import Link from "next/link"
import { useRouter } from "next/router"
import { ReactNode, useEffect, useState } from "react"
-import { consentQuestions } from "../data/consent-questionnaire"
+import { consentQuestions } from "../data/consent-questionnaire"
import {
ActionCard,
CenterLink,
@@ -29,12 +30,12 @@ interface Props {
only?: Methods
}
-function SettingsCard({
- flow,
- only,
- children,
-}: Props & { children: ReactNode }) {
- return {children}
+function StudyConsentCard({ children }: Props & { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
}
const StudyConsent: NextPage = () => {
@@ -71,9 +72,11 @@ const StudyConsent: NextPage = () => {
})
.then(({ data }) => {
setFlow(data)
- const csrfTokenFromHeaders = data.ui.nodes.find(
- (node: any) => node.attributes.name === "csrf_token",
- )?.attributes.value
+ const csrfTokenFromHeaders = (
+ data.ui.nodes.find(
+ (node: any) => node.attributes.name === "csrf_token",
+ )?.attributes as UiNodeInputAttributes
+ ).value
const traits = data.identity.traits
setCsrfToken(csrfTokenFromHeaders || "")
setTraits(traits)
@@ -82,7 +85,7 @@ const StudyConsent: NextPage = () => {
.catch(handleFlowError(router, "settings", setFlow))
}, [flowId, router, router.isReady, returnTo, flow])
- const handleChange = (event) => {
+ const handleChange = (event: React.ChangeEvent) => {
setConsent({
...consent,
[event.target.name]: String(event.target.checked),
@@ -91,7 +94,7 @@ const StudyConsent: NextPage = () => {
const onSubmit = (event: React.FormEvent) => {
event.preventDefault()
- const updatedValues = {
+ const updatedValues: UpdateSettingsFlowBody = {
csrf_token: csrfToken,
method: "profile",
traits: {
@@ -120,12 +123,12 @@ const StudyConsent: NextPage = () => {
return
}
})
- .catch(handleFlowError(router, "consent", setFlow))
+ .catch(handleFlowError(router, "settings", setFlow))
.catch(async (err: AxiosError) => {
// If the previous handler did not catch the error it's most likely a form validation error
if (err.response?.status === 400) {
// Yup, it is!
- setFlow(err.response?.data)
+ setFlow(err.response?.data as SettingsFlow)
return
}
@@ -141,8 +144,7 @@ const StudyConsent: NextPage = () => {
Study Consent
-
-
+
Study Consent
{
handleChange={handleChange}
consent={consent}
/>
-
+
Go back
@@ -168,16 +170,18 @@ const ConsentForm: React.FC = ({
}) => {
return (
- {questions.map((question, index) => {
+ {questions.map((question: any, index: number) => {
if (question.field_type === "info") {
return (
question.select_choices_or_calculations instanceof Array &&
- question.select_choices_or_calculations.map((info, idx) => (
-
-
- {info.label}
-
- ))
+ question.select_choices_or_calculations.map(
+ (info: any, idx: number) => (
+
+
+ {info.label}
+
+ ),
+ )
)
} else if (question.field_type === "checkbox") {
return (
diff --git a/pages/study.tsx b/pages/study.tsx
index d741e39..30844c0 100644
--- a/pages/study.tsx
+++ b/pages/study.tsx
@@ -2,8 +2,8 @@ import type { NextPage } from "next"
import Head from "next/head"
import { useRouter } from "next/router"
import { useEffect, useState } from "react"
-import { studyInfo } from "../data/study-questionnaire"
+import { studyInfo } from "../data/study-questionnaire"
// Import render helpers
import { MarginCard, CardTitle, TextCenterButton, InnerCard } from "../pkg"
@@ -17,6 +17,7 @@ const Study: NextPage = () => {
if (router.isReady) {
const { projectId } = router.query
if (typeof projectId === "string") {
+ sessionStorage.setItem("project_id", projectId)
setProjectId(projectId)
}
}
@@ -31,12 +32,7 @@ const Study: NextPage = () => {
{projectId} Research Study
-
+
Join Now
{/* */}
@@ -48,16 +44,18 @@ const Study: NextPage = () => {
const StudyInfo: React.FC = ({ questions }) => {
return (
- {questions.map((question, index) => {
+ {questions.map((question: any, index: number) => {
if (question.field_type === "info") {
return (
question.select_choices_or_calculations instanceof Array &&
- question.select_choices_or_calculations.map((info, idx) => (
-
-
- {info.label}
-
- ))
+ question.select_choices_or_calculations.map(
+ (info: any, idx: number) => (
+
+
+ {info.label}
+
+ ),
+ )
)
}
return null
diff --git a/pages/verification.tsx b/pages/verification.tsx
index 26dc847..2af6318 100644
--- a/pages/verification.tsx
+++ b/pages/verification.tsx
@@ -78,22 +78,21 @@ const Verification: NextPage = () => {
setFlow(data)
})
.catch((err: AxiosError) => {
+ const data = err.response?.data as VerificationFlow
switch (err.response?.status) {
case 400:
// Status code 400 implies the form validation had an error
- setFlow(err.response?.data)
+ setFlow(data)
return
case 410:
- const newFlowID = err.response.data.use_flow_id
router
// On submission, add the flow ID to the URL but do not navigate. This prevents the user loosing
// their data when they reload the page.
- .push(`/verification?flow=${newFlowID}`, undefined, {
+ .push(`/verification?flow=${data.id}`, undefined, {
shallow: true,
})
-
ory
- .getVerificationFlow({ id: newFlowID })
+ .getVerificationFlow({ id: data.id })
.then(({ data }) => setFlow(data))
return
}
diff --git a/pkg/errors.tsx b/pkg/errors.tsx
index dc3d8f0..4910f64 100644
--- a/pkg/errors.tsx
+++ b/pkg/errors.tsx
@@ -1,3 +1,7 @@
+import {
+ ErrorAuthenticatorAssuranceLevelNotSatisfied,
+ FlowError,
+} from "@ory/client"
import { AxiosError } from "axios"
import { NextRouter } from "next/router"
import { Dispatch, SetStateAction } from "react"
@@ -10,13 +14,16 @@ export function handleGetFlowError
(
resetFlow: Dispatch>,
) {
return async (err: AxiosError) => {
- switch (err.response?.data.error?.id) {
+ const data = err.response?.data as FlowError
+ switch (data.id) {
case "session_inactive":
await router.push("/login?return_to=" + window.location.href)
return
case "session_aal2_required":
- if (err.response?.data.redirect_browser_to) {
- const redirectTo = new URL(err.response?.data.redirect_browser_to)
+ const e = err.response
+ ?.data as ErrorAuthenticatorAssuranceLevelNotSatisfied
+ if (e.redirect_browser_to) {
+ const redirectTo = new URL(e.redirect_browser_to)
if (flowType === "settings") {
redirectTo.searchParams.set("return_to", window.location.href)
}
@@ -32,7 +39,7 @@ export function handleGetFlowError(
return
case "session_refresh_required":
// We need to re-authenticate to perform this action
- window.location.href = err.response?.data.redirect_browser_to
+ await router.push("/")
return
case "self_service_flow_return_to_forbidden":
// The flow expired, let's request a new one.
@@ -61,7 +68,7 @@ export function handleGetFlowError(
return
case "browser_location_change_required":
// Ory Kratos asked us to point the user to this URL.
- window.location.href = err.response.data.redirect_browser_to
+ await router.push("/")
return
}
diff --git a/pkg/ui/Flow.tsx b/pkg/ui/Flow.tsx
index 2769932..6e00060 100644
--- a/pkg/ui/Flow.tsx
+++ b/pkg/ui/Flow.tsx
@@ -4,6 +4,7 @@ import {
RegistrationFlow,
SettingsFlow,
UiNode,
+ UiNodeInputAttributes,
UpdateLoginFlowBody,
UpdateRecoveryFlowBody,
UpdateRegistrationFlowBody,
@@ -108,10 +109,18 @@ export class Flow extends Component, State> {
if (!flow) {
return []
}
- return flow.ui.nodes.filter(({ group }) => {
- if (!only) {
- return true
+ return flow.ui.nodes.filter(({ group, attributes }) => {
+ if (!only) return true
+
+ const isEmailNode = (attrs: UiNodeInputAttributes) =>
+ ["email", "submit"].includes(attrs.type) || group === "default"
+
+ if (only === "profile") {
+ return (
+ isEmailNode(attributes as UiNodeInputAttributes) && group === only
+ )
}
+
return group === "default" || group === only
})
}
diff --git a/pkg/ui/helpers.ts b/pkg/ui/helpers.ts
index fd9b3f7..a261c04 100644
--- a/pkg/ui/helpers.ts
+++ b/pkg/ui/helpers.ts
@@ -42,3 +42,16 @@ export const callWebauthnFunction = (functionBody: string) => {
return intervalHandle
}
+
+export function parseObject(input: any): any {
+ return Object.keys(input).reduce((output, key) => {
+ const [mainKey, subKey] = key.split(".")
+ if (subKey) {
+ output[mainKey] = output[mainKey] || {}
+ output[mainKey][subKey] = input[key]
+ } else {
+ output[key] = input[key]
+ }
+ return output
+ }, {} as any)
+}
diff --git a/styles/globals.css b/styles/globals.css
index e627b09..fb23b43 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -98,8 +98,12 @@ input {
}
.codebox {
- white-space: pre-wrap; /* Allows long lines to be wrapped */
- overflow: auto; /* Adds scrollbars if necessary */
- word-break: break-word; /* Breaks words if necessary to prevent overflow */
- max-width: 100%; /* Ensures the codebox does not exceed its container width */
-}
\ No newline at end of file
+ white-space: pre-wrap; /* Allows long lines to be wrapped */
+ overflow: auto; /* Adds scrollbars if necessary */
+ word-break: break-word; /* Breaks words if necessary to prevent overflow */
+ max-width: 100%; /* Ensures the codebox does not exceed its container width */
+}
+
+.cardMargin {
+ margin-top: 80px !important;
+}