Walk Create Community through the Figma screens and restore a chosen logo after reload, so a guest can leave without saving until they reach the email step.
This commit is contained in:
1 parent
ca1f58a50c
commit
2348a9e94f
61 files changed
+1110
-421
No files matched your search
@@ -57,4 +57,18 @@ describe("Alert dismiss control", () => {
|
||||
screen.queryByRole("button", { name: "Close" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an action control when actionLabel and onAction are set", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onAction = vi.fn();
|
||||
render(
|
||||
<Alert
|
||||
title="Check your email"
|
||||
actionLabel="Send it again"
|
||||
onAction={onAction}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "Send it again" }));
|
||||
expect(onAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -8,21 +8,25 @@ describe("CommunityStructureSelectScreen", () => {
|
||||
it("does not render dummy help icons", () => {
|
||||
renderWithProviders(<CommunityStructureSelectScreen />);
|
||||
|
||||
expect(screen.getByText("Organization Type")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("What kind of community are you, mainly?"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Anything else that fits?")).toBeInTheDocument();
|
||||
expect(screen.getByText("Scale")).toBeInTheDocument();
|
||||
expect(screen.getByText("Maturity")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Choose tags that describe your community/i),
|
||||
screen.getByText(/Your answers shape which cards we suggest/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByAltText("Help")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Pick one")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("gives structure chips Select/Deselect accessible names", () => {
|
||||
renderWithProviders(<CommunityStructureSelectScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Select Worker co-op" }),
|
||||
).toBeInTheDocument();
|
||||
screen.getAllByRole("button", { name: "Select Worker co-op" }).length,
|
||||
).toBeGreaterThanOrEqual(2);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Select Local" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
@@ -50,7 +50,7 @@ describe("CreateFlowFooter (behavioral tests)", () => {
|
||||
});
|
||||
expect(footer).toBeInTheDocument();
|
||||
const bar = screen.getByRole("progressbar");
|
||||
expect(bar).toHaveAttribute("aria-valuenow", String(1 / 6));
|
||||
expect(bar).toHaveAttribute("aria-valuenow", String(1 / 9));
|
||||
});
|
||||
|
||||
it("passes proportionBarProgress to the progress bar", () => {
|
||||
@@ -63,7 +63,7 @@ describe("CreateFlowFooter (behavioral tests)", () => {
|
||||
);
|
||||
expect(screen.getByRole("progressbar")).toHaveAttribute(
|
||||
"aria-valuenow",
|
||||
String(2 / 6),
|
||||
String(2 / 9),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -66,9 +66,9 @@ describe("CreateFlowTopNav (behavioral tests)", () => {
|
||||
expect(exitButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows Save & Exit when saveDraftOnExit is true", () => {
|
||||
it("shows Save and exit when saveDraftOnExit is true", () => {
|
||||
render(<CreateFlowTopNav saveDraftOnExit={true} />);
|
||||
const exitButton = screen.getByRole("button", { name: "Save & Exit" });
|
||||
const exitButton = screen.getByRole("button", { name: "Save and exit" });
|
||||
expect(exitButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -232,7 +232,7 @@ describe("CreateFlowTopNav (viewport < sm2 / 440px)", () => {
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Edit" })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Save & Exit" }),
|
||||
screen.queryByRole("button", { name: "Save and exit" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -255,7 +255,7 @@ describe("CreateFlowTopNav (viewport < sm2 / 440px)", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("menuitem", { name: "Edit" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("menuitem", { name: "Save & Exit" }),
|
||||
screen.getByRole("menuitem", { name: "Save and exit" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -284,7 +284,7 @@ describe("CreateFlowTopNav (viewport < sm2 / 440px)", () => {
|
||||
expect(handleEdit).toHaveBeenCalledTimes(1);
|
||||
|
||||
await user.click(kebab);
|
||||
await user.click(screen.getByRole("menuitem", { name: "Save & Exit" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: "Save and exit" }));
|
||||
expect(handleExit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ describe("InformationalScreen", () => {
|
||||
render(<InformationalScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "How CommunityRule helps groups like yours",
|
||||
name: "Write your community’s Rule",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -17,7 +17,7 @@ describe("InformationalScreen", () => {
|
||||
render(<InformationalScreen />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/This flow will give you recommendations to improve your community/i,
|
||||
/Work through it together or on your own/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -30,7 +30,7 @@ describe("InformationalScreen", () => {
|
||||
it("renders first numbered list item title", () => {
|
||||
render(<InformationalScreen />);
|
||||
expect(
|
||||
screen.getByText("Tell us about your organization"),
|
||||
screen.getByText("Tell us about your community"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -252,4 +252,27 @@ describe("MultiSelect – behaviour specifics", () => {
|
||||
|
||||
expect(handleClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a danger hint under the chips when errorMessage is set", () => {
|
||||
render(
|
||||
<MultiSelect
|
||||
options={defaultChipOptions}
|
||||
errorMessage="Pick one to continue."
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("alert")).toHaveTextContent(
|
||||
"Pick one to continue.",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows helper text beside the label", () => {
|
||||
render(
|
||||
<MultiSelect
|
||||
label="Main kind"
|
||||
helperText="Pick one"
|
||||
options={defaultChipOptions}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Pick one")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -61,8 +61,9 @@ describe("ProportionBar (behavioral tests)", () => {
|
||||
|
||||
it("handles all proportion states correctly", () => {
|
||||
const testCases = [
|
||||
{ progress: "1-0" as const, expected: 1 / 6 }, // First section 1/6 filled
|
||||
{ progress: "1-5" as const, expected: 1 }, // First section 6/6 filled (fully filled)
|
||||
{ progress: "1-0" as const, expected: 1 / 9 },
|
||||
{ progress: "1-5" as const, expected: 6 / 9 },
|
||||
{ progress: "1-8" as const, expected: 1 },
|
||||
{ progress: "2-0" as const, expected: 1 }, // First section full, second empty
|
||||
{ progress: "2-2" as const, expected: 1 + 1 / 2 }, // 1 + 1/2 per Figma `18861:15250`
|
||||
{ progress: "2-3" as const, expected: 1 + 3 / 4 }, // 1 + 3/4 per Figma `21434:17632`
|
||||
|
||||
@@ -22,6 +22,23 @@ function ReviewWithTitle({ title }: { title: string }) {
|
||||
return <CommunityReviewScreen />;
|
||||
}
|
||||
|
||||
function ReviewWithAnswers() {
|
||||
const { state, updateState } = useCreateFlow();
|
||||
const seededRef = React.useRef(false);
|
||||
useEffect(() => {
|
||||
if (seededRef.current) return;
|
||||
seededRef.current = true;
|
||||
updateState({
|
||||
title: "Garden Club",
|
||||
selectedOrganizationTypeIds: ["mutualAid"],
|
||||
selectedCommunitySizeIds: ["thirteenToOneHundred"],
|
||||
selectedMaturityIds: ["established"],
|
||||
});
|
||||
}, [updateState]);
|
||||
if (state.title !== "Garden Club") return null;
|
||||
return <CommunityReviewScreen />;
|
||||
}
|
||||
|
||||
describe("CommunityReviewScreen", () => {
|
||||
beforeEach(() => {
|
||||
testRouter.replace.mockReset();
|
||||
@@ -53,7 +70,7 @@ describe("CommunityReviewScreen", () => {
|
||||
render(<ReviewWithTitle title="Garden Club" />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "Your community is added - congrats!",
|
||||
name: "Here is your community",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -62,7 +79,7 @@ describe("CommunityReviewScreen", () => {
|
||||
render(<ReviewWithTitle title="Garden Club" />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/In the next section, we'll go through membership, decision-making, conflict resolution, and community values and create a custom operating manual for your organization based on the specifics you just shared./i,
|
||||
/Next you will write your Rule/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -81,6 +98,17 @@ describe("CommunityReviewScreen", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("lists kind, size, and maturity with Edit controls", () => {
|
||||
render(<ReviewWithAnswers />);
|
||||
expect(screen.getByText("Kind")).toBeInTheDocument();
|
||||
expect(screen.getByText("Mutual aid")).toBeInTheDocument();
|
||||
expect(screen.getByText("Size")).toBeInTheDocument();
|
||||
expect(screen.getByText("13 to 100 people")).toBeInTheDocument();
|
||||
expect(screen.getByText("Maturity")).toBeInTheDocument();
|
||||
expect(screen.getByText("Established")).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("button", { name: "Edit" })).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("renders Rule as a button when a community name exists", () => {
|
||||
render(<ReviewWithTitle title="Garden Club" />);
|
||||
const buttons = screen.getAllByRole("button");
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("CreateFlowTextFieldScreen (community name)", () => {
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByText("This will be the name of your community."),
|
||||
screen.getByText("Use the name your members know. You can change this later."),
|
||||
).toBeInTheDocument();
|
||||
const input = screen.getByRole("textbox", { name: "Community name" });
|
||||
expect(input).toBeInTheDocument();
|
||||
@@ -46,18 +46,29 @@ describe("CreateFlowTextFieldScreen (community description)", () => {
|
||||
<CreateFlowTextFieldScreen
|
||||
messageNamespace="create.community.communityContext"
|
||||
stateField="communityContext"
|
||||
locationField="communityLocation"
|
||||
maxLength={200}
|
||||
multiline
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByText(/Optional\. Write a short paragraph/i),
|
||||
screen.getByText(
|
||||
"For example, we cook and share free meals with neighbors every Sunday.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
const field = screen.getByRole("textbox", {
|
||||
name: "Community description",
|
||||
});
|
||||
expect(field.tagName).toBe("TEXTAREA");
|
||||
expect(field).not.toBeRequired();
|
||||
const location = screen.getByRole("textbox", {
|
||||
name: "Where is your community based?",
|
||||
});
|
||||
expect(location).toHaveAttribute("placeholder", "City or region");
|
||||
expect(location).not.toBeRequired();
|
||||
expect(
|
||||
screen.getByText("Optional. A city or region is enough."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -109,4 +109,20 @@ describe("Upload (behavioral tests)", () => {
|
||||
"text-[color:var(--color-content-default-tertiary)]",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a round preview, Replace, and filename when previewSrc is set", () => {
|
||||
render(
|
||||
<Upload
|
||||
previewSrc="blob:logo"
|
||||
previewAlt="Selected community logo preview"
|
||||
fileName="logo.png"
|
||||
buttonLabel="Replace"
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("img", { name: "Selected community logo preview" }),
|
||||
).toHaveAttribute("src", "blob:logo");
|
||||
expect(screen.getByRole("button", { name: "Replace" })).toBeInTheDocument();
|
||||
expect(screen.getByText("logo.png")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
fireEvent,
|
||||
cleanup,
|
||||
} from "../utils/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { CommunityUploadScreen } from "../../app/(app)/create/screens/upload/CommunityUploadScreen";
|
||||
import {
|
||||
@@ -70,7 +69,7 @@ describe("CommunityUploadScreen", () => {
|
||||
render(<CommunityUploadScreen />);
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "Add a photo to identify your group",
|
||||
name: "Add a logo",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -80,13 +79,12 @@ describe("CommunityUploadScreen", () => {
|
||||
expect(screen.getByRole("button", { name: "Upload" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/This photo will be used as a profile picture for your group/i,
|
||||
/Only people who can read your Rule can see this image/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("restores a pending IndexedDB photo and lets the user remove it", async () => {
|
||||
const user = userEvent.setup();
|
||||
it("restores a pending IndexedDB photo into the uploaded preview", async () => {
|
||||
const pending = new File([pngBytes], "avatar.png", { type: "image/png" });
|
||||
vi.mocked(readPendingCommunityAvatarFile).mockResolvedValue(pending);
|
||||
|
||||
@@ -94,22 +92,15 @@ describe("CommunityUploadScreen", () => {
|
||||
|
||||
const preview = await screen.findByRole("img", { name: copy.previewAlt });
|
||||
expect(preview).toHaveAttribute("src", "blob:pending-avatar");
|
||||
|
||||
const remove = screen.getByRole("button", {
|
||||
name: copy.clearPendingUploadAriaLabel,
|
||||
});
|
||||
await user.click(remove);
|
||||
|
||||
expect(clearPendingCommunityAvatarFile).toHaveBeenCalled();
|
||||
expect(screen.getByRole("button", { name: "Upload" })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("img", { name: copy.previewAlt }),
|
||||
).not.toBeInTheDocument();
|
||||
screen.getByRole("button", { name: copy.replaceButton }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("avatar.png")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("rejects an empty file with a clear error and does not stage it", async () => {
|
||||
render(<CommunityUploadScreen />);
|
||||
await screen.findByText(copy.signInToUploadNote);
|
||||
await screen.findByRole("heading", { name: copy.title });
|
||||
|
||||
fireEvent.change(fileInput(), {
|
||||
target: { files: [new File([], "empty.png", { type: "image/png" })] },
|
||||
@@ -123,7 +114,7 @@ describe("CommunityUploadScreen", () => {
|
||||
|
||||
it("rejects a text file renamed as PNG", async () => {
|
||||
render(<CommunityUploadScreen />);
|
||||
await screen.findByText(copy.signInToUploadNote);
|
||||
await screen.findByRole("heading", { name: copy.title });
|
||||
|
||||
fireEvent.change(fileInput(), {
|
||||
target: {
|
||||
@@ -139,7 +130,7 @@ describe("CommunityUploadScreen", () => {
|
||||
|
||||
it("rejects SVG uploads", async () => {
|
||||
render(<CommunityUploadScreen />);
|
||||
await screen.findByText(copy.signInToUploadNote);
|
||||
await screen.findByRole("heading", { name: copy.title });
|
||||
|
||||
fireEvent.change(fileInput(), {
|
||||
target: {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import communitySize from "../../messages/en/create/community/communitySize.json";
|
||||
import { SIZE_BANDS } from "../../lib/create/deckTypes";
|
||||
import { isCommunitySizeStepBlocked } from "../../app/(app)/create/utils/createFlowFooterLabels";
|
||||
import {
|
||||
isCommunityNameStepBlocked,
|
||||
isCommunitySizeStepBlocked,
|
||||
isCommunityStructureStepBlocked,
|
||||
} from "../../app/(app)/create/utils/createFlowFooterLabels";
|
||||
|
||||
/**
|
||||
* Inclusive bounds. 100 stays in 13 to 100. More than 100 starts at 101.
|
||||
@@ -64,3 +68,37 @@ describe("community size bands", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("community name and type continue gates", () => {
|
||||
it("blocks the name step until a title is entered", () => {
|
||||
expect(isCommunityNameStepBlocked("community-name", undefined)).toBe(true);
|
||||
expect(isCommunityNameStepBlocked("community-name", " ")).toBe(true);
|
||||
expect(isCommunityNameStepBlocked("community-name", "Garden Club")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isCommunityNameStepBlocked("community-size", "")).toBe(false);
|
||||
});
|
||||
|
||||
it("blocks the type step until a main kind and a maturity are chosen", () => {
|
||||
expect(
|
||||
isCommunityStructureStepBlocked("community-structure", [], ["earlyStage"]),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isCommunityStructureStepBlocked(
|
||||
"community-structure",
|
||||
["mutualAid"],
|
||||
[],
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isCommunityStructureStepBlocked(
|
||||
"community-structure",
|
||||
["mutualAid"],
|
||||
["established"],
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isCommunityStructureStepBlocked("community-size", [], []),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -17,11 +17,14 @@ describe("getProportionBarProgressForCreateFlowStep", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses 2-0 on community-save, review, and core-values (Create Community segment / same fill)", () => {
|
||||
expect(getProportionBarProgressForCreateFlowStep("community-save")).toBe(
|
||||
"2-0",
|
||||
it("maps the rest of Create Community onto part 1, skipping setup at 1-5", () => {
|
||||
expect(getProportionBarProgressForCreateFlowStep("community-upload")).toBe(
|
||||
"1-6",
|
||||
);
|
||||
expect(getProportionBarProgressForCreateFlowStep("review")).toBe("2-0");
|
||||
expect(getProportionBarProgressForCreateFlowStep("community-save")).toBe(
|
||||
"1-7",
|
||||
);
|
||||
expect(getProportionBarProgressForCreateFlowStep("review")).toBe("1-8");
|
||||
expect(getProportionBarProgressForCreateFlowStep("core-values")).toBe(
|
||||
"2-0",
|
||||
);
|
||||
|
||||
@@ -25,4 +25,10 @@ describe("create footer messages", () => {
|
||||
"Publish CommunityRule",
|
||||
);
|
||||
});
|
||||
|
||||
it("exposes Skip and Skip for now for optional community steps", () => {
|
||||
expect(messages.create.footer.skip).toBe("Skip");
|
||||
expect(messages.create.footer.skipForNow).toBe("Skip for now");
|
||||
expect(messages.create.footer.submitEmail).toBe("Submit email");
|
||||
});
|
||||
});
|
||||
@@ -175,7 +175,7 @@ describe("flowSteps", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("parseReviewReturnSearchParam accepts only final-review and edit-rule", () => {
|
||||
it("parseReviewReturnSearchParam accepts final-review, edit-rule, and review", () => {
|
||||
expect(
|
||||
parseReviewReturnSearchParam(
|
||||
new URLSearchParams("reviewReturn=final-review"),
|
||||
@@ -186,16 +186,22 @@ describe("flowSteps", () => {
|
||||
new URLSearchParams("reviewReturn=edit-rule"),
|
||||
),
|
||||
).toBe("edit-rule");
|
||||
expect(
|
||||
parseReviewReturnSearchParam(new URLSearchParams("reviewReturn=review")),
|
||||
).toBe("review");
|
||||
expect(
|
||||
parseReviewReturnSearchParam(new URLSearchParams("reviewReturn=nope")),
|
||||
).toBeNull();
|
||||
expect(parseReviewReturnSearchParam(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("offers Save & Exit from community-structure through final-review and on edit-rule", () => {
|
||||
it("offers Save and exit from community-save through final-review and on edit-rule", () => {
|
||||
expect(shouldOfferCreateFlowSaveAndExit("informational")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-name")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-structure")).toBe(true);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-structure")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-upload")).toBe(false);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("community-save")).toBe(true);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("review")).toBe(true);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("final-review")).toBe(true);
|
||||
expect(shouldOfferCreateFlowSaveAndExit("edit-rule")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -18,4 +18,8 @@ describe("isValidCreateFlowSaveEmail", () => {
|
||||
it("accepts a required type=email value", () => {
|
||||
expect(isValidCreateFlowSaveEmail("user@example.com")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a domain with no dot", () => {
|
||||
expect(isValidCreateFlowSaveEmail("maya@example")).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user