Tutorial Working

This commit is contained in:
2025-03-15 18:23:04 -04:00
parent a842c24d0d
commit ea0018bae2
15 changed files with 517 additions and 29 deletions

View File

@@ -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();
}
}
});
}

View File

@@ -33,8 +33,6 @@ export default function ProtectedNavLinks(){
path: "/logout" path: "/logout"
}); });
console.log("is admin = " + isSiteAdmin(accountPermissions));
return ( return (
<> <>

View File

@@ -3,6 +3,7 @@ import { HTMLProps, useState } from "react";
export interface Tab { export interface Tab {
tabId?: string;
tabHeader: React.ReactNode; tabHeader: React.ReactNode;
headerClasses?: string; headerClasses?: string;
tabContent: React.ReactNode; tabContent: React.ReactNode;
@@ -39,6 +40,7 @@ export default function TabGroup(props: TabGroupProps){
{ {
tabs.map((tab, index) => ( tabs.map((tab, index) => (
<TabHeader <TabHeader
id={tab.tabId}
key={index} key={index}
tab={tab} tab={tab}
active={activeTab === index} active={activeTab === index}
@@ -71,16 +73,19 @@ export default function TabGroup(props: TabGroupProps){
function TabHeader({ function TabHeader({
id,
tab, tab,
onClick, onClick,
active active
}:{ }:{
id?: string;
tab: Tab; tab: Tab;
onClick: () => void; onClick: () => void;
active: boolean; active: boolean;
}){ }){
return ( return (
<div <div
id={id}
className={clsx( className={clsx(
tab.headerClasses, tab.headerClasses,
"px-4 py-2 rounded-t-lg cursor-pointer whitespace-nowrap", "px-4 py-2 rounded-t-lg cursor-pointer whitespace-nowrap",

View File

@@ -1,4 +1,5 @@
import { Account } from "@/interface/Account"; import { Account } from "@/interface/Account";
import { AccountTutorialStatus } from "@/interface/AccountTutorialStatus";
import { RaidGroupPermissionType } from "@/interface/RaidGroup"; import { RaidGroupPermissionType } from "@/interface/RaidGroup";
import { api } from "@/util/AxiosUtil"; import { api } from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -124,6 +125,41 @@ export function useGetAccountsByRaidGroupCount(raidGroupId: string, searchTerm?:
}); });
} }
export function useGetTutorialsStatus(accountId: string | null){
return useQuery({
queryKey: ["tutorials", "account", accountId],
queryFn: async () => {
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){ export function useForcePasswordReset(accountId: string){

View File

@@ -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;
}

View File

@@ -1,33 +1,39 @@
import DangerMessage from "@/components/message/DangerMessage";
import TabGroup, { Tab } from "@/components/tab/TabGroup"; import TabGroup, { Tab } from "@/components/tab/TabGroup";
import TutorialComponent from "@/components/TutorialComponent";
import { useGetGame } from "@/hooks/GameHooks"; import { useGetGame } from "@/hooks/GameHooks";
import { TutorialStatus } from "@/interface/AccountTutorialStatus";
import { Game } from "@/interface/Game"; import { Game } from "@/interface/Game";
import { useAuth } from "@/providers/AuthProvider";
import GameCalendarDisplay from "@/ui/calendar/GameCalendarDisplay"; import GameCalendarDisplay from "@/ui/calendar/GameCalendarDisplay";
import GameHeader from "@/ui/game/GameHeader"; import GameHeader from "@/ui/game/GameHeader";
import GameClassDisplay from "@/ui/gameClass/GameClassDisplay"; import GameClassDisplay from "@/ui/gameClass/GameClassDisplay";
import RaidGroupsByGameDisplay from "@/ui/raidGroup/RaidGroupsByGameDisplay"; import RaidGroupsByGameDisplay from "@/ui/raidGroup/RaidGroupsByGameDisplay";
import { gameTutorialSteps } from "@/util/TutorialUtil";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Navigate, useParams } from "react-router"; import { Navigate, useParams } from "react-router";
export default function GamePage(){ export default function GamePage(){
const { gameId } = useParams(); const { gameId } = useParams();
const tabs: Tab[] = [
{
tabHeader: "Calendar",
tabContent: <GameCalendarDisplay gameId={gameId!}/>
},
{
tabHeader: "Raid Groups",
tabContent: <RaidGroupsByGameDisplay gameId={gameId!}/>
},
{
tabHeader: "Classes",
tabContent: <GameClassDisplay gameId={gameId!}/>
}
];
const [ game, setGame ] = useState<Game>(); const [ game, setGame ] = useState<Game>();
const { tutorialsStatus, setTutorialsStatus } = useAuth();
const gameQuery = useGetGame(gameId ?? "", false); 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(() => { useEffect(() => {
if(gameQuery.status === "success"){ if(gameQuery.status === "success"){
setGame(gameQuery.data); setGame(gameQuery.data);
@@ -42,7 +48,7 @@ export default function GamePage(){
} }
else if(gameQuery.status === "error"){ else if(gameQuery.status === "error"){
return ( return (
<div>Error</div> <DangerMessage>Error: {gameQuery.error.message}</DangerMessage>
); );
} }
else if(gameQuery.status === "success" && gameQuery.data === undefined){ else if(gameQuery.status === "success" && gameQuery.data === undefined){
@@ -51,10 +57,39 @@ export default function GamePage(){
); );
} }
else if(game){ else if(game){
const tabs: Tab[] = [
{
tabId: "calendarTab",
tabHeader: "Calendar",
tabContent: <GameCalendarDisplay gameId={gameId!}/>
},
{
tabId: "raidGroupsTab",
tabHeader: "Raid Groups",
tabContent: <RaidGroupsByGameDisplay gameId={gameId!}/>
},
{
tabId: "gameClassesTab",
tabHeader: "Classes",
tabContent: <GameClassDisplay gameId={gameId!}/>
}
];
return ( return (
<main <main
className="flex flex-col items-center justify-center" className="flex flex-col items-center justify-center"
> >
{/* Tutorials */}
<TutorialComponent
steps={gameTutorialSteps}
run={tutorialsStatus.gameTutorialStatus === TutorialStatus.NOT_COMPLETED}
showProgress={true}
showSkipButton={true}
continuous={true}
disableScrolling={true}
updateFunction={updateGameTutorialStatus}
/>
<GameHeader <GameHeader
game={game} game={game}
/> />

View File

@@ -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 AllGamesDisplay from "@/ui/game/AllGamesDisplay";
import { gamesTutorialSteps } from "@/util/TutorialUtil";
export default function GamesPage(){ 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 ( return (
<main <main
className="flex flex-col items-center justify-center gap-8 w-full" className="flex flex-col items-center justify-center gap-y-8 w-full"
> >
<TutorialComponent
steps={gamesTutorialSteps}
run={tutorialsStatus.gamesTutorialStatus === TutorialStatus.NOT_COMPLETED}
updateFunction={updateGamesTutorialStatus}
/>
<h1 <h1
className="text-4xl" className="text-4xl"
> >

View File

@@ -1,6 +1,8 @@
import DangerMessage from "@/components/message/DangerMessage"; import DangerMessage from "@/components/message/DangerMessage";
import TabGroup, { Tab } from "@/components/tab/TabGroup"; import TabGroup, { Tab } from "@/components/tab/TabGroup";
import TutorialComponent from "@/components/TutorialComponent";
import { useGetRaidGroup } from "@/hooks/RaidGroupHooks"; import { useGetRaidGroup } from "@/hooks/RaidGroupHooks";
import { TutorialStatus } from "@/interface/AccountTutorialStatus";
import { RaidGroup } from "@/interface/RaidGroup"; import { RaidGroup } from "@/interface/RaidGroup";
import { useAuth } from "@/providers/AuthProvider"; import { useAuth } from "@/providers/AuthProvider";
import RaidGroupAccountsTab from "@/ui/account/RaidGroupAccountsTab"; import RaidGroupAccountsTab from "@/ui/account/RaidGroupAccountsTab";
@@ -12,17 +14,20 @@ import RaidGroupRequestTab from "@/ui/raidGroupRequest/RaidGroupRequestTab";
import RaidInstanceTab from "@/ui/raidInstance/RaidInstanceTab"; import RaidInstanceTab from "@/ui/raidInstance/RaidInstanceTab";
import RaidLayoutTab from "@/ui/raidLayout/RaidLayoutTab"; import RaidLayoutTab from "@/ui/raidLayout/RaidLayoutTab";
import { isRaidGroupAdmin } from "@/util/PermissionUtil"; import { isRaidGroupAdmin } from "@/util/PermissionUtil";
import { raidGroupTutorialSteps } from "@/util/TutorialUtil";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Navigate, useParams } from "react-router"; import { Navigate, useParams } from "react-router";
export default function RaidGroupPage(){ export default function RaidGroupPage(){
const { raidGroupId } = useParams(); const { raidGroupId } = useParams();
const { accountPermissions, raidGroupPermissions } = useAuth(); const { accountPermissions, raidGroupPermissions, tutorialsStatus, setTutorialsStatus } = useAuth();
const [ raidGroup, setRaidGroup ] = useState<RaidGroup>(); const [ raidGroup, setRaidGroup ] = useState<RaidGroup>();
const raidGroupQuery = useGetRaidGroup(raidGroupId ?? "", false); const raidGroupQuery = useGetRaidGroup(raidGroupId ?? "", false);
useEffect(() => { useEffect(() => {
if(raidGroupQuery.status === "success"){ if(raidGroupQuery.status === "success"){
setRaidGroup(raidGroupQuery.data); setRaidGroup(raidGroupQuery.data);
@@ -30,6 +35,18 @@ export default function RaidGroupPage(){
}, [ raidGroupQuery ]); }, [ 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"){ if(raidGroupQuery.status === "pending"){
return ( return (
<div>Loading...</div> <div>Loading...</div>
@@ -48,22 +65,27 @@ export default function RaidGroupPage(){
else if(raidGroup){ else if(raidGroup){
const tabs: Tab[] = [ const tabs: Tab[] = [
{ {
tabId: "calendarTab",
tabHeader: "Calendar", tabHeader: "Calendar",
tabContent: <RaidGroupCalendarDisplay gameId={raidGroup?.gameId ?? ""} raidGroupId={raidGroupId!}/> tabContent: <RaidGroupCalendarDisplay gameId={raidGroup?.gameId ?? ""} raidGroupId={raidGroupId!}/>
}, },
{ {
tabId: "peopleTab",
tabHeader: "People", tabHeader: "People",
tabContent: <PersonTab raidGroup={raidGroup}/> tabContent: <PersonTab raidGroup={raidGroup}/>
}, },
{ {
tabId: "classGroupsTab",
tabHeader: "Class Groups", tabHeader: "Class Groups",
tabContent: <ClassGroupsTab raidGroup={raidGroup}/> tabContent: <ClassGroupsTab raidGroup={raidGroup}/>
}, },
{ {
tabId: "raidLayoutsTab",
tabHeader: "Raid Layout", tabHeader: "Raid Layout",
tabContent: <RaidLayoutTab raidGroup={raidGroup}/> tabContent: <RaidLayoutTab raidGroup={raidGroup}/>
}, },
{ {
tabId: "instancesTab",
tabHeader: "Raid Instances", tabHeader: "Raid Instances",
tabContent: <RaidInstanceTab raidGroup={raidGroup}/> tabContent: <RaidInstanceTab raidGroup={raidGroup}/>
} }
@@ -71,10 +93,12 @@ export default function RaidGroupPage(){
if(isRaidGroupAdmin(raidGroupId!, raidGroupPermissions, accountPermissions)){ if(isRaidGroupAdmin(raidGroupId!, raidGroupPermissions, accountPermissions)){
tabs.push({ tabs.push({
tabId: "usersTab",
tabHeader: "Users", tabHeader: "Users",
tabContent: <RaidGroupAccountsTab raidGroup={raidGroup}/> tabContent: <RaidGroupAccountsTab raidGroup={raidGroup}/>
}); });
tabs.push({ tabs.push({
tabId: "requestsTab",
tabHeader: "Requests", tabHeader: "Requests",
tabContent: <RaidGroupRequestTab raidGroup={raidGroup}/> tabContent: <RaidGroupRequestTab raidGroup={raidGroup}/>
}); });
@@ -85,6 +109,15 @@ export default function RaidGroupPage(){
<main <main
className="flex flex-col items-center justify-center gap-y-8" className="flex flex-col items-center justify-center gap-y-8"
> >
<TutorialComponent
steps={raidGroupTutorialSteps}
run={tutorialsStatus.raidGroupTutorialStatus === TutorialStatus.NOT_COMPLETED}
showProgress={true}
showSkipButton={true}
continuous={true}
disableScrolling={true}
updateFunction={updateRaidGroupTutorialStatus}
/>
<RaidGroupHeader <RaidGroupHeader
raidGroup={raidGroup} raidGroup={raidGroup}
/> />

View File

@@ -1,15 +1,35 @@
import TutorialComponent from "@/components/TutorialComponent";
import { TutorialStatus } from "@/interface/AccountTutorialStatus";
import { useAuth } from "@/providers/AuthProvider"; import { useAuth } from "@/providers/AuthProvider";
import RaidGroupsByAccountDisplay from "@/ui/raidGroup/RaidGroupsByAccountDisplay"; import RaidGroupsByAccountDisplay from "@/ui/raidGroup/RaidGroupsByAccountDisplay";
import { raidGroupsTutorialSteps } from "@/util/TutorialUtil";
export default function RaidGroupsPage(){ 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 ( return (
<main <main
className="flex flex-col items-center justify-center gap-8" className="flex flex-col items-center justify-center gap-8"
> >
<TutorialComponent
steps={raidGroupsTutorialSteps}
run={tutorialsStatus.raidGroupsTutorialStatus === TutorialStatus.NOT_COMPLETED}
updateFunction={updateRaidGroupsTutorialStatus}
/>
<h1 <h1
className="text-4xl" className="text-4xl"
> >

View File

@@ -1,15 +1,32 @@
import PrimaryButton from "@/components/button/PrimaryButton"; 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 RaidInstanceLayoutProvider from "@/providers/RaidInstanceLayoutProvider";
import RaidInstanceCreatorUI from "@/ui/raidInstance/creator/RaidInstanceCreatorUI"; import RaidInstanceCreatorUI from "@/ui/raidInstance/creator/RaidInstanceCreatorUI";
import { instanceTutorialSteps } from "@/util/TutorialUtil";
import { BsArrowLeft } from "react-icons/bs"; import { BsArrowLeft } from "react-icons/bs";
import { useNavigate, useParams } from "react-router"; import { useNavigate, useParams } from "react-router";
export default function RaidInstancePage(){ export default function RaidInstancePage(){
const { raidGroupId, raidInstanceId } = useParams(); const { raidGroupId, raidInstanceId } = useParams();
const { tutorialsStatus, setTutorialsStatus } = useAuth();
const navigator = useNavigate(); 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 ( return (
<RaidInstanceLayoutProvider <RaidInstanceLayoutProvider
raidGroupId={raidGroupId ?? ""} raidGroupId={raidGroupId ?? ""}
@@ -18,6 +35,15 @@ export default function RaidInstancePage(){
<main <main
className="flex flex-col items-center justify-center" className="flex flex-col items-center justify-center"
> >
<TutorialComponent
steps={instanceTutorialSteps}
run={tutorialsStatus.instanceTutorialStatus === TutorialStatus.NOT_COMPLETED}
showProgress={true}
showSkipButton={true}
continuous={true}
disableScrolling={true}
updateFunction={updateGamesTutorialStatus}
/>
{/* Back to Raid Group Button */} {/* Back to Raid Group Button */}
<div <div
className="flex flex-row items-center justify-start w-full my-8" className="flex flex-row items-center justify-start w-full my-8"

View File

@@ -1,4 +1,6 @@
import { useGetTutorialsStatus, useUpdateTutorialsStatus } from "@/hooks/AccountHooks";
import { AccountPermission } from "@/interface/AccountPermission"; import { AccountPermission } from "@/interface/AccountPermission";
import { AccountTutorialStatus } from "@/interface/AccountTutorialStatus";
import { GamePermission } from "@/interface/GamePermission"; import { GamePermission } from "@/interface/GamePermission";
import { RaidGroupPermission } from "@/interface/RaidGroupPermission"; import { RaidGroupPermission } from "@/interface/RaidGroupPermission";
import { RaidGroupRequest } from "@/interface/RaidGroupRequest"; import { RaidGroupRequest } from "@/interface/RaidGroupRequest";
@@ -23,6 +25,8 @@ type AuthProviderState = {
raidGroupPermissions: RaidGroupPermission[]; raidGroupPermissions: RaidGroupPermission[];
gamePermissions: GamePermission[]; gamePermissions: GamePermission[];
raidGroupRequests: RaidGroupRequest[]; raidGroupRequests: RaidGroupRequest[];
tutorialsStatus: AccountTutorialStatus;
setTutorialsStatus: (tutorialsStatus: AccountTutorialStatus) => void;
} }
const initialState: AuthProviderState = { const initialState: AuthProviderState = {
@@ -34,7 +38,9 @@ const initialState: AuthProviderState = {
accountPermissions: [], accountPermissions: [],
raidGroupPermissions: [], raidGroupPermissions: [],
gamePermissions: [], gamePermissions: [],
raidGroupRequests: [] raidGroupRequests: [],
tutorialsStatus: {} as AccountTutorialStatus,
setTutorialsStatus: () => null
} }
const AuthContext = createContext<AuthProviderState>(initialState); const AuthContext = createContext<AuthProviderState>(initialState);
@@ -51,6 +57,11 @@ export function AuthProvider({
const [ raidGroupPermissions, setRaidGroupPermissions ] = useState<RaidGroupPermission[]>([]); const [ raidGroupPermissions, setRaidGroupPermissions ] = useState<RaidGroupPermission[]>([]);
const [ gamePermissions, setGamePermissions ] = useState<GamePermission[]>([]); const [ gamePermissions, setGamePermissions ] = useState<GamePermission[]>([]);
const [ raidGroupRequests, setRaidGroupRequests ] = useState<RaidGroupRequest[]>([]); const [ raidGroupRequests, setRaidGroupRequests ] = useState<RaidGroupRequest[]>([]);
const [ tutorialsStatus, setTutorialsStatus ] = useState<AccountTutorialStatus>({} as AccountTutorialStatus);
const tutorialsStatusQuery = useGetTutorialsStatus(accountId);
const { mutate: tutorialsStatusMutation } = useUpdateTutorialsStatus();
const fetchToken = useCallback(async () => { const fetchToken = useCallback(async () => {
@@ -112,18 +123,29 @@ export function AuthProvider({
fetchToken(); fetchToken();
}, [ 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(() => ({ const currentTokens = useMemo(() => ({
jwt, jwt, setJwt,
setJwt, expiration, setExpiration,
expiration,
setExpiration,
accountId, accountId,
accountPermissions, accountPermissions,
raidGroupPermissions, raidGroupPermissions,
gamePermissions, gamePermissions,
raidGroupRequests raidGroupRequests,
}), [ jwt, setJwt, expiration, setExpiration, accountId, accountPermissions, raidGroupPermissions, gamePermissions, raidGroupRequests ]); tutorialsStatus, setTutorialsStatus: updateTutorialsStatus
}), [ jwt, expiration, accountId, accountPermissions, raidGroupPermissions, gamePermissions, raidGroupRequests, tutorialsStatus, updateTutorialsStatus ]);
//TODO: Return a spinner while the first token is being fetched //TODO: Return a spinner while the first token is being fetched

View File

@@ -195,6 +195,7 @@ export default function RaidInstanceHeader(){
> >
{ {
<PrimaryButton <PrimaryButton
id="saveInstanceButton"
onClick={saveRaidInstance} onClick={saveRaidInstance}
> >
Save Save

View File

@@ -42,6 +42,7 @@ export default function RaidInstanceCreatorTable({
</div> </div>
{/* Buttons */} {/* Buttons */}
<PrimaryButton <PrimaryButton
id="instanceAddRunButton"
onClick={addRun} onClick={addRun}
> >
Add Run Add Run

View File

@@ -104,6 +104,7 @@ export default function RaidInstanceCreatorTableBody({
className="flex flex-row justify-center items-center cursor-pointer p-2 space-x-2" className="flex flex-row justify-center items-center cursor-pointer p-2 space-x-2"
> >
<DangerButton <DangerButton
id={`removeRun${runIndex}`}
variant="ghost" variant="ghost"
shape="square" shape="square"
onClick={() => removeRun(runIndex)} onClick={() => removeRun(runIndex)}
@@ -114,6 +115,7 @@ export default function RaidInstanceCreatorTableBody({
/> />
</DangerButton> </DangerButton>
<TertiaryButton <TertiaryButton
id={`copyDiscordString${runIndex}`}
variant="ghost" variant="ghost"
shape="square" shape="square"
onClick={() => copyDiscordStringToClipBoard(runIndex)} onClick={() => copyDiscordStringToClipBoard(runIndex)}

253
src/util/TutorialUtil.ts Normal file
View File

@@ -0,0 +1,253 @@
import { Step } from "react-joyride";
const nextButtonStyles = {
backgroundColor: "#0000FF"
};
const backButtonStyles = {
color: "#0000FF"
}
export const gamesTutorialSteps: Array<Step> = [
{
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<Step> = [
{
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<Step> = [
{
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<Step> = [
{
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<Step> = [
{
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
}
}
];