diff --git a/src/components/TutorialComponent.tsx b/src/components/TutorialComponent.tsx new file mode 100644 index 0000000..9701fb4 --- /dev/null +++ b/src/components/TutorialComponent.tsx @@ -0,0 +1,17 @@ +import { CallBackProps, Joyride, Props } from "react-joyride"; + + +interface TutorialComponentProps extends Props { + updateFunction: () => void; +} + +export default function TutorialComponent(props: TutorialComponentProps){ + return Joyride({ + ...props, + callback: (callbackProps: CallBackProps) => { + if(callbackProps.type === "tour:end"){ + props.updateFunction(); + } + } + }); +} diff --git a/src/components/nav/ProtectedNavLinks.tsx b/src/components/nav/ProtectedNavLinks.tsx index f7228db..78ff221 100644 --- a/src/components/nav/ProtectedNavLinks.tsx +++ b/src/components/nav/ProtectedNavLinks.tsx @@ -33,8 +33,6 @@ export default function ProtectedNavLinks(){ path: "/logout" }); - console.log("is admin = " + isSiteAdmin(accountPermissions)); - return ( <> diff --git a/src/components/tab/TabGroup.tsx b/src/components/tab/TabGroup.tsx index b5c2e61..66e52d5 100644 --- a/src/components/tab/TabGroup.tsx +++ b/src/components/tab/TabGroup.tsx @@ -3,6 +3,7 @@ import { HTMLProps, useState } from "react"; export interface Tab { + tabId?: string; tabHeader: React.ReactNode; headerClasses?: string; tabContent: React.ReactNode; @@ -39,6 +40,7 @@ export default function TabGroup(props: TabGroupProps){ { tabs.map((tab, index) => ( void; active: boolean; }){ return (
{ + const response = await api.get(`/account/tutorial`); + + if(response.status !== 200){ + throw new Error("Failed to get tutorials status"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + return response.data as AccountTutorialStatus; + }, + enabled: !!accountId + }); +} + +export function useUpdateTutorialsStatus(){ + return useMutation({ + mutationKey: ["tutorials", "accounts"], + mutationFn: async (tutorials: AccountTutorialStatus) => { + const response = await api.put(`/account/tutorial`, tutorials); + + if(response.status !== 200){ + throw new Error("Failed to update tutorials status"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + } + }); +} + export function useForcePasswordReset(accountId: string){ diff --git a/src/interface/AccountTutorialStatus.ts b/src/interface/AccountTutorialStatus.ts new file mode 100644 index 0000000..1c4f33f --- /dev/null +++ b/src/interface/AccountTutorialStatus.ts @@ -0,0 +1,15 @@ +export enum TutorialStatus { + COMPLETED = "COMPLETED", + NOT_COMPLETED = "NOT_COMPLETED" +} + + +export interface AccountTutorialStatus { + accountTutorialStatusId?: string; + accountId: string; + gamesTutorialStatus: TutorialStatus; + gameTutorialStatus: TutorialStatus; + raidGroupsTutorialStatus: TutorialStatus; + raidGroupTutorialStatus: TutorialStatus; + instanceTutorialStatus: TutorialStatus; +} diff --git a/src/pages/protected/GamePage.tsx b/src/pages/protected/GamePage.tsx index 3328358..b3e56c1 100644 --- a/src/pages/protected/GamePage.tsx +++ b/src/pages/protected/GamePage.tsx @@ -1,33 +1,39 @@ +import DangerMessage from "@/components/message/DangerMessage"; import TabGroup, { Tab } from "@/components/tab/TabGroup"; +import TutorialComponent from "@/components/TutorialComponent"; import { useGetGame } from "@/hooks/GameHooks"; +import { TutorialStatus } from "@/interface/AccountTutorialStatus"; import { Game } from "@/interface/Game"; +import { useAuth } from "@/providers/AuthProvider"; import GameCalendarDisplay from "@/ui/calendar/GameCalendarDisplay"; import GameHeader from "@/ui/game/GameHeader"; import GameClassDisplay from "@/ui/gameClass/GameClassDisplay"; import RaidGroupsByGameDisplay from "@/ui/raidGroup/RaidGroupsByGameDisplay"; +import { gameTutorialSteps } from "@/util/TutorialUtil"; import { useEffect, useState } from "react"; import { Navigate, useParams } from "react-router"; export default function GamePage(){ const { gameId } = useParams(); - const tabs: Tab[] = [ - { - tabHeader: "Calendar", - tabContent: - }, - { - tabHeader: "Raid Groups", - tabContent: - }, - { - tabHeader: "Classes", - tabContent: - } - ]; - const [ game, setGame ] = useState(); + const { tutorialsStatus, setTutorialsStatus } = useAuth(); + const gameQuery = useGetGame(gameId ?? "", false); + + + const updateGameTutorialStatus = () => { + const newTutorialsStatus = { ...tutorialsStatus }; + if(tutorialsStatus.gameTutorialStatus === TutorialStatus.COMPLETED){ + newTutorialsStatus.gameTutorialStatus = TutorialStatus.NOT_COMPLETED; + } + else if(tutorialsStatus.gameTutorialStatus === TutorialStatus.NOT_COMPLETED){ + newTutorialsStatus.gameTutorialStatus = TutorialStatus.COMPLETED; + } + setTutorialsStatus(newTutorialsStatus); + } + + useEffect(() => { if(gameQuery.status === "success"){ setGame(gameQuery.data); @@ -42,7 +48,7 @@ export default function GamePage(){ } else if(gameQuery.status === "error"){ return ( -
Error
+ Error: {gameQuery.error.message} ); } else if(gameQuery.status === "success" && gameQuery.data === undefined){ @@ -51,10 +57,39 @@ export default function GamePage(){ ); } else if(game){ + const tabs: Tab[] = [ + { + tabId: "calendarTab", + tabHeader: "Calendar", + tabContent: + }, + { + tabId: "raidGroupsTab", + tabHeader: "Raid Groups", + tabContent: + }, + { + tabId: "gameClassesTab", + tabHeader: "Classes", + tabContent: + } + ]; + + return (
+ {/* Tutorials */} + diff --git a/src/pages/protected/GamesPage.tsx b/src/pages/protected/GamesPage.tsx index 9cfdd06..baf1851 100644 --- a/src/pages/protected/GamesPage.tsx +++ b/src/pages/protected/GamesPage.tsx @@ -1,11 +1,35 @@ +import TutorialComponent from "@/components/TutorialComponent"; +import { TutorialStatus } from "@/interface/AccountTutorialStatus"; +import { useAuth } from "@/providers/AuthProvider"; import AllGamesDisplay from "@/ui/game/AllGamesDisplay"; +import { gamesTutorialSteps } from "@/util/TutorialUtil"; export default function GamesPage(){ + const { tutorialsStatus, setTutorialsStatus } = useAuth(); + + + const updateGamesTutorialStatus = () => { + const newTutorialsStatus = { ...tutorialsStatus }; + if(tutorialsStatus.gamesTutorialStatus === TutorialStatus.COMPLETED){ + newTutorialsStatus.gamesTutorialStatus = TutorialStatus.NOT_COMPLETED; + } + else if(tutorialsStatus.gamesTutorialStatus === TutorialStatus.NOT_COMPLETED){ + newTutorialsStatus.gamesTutorialStatus = TutorialStatus.COMPLETED; + } + setTutorialsStatus(newTutorialsStatus); + } + + return (
+

diff --git a/src/pages/protected/RaidGroupPage.tsx b/src/pages/protected/RaidGroupPage.tsx index 76b858b..5843f53 100644 --- a/src/pages/protected/RaidGroupPage.tsx +++ b/src/pages/protected/RaidGroupPage.tsx @@ -1,6 +1,8 @@ import DangerMessage from "@/components/message/DangerMessage"; import TabGroup, { Tab } from "@/components/tab/TabGroup"; +import TutorialComponent from "@/components/TutorialComponent"; import { useGetRaidGroup } from "@/hooks/RaidGroupHooks"; +import { TutorialStatus } from "@/interface/AccountTutorialStatus"; import { RaidGroup } from "@/interface/RaidGroup"; import { useAuth } from "@/providers/AuthProvider"; import RaidGroupAccountsTab from "@/ui/account/RaidGroupAccountsTab"; @@ -12,17 +14,20 @@ import RaidGroupRequestTab from "@/ui/raidGroupRequest/RaidGroupRequestTab"; import RaidInstanceTab from "@/ui/raidInstance/RaidInstanceTab"; import RaidLayoutTab from "@/ui/raidLayout/RaidLayoutTab"; import { isRaidGroupAdmin } from "@/util/PermissionUtil"; +import { raidGroupTutorialSteps } from "@/util/TutorialUtil"; import { useEffect, useState } from "react"; import { Navigate, useParams } from "react-router"; export default function RaidGroupPage(){ const { raidGroupId } = useParams(); - const { accountPermissions, raidGroupPermissions } = useAuth(); - + const { accountPermissions, raidGroupPermissions, tutorialsStatus, setTutorialsStatus } = useAuth(); const [ raidGroup, setRaidGroup ] = useState(); + + const raidGroupQuery = useGetRaidGroup(raidGroupId ?? "", false); + useEffect(() => { if(raidGroupQuery.status === "success"){ setRaidGroup(raidGroupQuery.data); @@ -30,6 +35,18 @@ export default function RaidGroupPage(){ }, [ raidGroupQuery ]); + const updateRaidGroupTutorialStatus = () => { + const newTutorialsStatus = { ...tutorialsStatus }; + if(tutorialsStatus.raidGroupTutorialStatus === TutorialStatus.COMPLETED){ + newTutorialsStatus.raidGroupTutorialStatus = TutorialStatus.NOT_COMPLETED; + } + else if(tutorialsStatus.raidGroupTutorialStatus === TutorialStatus.NOT_COMPLETED){ + newTutorialsStatus.raidGroupTutorialStatus = TutorialStatus.COMPLETED; + } + setTutorialsStatus(newTutorialsStatus); + } + + if(raidGroupQuery.status === "pending"){ return (
Loading...
@@ -48,22 +65,27 @@ export default function RaidGroupPage(){ else if(raidGroup){ const tabs: Tab[] = [ { + tabId: "calendarTab", tabHeader: "Calendar", tabContent: }, { + tabId: "peopleTab", tabHeader: "People", tabContent: }, { + tabId: "classGroupsTab", tabHeader: "Class Groups", tabContent: }, { + tabId: "raidLayoutsTab", tabHeader: "Raid Layout", tabContent: }, { + tabId: "instancesTab", tabHeader: "Raid Instances", tabContent: } @@ -71,10 +93,12 @@ export default function RaidGroupPage(){ if(isRaidGroupAdmin(raidGroupId!, raidGroupPermissions, accountPermissions)){ tabs.push({ + tabId: "usersTab", tabHeader: "Users", tabContent: }); tabs.push({ + tabId: "requestsTab", tabHeader: "Requests", tabContent: }); @@ -85,6 +109,15 @@ export default function RaidGroupPage(){
+ diff --git a/src/pages/protected/RaidGroupsPage.tsx b/src/pages/protected/RaidGroupsPage.tsx index 00cb9c1..b978012 100644 --- a/src/pages/protected/RaidGroupsPage.tsx +++ b/src/pages/protected/RaidGroupsPage.tsx @@ -1,15 +1,35 @@ +import TutorialComponent from "@/components/TutorialComponent"; +import { TutorialStatus } from "@/interface/AccountTutorialStatus"; import { useAuth } from "@/providers/AuthProvider"; import RaidGroupsByAccountDisplay from "@/ui/raidGroup/RaidGroupsByAccountDisplay"; +import { raidGroupsTutorialSteps } from "@/util/TutorialUtil"; export default function RaidGroupsPage(){ - const { accountId } = useAuth(); + const { accountId, tutorialsStatus, setTutorialsStatus } = useAuth(); + + + const updateRaidGroupsTutorialStatus = () => { + const newTutorialsStatus = { ...tutorialsStatus }; + if(tutorialsStatus.raidGroupsTutorialStatus === TutorialStatus.COMPLETED){ + newTutorialsStatus.raidGroupsTutorialStatus = TutorialStatus.NOT_COMPLETED; + } + else if(tutorialsStatus.raidGroupsTutorialStatus === TutorialStatus.NOT_COMPLETED){ + newTutorialsStatus.raidGroupsTutorialStatus = TutorialStatus.COMPLETED; + } + setTutorialsStatus(newTutorialsStatus); + } return (
+

diff --git a/src/pages/protected/RaidInstancePage.tsx b/src/pages/protected/RaidInstancePage.tsx index 2077c85..fbe7acb 100644 --- a/src/pages/protected/RaidInstancePage.tsx +++ b/src/pages/protected/RaidInstancePage.tsx @@ -1,15 +1,32 @@ import PrimaryButton from "@/components/button/PrimaryButton"; +import TutorialComponent from "@/components/TutorialComponent"; +import { TutorialStatus } from "@/interface/AccountTutorialStatus"; +import { useAuth } from "@/providers/AuthProvider"; import RaidInstanceLayoutProvider from "@/providers/RaidInstanceLayoutProvider"; import RaidInstanceCreatorUI from "@/ui/raidInstance/creator/RaidInstanceCreatorUI"; +import { instanceTutorialSteps } from "@/util/TutorialUtil"; import { BsArrowLeft } from "react-icons/bs"; import { useNavigate, useParams } from "react-router"; export default function RaidInstancePage(){ const { raidGroupId, raidInstanceId } = useParams(); + const { tutorialsStatus, setTutorialsStatus } = useAuth(); const navigator = useNavigate(); + const updateGamesTutorialStatus = () => { + const newTutorialsStatus = { ...tutorialsStatus }; + if(tutorialsStatus.instanceTutorialStatus === TutorialStatus.COMPLETED){ + newTutorialsStatus.instanceTutorialStatus = TutorialStatus.NOT_COMPLETED; + } + else if(tutorialsStatus.instanceTutorialStatus === TutorialStatus.NOT_COMPLETED){ + newTutorialsStatus.instanceTutorialStatus = TutorialStatus.COMPLETED; + } + setTutorialsStatus(newTutorialsStatus); + } + + return ( + {/* Back to Raid Group Button */}
void; } const initialState: AuthProviderState = { @@ -34,7 +38,9 @@ const initialState: AuthProviderState = { accountPermissions: [], raidGroupPermissions: [], gamePermissions: [], - raidGroupRequests: [] + raidGroupRequests: [], + tutorialsStatus: {} as AccountTutorialStatus, + setTutorialsStatus: () => null } const AuthContext = createContext(initialState); @@ -51,6 +57,11 @@ export function AuthProvider({ const [ raidGroupPermissions, setRaidGroupPermissions ] = useState([]); const [ gamePermissions, setGamePermissions ] = useState([]); const [ raidGroupRequests, setRaidGroupRequests ] = useState([]); + const [ tutorialsStatus, setTutorialsStatus ] = useState({} as AccountTutorialStatus); + + + const tutorialsStatusQuery = useGetTutorialsStatus(accountId); + const { mutate: tutorialsStatusMutation } = useUpdateTutorialsStatus(); const fetchToken = useCallback(async () => { @@ -112,18 +123,29 @@ export function AuthProvider({ fetchToken(); }, [ fetchToken ]); + //Update the tutorial status when fetched + useEffect(() => { + if(tutorialsStatusQuery.status === "success"){ + setTutorialsStatus(tutorialsStatusQuery.data); + } + }, [ tutorialsStatusQuery.status, tutorialsStatusQuery.data ]); + + const updateTutorialsStatus = useCallback((newTutorialsStatus: AccountTutorialStatus) => { + setTutorialsStatus(newTutorialsStatus); + tutorialsStatusMutation(newTutorialsStatus); + }, [ setTutorialsStatus, tutorialsStatusMutation ]); + const currentTokens = useMemo(() => ({ - jwt, - setJwt, - expiration, - setExpiration, + jwt, setJwt, + expiration, setExpiration, accountId, accountPermissions, raidGroupPermissions, gamePermissions, - raidGroupRequests - }), [ jwt, setJwt, expiration, setExpiration, accountId, accountPermissions, raidGroupPermissions, gamePermissions, raidGroupRequests ]); + raidGroupRequests, + tutorialsStatus, setTutorialsStatus: updateTutorialsStatus + }), [ jwt, expiration, accountId, accountPermissions, raidGroupPermissions, gamePermissions, raidGroupRequests, tutorialsStatus, updateTutorialsStatus ]); //TODO: Return a spinner while the first token is being fetched diff --git a/src/ui/raidInstance/RaidInstanceHeader.tsx b/src/ui/raidInstance/RaidInstanceHeader.tsx index 72da72f..865fb2b 100644 --- a/src/ui/raidInstance/RaidInstanceHeader.tsx +++ b/src/ui/raidInstance/RaidInstanceHeader.tsx @@ -195,6 +195,7 @@ export default function RaidInstanceHeader(){ > { Save diff --git a/src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx b/src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx index 340c279..b19ee56 100644 --- a/src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx +++ b/src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx @@ -42,6 +42,7 @@ export default function RaidInstanceCreatorTable({
{/* Buttons */} Add Run diff --git a/src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx b/src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx index 2544493..250f5a0 100644 --- a/src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx +++ b/src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx @@ -104,6 +104,7 @@ export default function RaidInstanceCreatorTableBody({ className="flex flex-row justify-center items-center cursor-pointer p-2 space-x-2" > removeRun(runIndex)} @@ -114,6 +115,7 @@ export default function RaidInstanceCreatorTableBody({ /> copyDiscordStringToClipBoard(runIndex)} diff --git a/src/util/TutorialUtil.ts b/src/util/TutorialUtil.ts new file mode 100644 index 0000000..2b6a413 --- /dev/null +++ b/src/util/TutorialUtil.ts @@ -0,0 +1,253 @@ +import { Step } from "react-joyride"; + + +const nextButtonStyles = { + backgroundColor: "#0000FF" +}; + +const backButtonStyles = { + color: "#0000FF" +} + + +export const gamesTutorialSteps: Array = [ + { + target: "body", + content: "This is the games page. It allows you to select a game to view its details and Raid Groups.", + placement: "center", + styles: { + buttonNext: nextButtonStyles + } + } +]; + + +export const gameTutorialSteps: Array = [ + { + target: "body", + content: "This is the games page. From here you can see info about the game and groups that play this game.", + placement: "center", + styles: { + buttonNext: nextButtonStyles, + buttonBack: backButtonStyles + } + }, + { + target: "#calendarTab", + content: "This is the Calendar tab. It is populated with game-wide events by Game Admins.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles, + buttonBack: backButtonStyles + } + }, + { + target: "#raidGroupsTab", + content: "This tab displays all the Raid Groups associated with this game. " + + "If you already belong to a Raid Group you can go to tis Raid Group page. " + + "You can also request to join an existing Raid Group or create your own.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles, + buttonBack: backButtonStyles + } + }, + { + target: "#gameClassesTab", + content: "This tab displays all the classes in the game. " + + "From here you can update, add, or delete information related to the classes.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles, + buttonBack: backButtonStyles + } + } +]; + +export const raidGroupsTutorialSteps: Array = [ + { + target: "body", + content: "This is the Raid Groups page. " + + "This is where you can see all of the Raid Groups to which you belong. " + + "When you select one of the Group you will be taken to their Raid Group page.", + placement: "center", + styles: { + buttonNext: nextButtonStyles + } + } +]; + +export const raidGroupTutorialSteps: Array = [ + { + target: "body", + content: "This is the Raid Group page. " + + "From here you can see everything that is happening in the Raid Group.", + placement: "center", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#peopleTab", + content: "Start your Raid Group by adding the people who will be participating in your raids. " + + "By adding the person's Discord ID you will be able to copy raids directly from Raid Builder into Discord with the user tagged.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#peopleTab", + content: "Once a person has been added you can add characters to that person. " + + "These characters can then be used to make a roster. " + + "In order to edit or delete a character you can click the person's name to go to the details page.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "body", + content: "This is all that is required to begin creating rosters. " + + "However, there are a few more steps you can take to make creating rosters faster and easier.", + placement: "center", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#classGroupsTab", + content: "Class Groups allow you to define roles in a raid and determine which classes are capable of filling that role.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#classGroupsTab", + content: "As an example:\n" + + "You could create a Class Group named \"Damage\" and include the classes \"Hunter\" and \"Rogue\"." + + "Then, during roster creation you can select the \"Damage\" role as required. " + + "This will automatically filter the characters so you only see Hunters and Rogues when filling that spot in the roster.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#raidLayoutsTab", + content: "Raid Layouts allow you to save the makeup of a raid, in regards to which Class Groups are required. " + + "Whether this is a raid you run often, or the general makeup of a good group, " + + "having a Layout before you create a roster will automatically fill in the Class Groups section of the roster, " + + "meaning all characters are correctly filtered for each role from the start.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#instancesTab", + content: "This tab holds all of the rosters that you have created. " + + "They can have distinct names and times, and you can even create multiple rosters within the same instance to facilitate instance farming.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#calendarTab", + content: "The Calendar tab is where you can see all of the events that are happening in the Raid Group. " + + "These events include the events from the Game, as well as Instances and any other custom events that Raid Group admins have created.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#usersTab", + content: "The users tab displays website accounts that have access to the Raid Group and let you control the permission level of each user.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#requestsTab", + content: "The Requests tab shows users that have requested access to your Raid Group. " + + "Once you have reviewed the request you can deny it, or accept it and choose the user's permission level.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + } +]; + + +export const instanceTutorialSteps: Array = [ + { + target: "body", + content: "This is the Raid Instance page. " + + "This is where you can setup rosters for a run.", + placement: "center", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#selectRaidLayoutButton", + content: "To start you can pick an existing Raid Layout, which will automatically filter characters in each slot for you.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#instanceTableHead", + content: "You can also create an adhoc raid by clicking one of the Any cells and selecting Class Groups directly.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#instanceTableBody", + content: "Or you can begin by clicking None and selecting characters to fill each slot.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#saveInstanceButton", + content: "You can save the Instance at any time. This will cause it to appear on the Instance and Calendar tabs. " + + "You can also come back and edit it later if needed.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#instanceAddRunButton", + content: "Click this button to add another run to the instance.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#removeRun0", + content: "Click this button to remove this run from the instance.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + }, + { + target: "#copyDiscordString0", + content: "Click this button to copy a string to your clipboard that is ready to be pasted into Discord.", + placement: "bottom", + styles: { + buttonNext: nextButtonStyles + } + } +];