Tutorial Working
This commit is contained in:
17
src/components/TutorialComponent.tsx
Normal file
17
src/components/TutorialComponent.tsx
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -33,8 +33,6 @@ export default function ProtectedNavLinks(){
|
|||||||
path: "/logout"
|
path: "/logout"
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("is admin = " + isSiteAdmin(accountPermissions));
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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){
|
||||||
|
|||||||
15
src/interface/AccountTutorialStatus.ts
Normal file
15
src/interface/AccountTutorialStatus.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ export default function RaidInstanceHeader(){
|
|||||||
>
|
>
|
||||||
{
|
{
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
id="saveInstanceButton"
|
||||||
onClick={saveRaidInstance}
|
onClick={saveRaidInstance}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export default function RaidInstanceCreatorTable({
|
|||||||
</div>
|
</div>
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
id="instanceAddRunButton"
|
||||||
onClick={addRun}
|
onClick={addRun}
|
||||||
>
|
>
|
||||||
Add Run
|
Add Run
|
||||||
|
|||||||
@@ -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
253
src/util/TutorialUtil.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user