mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-04-02 03:00:32 -04:00
Modified setupOptions data structure to be more straightforward, consolidated step logic
This commit is contained in:
@@ -7,20 +7,21 @@ import { useSetupFlow } from 'contexts/SetupContext';
|
||||
import React from 'react';
|
||||
|
||||
export const SetupHeader: React.FC = () => {
|
||||
const { step, screen, onNextPress, onBackPress, options } = useSetupFlow();
|
||||
const { stepIndex, onNextPress, onBackPress, options } = useSetupFlow();
|
||||
|
||||
const {sectionIndex} = options.steps[stepIndex];
|
||||
|
||||
return (
|
||||
<Grid templateColumns="0.5fr 1fr 1fr 1fr 0.5fr" gap="1rem" w="100%">
|
||||
<FlexContainer justify="flex-end" onClick={onBackPress} cursor="pointer">
|
||||
<Image src={BackImage} h="1rem" alt="Back" />
|
||||
</FlexContainer>
|
||||
{options.map((option, id) => (
|
||||
<StepProgress
|
||||
{options.sections.map((option, id) => (
|
||||
<SectionProgress
|
||||
key={option.label}
|
||||
title={option.title}
|
||||
step={id}
|
||||
isActive={step === id}
|
||||
isDone={step > id}
|
||||
screen={screen}
|
||||
isActive={sectionIndex === id}
|
||||
isDone={sectionIndex > id}
|
||||
/>
|
||||
))}
|
||||
<FlexContainer justify="flex-end" onClick={onNextPress} cursor="pointer">
|
||||
@@ -34,22 +35,16 @@ interface StepProps {
|
||||
title: { [any: string]: string | undefined };
|
||||
isDone: boolean;
|
||||
isActive: boolean;
|
||||
step: number;
|
||||
screen: number;
|
||||
}
|
||||
|
||||
export const StepProgress: React.FC<StepProps> = ({
|
||||
export const SectionProgress: React.FC<StepProps> = ({
|
||||
title,
|
||||
isDone,
|
||||
isActive,
|
||||
step,
|
||||
screen,
|
||||
}) => {
|
||||
const { options } = useSetupFlow();
|
||||
const { options, stepIndex } = useSetupFlow();
|
||||
|
||||
const progress = isDone
|
||||
? 100
|
||||
: Math.floor(((screen + 1) * 100.0) / options[step].screens.length);
|
||||
const progress = isDone ? 100 : options.progressWithinSection(stepIndex);
|
||||
return (
|
||||
<FlexContainer pos="relative">
|
||||
<ResponsiveText
|
||||
|
||||
@@ -4,14 +4,11 @@ import { useSetupFlow } from 'contexts/SetupContext';
|
||||
import React from 'react';
|
||||
|
||||
export const SetupProfile: React.FC = ({children}) => {
|
||||
const {
|
||||
step,
|
||||
numTotalSteps
|
||||
} = useSetupFlow();
|
||||
const {options, stepIndex} = useSetupFlow();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
{(step + 1) % numTotalSteps !== 0 && <SetupHeader />}
|
||||
{options.numSteps - 1 > stepIndex && <SetupHeader />}
|
||||
<FlexContainer flex={1} pt={24}>
|
||||
{children}
|
||||
</FlexContainer>
|
||||
|
||||
@@ -1,99 +1,73 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { SetupOptions } from 'utils/setupOptions';
|
||||
|
||||
type SetupOption = {
|
||||
label: string;
|
||||
title: {
|
||||
[any: string]: string | undefined;
|
||||
};
|
||||
screens: Array<{
|
||||
label: string;
|
||||
component?: React.ReactNode;
|
||||
slug?: string;
|
||||
}>;
|
||||
};
|
||||
const urlPrefix = `/profile/setup/`;
|
||||
|
||||
type SetupContextType = {
|
||||
options: Array<SetupOption>;
|
||||
step: number;
|
||||
screen: number;
|
||||
options: SetupOptions;
|
||||
stepIndex: number;
|
||||
onNextPress: () => void;
|
||||
onBackPress: () => void;
|
||||
nextButtonLabel: string;
|
||||
numTotalSteps: number;
|
||||
};
|
||||
|
||||
export const SetupContext = React.createContext<SetupContextType>({
|
||||
options: [],
|
||||
step: 0,
|
||||
screen: 0,
|
||||
options: new SetupOptions(),
|
||||
stepIndex: 0,
|
||||
onNextPress: () => undefined,
|
||||
onBackPress: () => undefined,
|
||||
nextButtonLabel: 'Next Step',
|
||||
numTotalSteps: 0,
|
||||
});
|
||||
|
||||
type Props = {
|
||||
options: Array<SetupOption>;
|
||||
};
|
||||
export const SetupContextProvider: React.FC = ({children}) => {
|
||||
const options = useMemo(() => {
|
||||
return new SetupOptions();
|
||||
}, []);
|
||||
|
||||
export const SetupContextProvider: React.FC<Props> = ({
|
||||
children,
|
||||
options
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [step, setStep] = useState<number>(0);
|
||||
const [screen, setScreen] = useState<number>(0);
|
||||
const numTotalSteps = options.length;
|
||||
|
||||
const pageMatches = router.pathname.match(`${urlPrefix}(.+)`);
|
||||
const slug = pageMatches != null && pageMatches.length > 1 ? pageMatches[1] : null;
|
||||
const stepIndex = options.stepIndexMatchingSlug(slug);
|
||||
const currentStep = options.steps[stepIndex];
|
||||
|
||||
const [nextButtonLabel, setNextButtonLabel] = useState('Next Step');
|
||||
|
||||
useEffect(() => {
|
||||
const numScreens = options[step].screens.length;
|
||||
if (step >= numTotalSteps - 1) {
|
||||
setNextButtonLabel(options[(step + 1) % numTotalSteps].label);
|
||||
}
|
||||
if (screen + 1 >= numScreens) {
|
||||
setNextButtonLabel(`Next: ${options[(step + 1) % numTotalSteps].label}`);
|
||||
if (options.isLastStep(stepIndex)) {
|
||||
setNextButtonLabel(currentStep.label);
|
||||
} else {
|
||||
setNextButtonLabel(
|
||||
`Next: ${options[step].screens[(screen + 1) % numScreens].label}`,
|
||||
);
|
||||
const nextStep = options.steps[stepIndex + 1];
|
||||
let nextStepLabel = nextStep.label;
|
||||
if (options.isFinalStepOfSection(stepIndex)) {
|
||||
nextStepLabel = options.sections[nextStep.sectionIndex].label;
|
||||
}
|
||||
setNextButtonLabel(`Next: ${nextStepLabel}`);
|
||||
}
|
||||
}, [options, step, screen, setNextButtonLabel, numTotalSteps]);
|
||||
}, [options, stepIndex, setNextButtonLabel, currentStep]);
|
||||
|
||||
const onNextPress = useCallback(() => {
|
||||
const numScreens = options[step].screens.length;
|
||||
if (step >= numTotalSteps - 1 && screen >= numScreens - 1) return;
|
||||
if (screen + 1 >= numScreens) {
|
||||
setStep((step + 1) % numTotalSteps);
|
||||
setScreen(0);
|
||||
} else {
|
||||
setScreen((screen + 1) % numScreens);
|
||||
if (!options.isLastStep(stepIndex)) {
|
||||
const nextStep = options.steps[stepIndex + 1];
|
||||
router.push(`${urlPrefix}${nextStep.slug}`);
|
||||
}
|
||||
router.push(`/profile/setup/${options[step].screens[(screen + 1) % numScreens].slug}`);
|
||||
}, [router, options, step, screen, setStep, setScreen, numTotalSteps]);
|
||||
}, [router, options, stepIndex]);
|
||||
|
||||
const onBackPress = useCallback(() => {
|
||||
if (step <= 0 && screen <= 0) {
|
||||
if (stepIndex <= 0) {
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
const numScreens = options[step].screens.length;
|
||||
if (screen <= 0) {
|
||||
setStep((step - 1) % numTotalSteps);
|
||||
setScreen(options[(step - 1) % numTotalSteps].screens.length - 1);
|
||||
} else {
|
||||
setScreen((screen - 1) % numScreens);
|
||||
const previousStep = options.steps[stepIndex - 1];
|
||||
router.push(`${urlPrefix}${previousStep.slug}`);
|
||||
}
|
||||
}, [router, options, step, screen, setStep, setScreen, numTotalSteps]);
|
||||
}, [router, options, stepIndex]);
|
||||
|
||||
return (
|
||||
<SetupContext.Provider
|
||||
value={{
|
||||
options,
|
||||
step,
|
||||
screen,
|
||||
numTotalSteps,
|
||||
stepIndex,
|
||||
onNextPress,
|
||||
onBackPress,
|
||||
nextButtonLabel,
|
||||
|
||||
@@ -2,10 +2,20 @@ import { SetupAvailability } from 'components/Setup/SetupAvailability';
|
||||
import { SetupProfile } from 'components/Setup/SetupProfile';
|
||||
import { SetupContextProvider } from 'contexts/SetupContext';
|
||||
import { useUser } from 'lib/hooks';
|
||||
import { InferGetStaticPropsType } from 'next';
|
||||
import React, { useState } from 'react';
|
||||
import { options as setupOptions } from 'utils/setupOptions';
|
||||
|
||||
const AvailabilitySetup: React.FC = () => {
|
||||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
hideAppDrawer: true
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
|
||||
|
||||
const AvailabilitySetup: React.FC<DefaultSetupProps> = () => {
|
||||
|
||||
const [availability, setAvailability] = useState<string>('');
|
||||
const { user } = useUser({ redirectTo: '/' });
|
||||
@@ -18,7 +28,7 @@ const AvailabilitySetup: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SetupContextProvider options={setupOptions}>
|
||||
<SetupContextProvider>
|
||||
<SetupProfile>
|
||||
<SetupAvailability availability={availability} setAvailability={setAvailability} />
|
||||
</SetupProfile>
|
||||
|
||||
26
packages/web/pages/profile/setup/complete.tsx
Normal file
26
packages/web/pages/profile/setup/complete.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { SetupDone } from 'components/Setup/SetupDone';
|
||||
import { SetupProfile } from 'components/Setup/SetupProfile';
|
||||
import { SetupContextProvider } from 'contexts/SetupContext';
|
||||
import { InferGetStaticPropsType } from 'next';
|
||||
import React from 'react';
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
hideAppDrawer: true
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
|
||||
|
||||
const SetupComplete: React.FC<DefaultSetupProps> = () => {
|
||||
return (
|
||||
<SetupContextProvider>
|
||||
<SetupProfile>
|
||||
<SetupDone />
|
||||
</SetupProfile>
|
||||
</SetupContextProvider>
|
||||
);
|
||||
};
|
||||
export default SetupComplete;
|
||||
@@ -4,10 +4,20 @@ import { SetupContextProvider } from 'contexts/SetupContext';
|
||||
import { getMemberships } from 'graphql/getMemberships';
|
||||
import { Membership } from 'graphql/types';
|
||||
import { useWeb3 } from 'lib/hooks';
|
||||
import { InferGetStaticPropsType } from 'next';
|
||||
import React, { useState } from 'react';
|
||||
import { options as setupOptions } from 'utils/setupOptions';
|
||||
|
||||
const MembershipsSetup: React.FC = () => {
|
||||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
hideAppDrawer: true
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
|
||||
|
||||
const MembershipsSetup: React.FC<DefaultSetupProps> = () => {
|
||||
|
||||
const [memberships, setMemberships] = useState<
|
||||
Array<Membership> | null | undefined
|
||||
@@ -19,7 +29,7 @@ const MembershipsSetup: React.FC = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<SetupContextProvider options={setupOptions}>
|
||||
<SetupContextProvider>
|
||||
<SetupProfile>
|
||||
<SetupMemberships memberships={memberships} setMemberships={setMemberships} />
|
||||
</SetupProfile>
|
||||
|
||||
@@ -6,14 +6,14 @@ import { PersonalityType, PersonalityTypes } from 'graphql/types';
|
||||
import { useUser } from 'lib/hooks';
|
||||
import { InferGetStaticPropsType } from 'next';
|
||||
import React, { useState } from 'react';
|
||||
import { options as setupOptions } from 'utils/setupOptions';
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
const personalityTypeChoices = await getPersonalityTypes();
|
||||
|
||||
return {
|
||||
props: {
|
||||
personalityTypeChoices
|
||||
personalityTypeChoices,
|
||||
hideAppDrawer: true
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -34,7 +34,7 @@ const PersonalityTypeSetup: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SetupContextProvider options={setupOptions}>
|
||||
<SetupContextProvider>
|
||||
<SetupProfile>
|
||||
<SetupPersonalityType
|
||||
personalityTypeChoices={personalityTypeChoices}
|
||||
|
||||
@@ -6,15 +6,14 @@ import { getPlayerTypes } from 'graphql/getPlayerTypes';
|
||||
import { useUser } from 'lib/hooks';
|
||||
import { InferGetStaticPropsType } from 'next';
|
||||
import React, { useState } from 'react';
|
||||
import { options as setupOptions } from 'utils/setupOptions';
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
const playerTypeChoices = await getPlayerTypes();
|
||||
|
||||
return {
|
||||
props: {
|
||||
hideAppDrawer: true,
|
||||
playerTypeChoices
|
||||
playerTypeChoices,
|
||||
hideAppDrawer: true
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -34,7 +33,7 @@ const PlayerTypeSetup: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SetupContextProvider options={setupOptions}>
|
||||
<SetupContextProvider>
|
||||
<SetupProfile>
|
||||
<SetupPlayerType
|
||||
playerTypeChoices={playerTypeChoices}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { getSkills } from 'graphql/getSkills';
|
||||
import { useUser } from 'lib/hooks';
|
||||
import { InferGetStaticPropsType } from 'next';
|
||||
import React, { useState } from 'react';
|
||||
import { options as setupOptions } from 'utils/setupOptions';
|
||||
import { parseSkills, SkillOption } from 'utils/skillHelpers';
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
@@ -14,7 +13,8 @@ export const getStaticProps = async () => {
|
||||
|
||||
return {
|
||||
props: {
|
||||
skillChoices
|
||||
skillChoices,
|
||||
hideAppDrawer: true
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -44,7 +44,7 @@ const SkillsSetup: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SetupContextProvider options={setupOptions}>
|
||||
<SetupContextProvider>
|
||||
<SetupProfile>
|
||||
<SetupSkills
|
||||
skillChoices={skillChoices}
|
||||
|
||||
@@ -2,10 +2,20 @@ import { SetupProfile } from 'components/Setup/SetupProfile';
|
||||
import { SetupTimeZone } from 'components/Setup/SetupTimeZone';
|
||||
import { SetupContextProvider } from 'contexts/SetupContext';
|
||||
import { useUser } from 'lib/hooks';
|
||||
import { InferGetStaticPropsType } from 'next';
|
||||
import React, { useState } from 'react';
|
||||
import { options as setupOptions } from 'utils/setupOptions';
|
||||
|
||||
const TimeZoneSetup: React.FC = () => {
|
||||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
hideAppDrawer: true
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
|
||||
|
||||
const TimeZoneSetup: React.FC<DefaultSetupProps> = () => {
|
||||
|
||||
const [timeZone, setTimeZone] = useState<string>('');
|
||||
const { user } = useUser({ redirectTo: '/' });
|
||||
@@ -18,7 +28,7 @@ const TimeZoneSetup: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SetupContextProvider options={setupOptions}>
|
||||
<SetupContextProvider>
|
||||
<SetupProfile>
|
||||
<SetupTimeZone timeZone={timeZone} setTimeZone={setTimeZone} />
|
||||
</SetupProfile>
|
||||
|
||||
@@ -2,11 +2,20 @@ import { SetupProfile } from 'components/Setup/SetupProfile';
|
||||
import { SetupUsername } from 'components/Setup/SetupUsername';
|
||||
import { SetupContextProvider } from 'contexts/SetupContext';
|
||||
import { useUser, useWeb3 } from 'lib/hooks';
|
||||
import { InferGetStaticPropsType } from 'next';
|
||||
import React, { useState } from 'react';
|
||||
import { options as setupOptions } from 'utils/setupOptions';
|
||||
|
||||
const UsernameSetup: React.FC = () => {
|
||||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
hideAppDrawer: true
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export type DefaultSetupProps = InferGetStaticPropsType<typeof getStaticProps>;
|
||||
|
||||
const UsernameSetup: React.FC<DefaultSetupProps> = () => {
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const { address } = useWeb3();
|
||||
const { user } = useUser({ redirectTo: '/' });
|
||||
@@ -23,7 +32,7 @@ const UsernameSetup: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SetupContextProvider options={setupOptions}>
|
||||
<SetupContextProvider>
|
||||
<SetupProfile>
|
||||
<SetupUsername username={username} setUsername={setUsername} />
|
||||
</SetupProfile>
|
||||
|
||||
@@ -1,63 +1,104 @@
|
||||
import { SetupDone } from 'components/Setup/SetupDone';
|
||||
import React from 'react';
|
||||
|
||||
export const options = [
|
||||
{
|
||||
label: 'About You',
|
||||
title: { base: 'About You', sm: '1. About You' },
|
||||
screens: [
|
||||
{
|
||||
label: 'Username',
|
||||
slug: 'username'
|
||||
export type SetupStep = {
|
||||
label: string;
|
||||
slug?: string;
|
||||
sectionIndex: number;
|
||||
}
|
||||
|
||||
export type SetupSection = {
|
||||
label: string;
|
||||
title: {
|
||||
[any: string]: string | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export class SetupOptions {
|
||||
sections: SetupSection[] = [
|
||||
{
|
||||
label: 'About You',
|
||||
title: { base: 'About You', sm: '1. About You' }
|
||||
}, {
|
||||
label: 'Profile',
|
||||
title: {
|
||||
base: 'Profile',
|
||||
sm: '2. Profile',
|
||||
lg: '2. Professional Profile',
|
||||
}
|
||||
}, {
|
||||
label: 'Start Playing',
|
||||
title: {
|
||||
base: 'Play',
|
||||
sm: '3. Play',
|
||||
md: '3. Start Playing',
|
||||
},
|
||||
{
|
||||
label: 'Personality Type',
|
||||
slug: 'personalityType'
|
||||
},
|
||||
{
|
||||
label: 'Player Type',
|
||||
slug: 'playerType',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Profile',
|
||||
title: {
|
||||
base: 'Profile',
|
||||
sm: '2. Profile',
|
||||
lg: '2. Professional Profile',
|
||||
},
|
||||
screens: [
|
||||
{
|
||||
label: 'Skills',
|
||||
slug: 'skills'
|
||||
},
|
||||
{
|
||||
label: 'Availability',
|
||||
slug: 'availability',
|
||||
},
|
||||
{
|
||||
label: 'Time Zone',
|
||||
slug: 'timeZone'
|
||||
},
|
||||
{
|
||||
label: 'Memberships',
|
||||
slug: 'memberships',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Start Playing',
|
||||
title: {
|
||||
base: 'Play',
|
||||
sm: '3. Play',
|
||||
md: '3. Start Playing',
|
||||
},
|
||||
screens: [
|
||||
{
|
||||
label: 'Done',
|
||||
component: <SetupDone />,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
steps: SetupStep[] = [
|
||||
{
|
||||
label: 'Username',
|
||||
slug: 'username',
|
||||
sectionIndex: 0
|
||||
}, {
|
||||
label: 'Personality Type',
|
||||
slug: 'personalityType',
|
||||
sectionIndex: 0
|
||||
}, {
|
||||
label: 'Player Type',
|
||||
slug: 'playerType',
|
||||
sectionIndex: 0
|
||||
}, {
|
||||
label: 'Skills',
|
||||
slug: 'skills',
|
||||
sectionIndex: 1
|
||||
}, {
|
||||
label: 'Availability',
|
||||
slug: 'availability',
|
||||
sectionIndex: 1
|
||||
}, {
|
||||
label: 'Time Zone',
|
||||
slug: 'timeZone',
|
||||
sectionIndex: 1
|
||||
}, {
|
||||
label: 'Memberships',
|
||||
slug: 'memberships',
|
||||
sectionIndex: 1
|
||||
}, {
|
||||
label: 'Start Playing',
|
||||
slug: 'complete',
|
||||
sectionIndex: 2
|
||||
}
|
||||
]
|
||||
|
||||
stepIndexMatchingSlug(slug: string | null): number {
|
||||
return this.steps.findIndex(step => step.slug === slug);
|
||||
};
|
||||
|
||||
get numSteps(): number {
|
||||
return this.steps.length;
|
||||
}
|
||||
|
||||
isLastStep(stepIndex: number): boolean {
|
||||
return stepIndex >= this.numSteps - 1;
|
||||
}
|
||||
|
||||
isFinalStepOfSection(stepIndex: number): boolean {
|
||||
if (this.isLastStep(stepIndex)) return true;
|
||||
return this.steps[stepIndex].sectionIndex !== this.steps[stepIndex + 1].sectionIndex;
|
||||
}
|
||||
|
||||
progressWithinSection(stepIndex: number): number {
|
||||
const stepSectionIndex = this.steps[stepIndex].sectionIndex;
|
||||
let stepsCompletedInSection = 0;
|
||||
const stepsInSection = this.steps.reduce((count:number, step:SetupStep, index:number) => {
|
||||
if (stepIndex === index) {
|
||||
stepsCompletedInSection = count;
|
||||
}
|
||||
if (step.sectionIndex === stepSectionIndex) {
|
||||
return count + 1;
|
||||
}
|
||||
return count;
|
||||
}, 0);
|
||||
return Math.floor((stepsCompletedInSection + 1) * 100.0) / stepsInSection;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user