mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 22:39:54 -06:00
Smoothen Progressbar animations and minor survey editor improvements (#339)
* auto focus on sign up * update PR template * add updatedAt date to survey summary * add animation to Progress, make timer smoother * change button size in question card, auto focus * add transition to js widget, fix auto focus in editor --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -37,4 +37,5 @@ Fixes # (issue)
|
||||
- [ ] Checked for warnings, there are none
|
||||
- [ ] Removed all `console.logs`
|
||||
- [ ] Merged the latest changes from main onto my branch with `git pull origin main`
|
||||
- [ ] My changes don't cause any responsiveness issues
|
||||
- [ ] Updated the Formbricks Docs if changes were necessary
|
||||
|
||||
@@ -78,7 +78,12 @@ export default function PreviewSurvey({
|
||||
frameRef.current = requestAnimationFrame(frame);
|
||||
} else {
|
||||
handleStopCountdown();
|
||||
// close modal
|
||||
setIsModalOpen(false);
|
||||
// reopen the modal after 1 second
|
||||
setTimeout(() => {
|
||||
setIsModalOpen(true);
|
||||
setActiveQuestionId(questions[0]?.id || ""); // set first question as active
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ export default function CTAQuestionForm({
|
||||
lastQuestion,
|
||||
}: CTAQuestionFormProps): JSX.Element {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -317,10 +317,10 @@ export default function LogicEditor({
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<Button
|
||||
id="logicJumps"
|
||||
size="sm"
|
||||
className="bg-slate-100 hover:bg-slate-50"
|
||||
type="button"
|
||||
name="logicJumps"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
StartIcon={SplitIcon}
|
||||
onClick={() => addLogic()}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
localSurvey: Survey;
|
||||
@@ -19,6 +20,10 @@ export default function MultipleChoiceMultiForm({
|
||||
updateQuestion,
|
||||
lastQuestion,
|
||||
}: OpenQuestionFormProps): JSX.Element {
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
const questionRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: any) => {
|
||||
const newChoices = !question.choices
|
||||
? []
|
||||
@@ -32,6 +37,7 @@ export default function MultipleChoiceMultiForm({
|
||||
};
|
||||
|
||||
const addChoice = () => {
|
||||
setIsNew(false); // This question is no longer new.
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
const otherChoice = newChoices.find((choice) => choice.id === "other");
|
||||
if (otherChoice) {
|
||||
@@ -70,12 +76,26 @@ export default function MultipleChoiceMultiForm({
|
||||
updateQuestion(questionIdx, { choices: newChoices, logic: newLogic });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (lastChoiceRef.current) {
|
||||
lastChoiceRef.current?.focus();
|
||||
}
|
||||
}, [question.choices?.length]);
|
||||
|
||||
// This effect will run once on initial render, setting focus to the question input.
|
||||
useEffect(() => {
|
||||
if (isNew && questionRef.current) {
|
||||
questionRef.current.focus();
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
ref={questionRef}
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
@@ -103,6 +123,7 @@ export default function MultipleChoiceMultiForm({
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<div key={choiceIdx} className="inline-flex w-full items-center">
|
||||
<Input
|
||||
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
|
||||
id={choice.id}
|
||||
name={choice.id}
|
||||
value={choice.label}
|
||||
@@ -119,7 +140,7 @@ export default function MultipleChoiceMultiForm({
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="secondary" type="button" onClick={() => addChoice()}>
|
||||
<Button variant="secondary" size="sm" type="button" onClick={() => addChoice()}>
|
||||
Add Option
|
||||
</Button>
|
||||
{question.choices.filter((c) => c.id === "other").length === 0 && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface OpenQuestionFormProps {
|
||||
localSurvey: Survey;
|
||||
@@ -19,6 +20,10 @@ export default function MultipleChoiceSingleForm({
|
||||
updateQuestion,
|
||||
lastQuestion,
|
||||
}: OpenQuestionFormProps): JSX.Element {
|
||||
const lastChoiceRef = useRef<HTMLInputElement>(null);
|
||||
const [isNew, setIsNew] = useState(true);
|
||||
const questionRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const updateChoice = (choiceIdx: number, updatedAttributes: any) => {
|
||||
const newChoices = !question.choices
|
||||
? []
|
||||
@@ -32,6 +37,7 @@ export default function MultipleChoiceSingleForm({
|
||||
};
|
||||
|
||||
const addChoice = () => {
|
||||
setIsNew(false); // This question is no longer new.
|
||||
let newChoices = !question.choices ? [] : question.choices;
|
||||
const otherChoice = newChoices.find((choice) => choice.id === "other");
|
||||
if (otherChoice) {
|
||||
@@ -70,12 +76,26 @@ export default function MultipleChoiceSingleForm({
|
||||
updateQuestion(questionIdx, { choices: newChoices, logic: newLogic });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (lastChoiceRef.current) {
|
||||
lastChoiceRef.current?.focus();
|
||||
}
|
||||
}, [question.choices?.length]);
|
||||
|
||||
// This effect will run once on initial render, setting focus to the question input.
|
||||
useEffect(() => {
|
||||
if (isNew && questionRef.current) {
|
||||
questionRef.current.focus();
|
||||
}
|
||||
}, [isNew]);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
ref={questionRef}
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
@@ -103,6 +123,7 @@ export default function MultipleChoiceSingleForm({
|
||||
question.choices.map((choice, choiceIdx) => (
|
||||
<div key={choiceIdx} className="inline-flex w-full items-center">
|
||||
<Input
|
||||
ref={choiceIdx === question.choices.length - 1 ? lastChoiceRef : null}
|
||||
id={choice.id}
|
||||
name={choice.id}
|
||||
value={choice.label}
|
||||
@@ -119,7 +140,7 @@ export default function MultipleChoiceSingleForm({
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="secondary" type="button" onClick={() => addChoice()}>
|
||||
<Button variant="secondary" size="sm" type="button" onClick={() => addChoice()}>
|
||||
Add Option
|
||||
</Button>
|
||||
{question.choices.filter((c) => c.id === "other").length === 0 && (
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function NPSQuestionForm({
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function OpenQuestionForm({
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function RatingQuestionForm({
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function MultipleChoiceSummary({
|
||||
|
||||
const results: ChoiceResult[] = useMemo(() => {
|
||||
if (!("choices" in questionSummary.question)) return [];
|
||||
console.log(questionSummary.responses);
|
||||
|
||||
// build a dictionary of choices
|
||||
const resultsDict: { [key: string]: ChoiceResult } = {};
|
||||
for (const choice of questionSummary.question.choices) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import LinkSurveyModal from "./LinkSurveyModal";
|
||||
import { timeSinceConditionally } from "@formbricks/lib/time";
|
||||
|
||||
export default function SummaryMetadata({ surveyId, environmentId }) {
|
||||
const { responsesData, isLoadingResponses, isErrorResponses } = useResponses(environmentId, surveyId);
|
||||
@@ -128,7 +129,9 @@ export default function SummaryMetadata({ surveyId, environmentId }) {
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between lg:col-span-1">
|
||||
<div className=""></div>
|
||||
<div className="text-right text-xs text-slate-400">
|
||||
Last updated: {timeSinceConditionally(survey.updatedAt)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-x-1.5">
|
||||
{survey.type === "link" && (
|
||||
<Button
|
||||
|
||||
@@ -11,6 +11,7 @@ import { GithubButton } from "./GithubButton";
|
||||
|
||||
export const SigninForm = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
setLoggingIn(true);
|
||||
@@ -48,6 +49,7 @@ export const SigninForm = () => {
|
||||
Email address
|
||||
</label>
|
||||
<input
|
||||
ref={emailRef}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
@@ -90,6 +92,8 @@ export const SigninForm = () => {
|
||||
if (!showLogin) {
|
||||
setShowLogin(true);
|
||||
setButtonEnabled(false);
|
||||
// Add a slight delay before focusing the input field to ensure it's visible
|
||||
setTimeout(() => emailRef.current?.focus(), 100);
|
||||
} else if (formRef.current) {
|
||||
formRef.current.requestSubmit();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const SignupForm = () => {
|
||||
const router = useRouter();
|
||||
const [error, setError] = useState<string>("");
|
||||
const [signingUp, setSigningUp] = useState(false);
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
setSigningUp(true);
|
||||
@@ -78,6 +79,7 @@ export const SignupForm = () => {
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
ref={nameRef}
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
@@ -136,6 +138,8 @@ export const SignupForm = () => {
|
||||
if (!showLogin) {
|
||||
setShowLogin(true);
|
||||
setButtonEnabled(false);
|
||||
// Add a slight delay before focusing the input field to ensure it's visible
|
||||
setTimeout(() => nameRef.current?.focus(), 100);
|
||||
} else if (formRef.current) {
|
||||
formRef.current.requestSubmit();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Input } from "@/../../packages/ui";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
|
||||
import { useEffect, useState } from "react";
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { Input } from "@/../../packages/ui";
|
||||
|
||||
interface MultipleChoiceMultiProps {
|
||||
question: MultipleChoiceMultiQuestion;
|
||||
@@ -46,7 +46,7 @@ export default function MultipleChoiceMultiQuestion({
|
||||
};
|
||||
|
||||
onSubmit(data);
|
||||
// console.log(data);
|
||||
|
||||
setSelectedChoices([]); // reset value
|
||||
setShowOther(false);
|
||||
setOtherSpecified("");
|
||||
@@ -103,10 +103,9 @@ export default function MultipleChoiceMultiQuestion({
|
||||
</span>
|
||||
{choice.id === "other" && showOther && (
|
||||
<Input
|
||||
type="text"
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
className="mt-2 bg-white"
|
||||
className="mt-2 bg-white focus:border-slate-300"
|
||||
placeholder="Please specify"
|
||||
onChange={(e) => setOtherSpecified(e.currentTarget.value)}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Input } from "@/../../packages/ui";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import type { MultipleChoiceSingleQuestion } from "@formbricks/types/questions";
|
||||
import { useState } from "react";
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import SubmitButton from "@/components/preview/SubmitButton";
|
||||
import { Input } from "@/../../packages/ui";
|
||||
import { useRef } from "react";
|
||||
|
||||
interface MultipleChoiceSingleProps {
|
||||
question: MultipleChoiceSingleQuestion;
|
||||
@@ -21,19 +20,15 @@ export default function MultipleChoiceSingleQuestion({
|
||||
brandColor,
|
||||
}: MultipleChoiceSingleProps) {
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const otherSpecify = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const value = otherSpecify.current?.value || e.currentTarget[question.id].value;
|
||||
const value = e.currentTarget[question.id].value;
|
||||
const data = {
|
||||
[question.id]: value,
|
||||
};
|
||||
// console.log(data);
|
||||
|
||||
onSubmit(data);
|
||||
setSelectedChoice(null); // reset form
|
||||
}}>
|
||||
@@ -70,12 +65,12 @@ export default function MultipleChoiceSingleQuestion({
|
||||
</span>
|
||||
{choice.id === "other" && selectedChoice === "other" && (
|
||||
<Input
|
||||
ref={otherSpecify}
|
||||
id="other-specify"
|
||||
name="other-specify"
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
placeholder="Please specify"
|
||||
className="mt-3 bg-white"
|
||||
className="mt-3 bg-white focus:border-slate-300"
|
||||
required={question.required}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
autoFocus
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
export default function Progress({ progress, brandColor }: { progress: number; brandColor: string }) {
|
||||
import React from "react";
|
||||
|
||||
const ProgressComponent = ({ progress, brandColor }) => {
|
||||
return (
|
||||
<div className="h-1 w-full rounded-full bg-slate-200">
|
||||
<div
|
||||
className="h-1 rounded-full bg-slate-700"
|
||||
className="transition-width h-1 rounded-full duration-500"
|
||||
style={{ backgroundColor: brandColor, width: `${Math.floor(progress * 100)}%` }}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ProgressComponent.displayName = "Progress";
|
||||
|
||||
const Progress = React.memo(ProgressComponent, (prevProps, nextProps) => {
|
||||
// Only re-render if progress or brandColor changes
|
||||
return prevProps.progress === nextProps.progress && prevProps.brandColor === nextProps.brandColor;
|
||||
});
|
||||
|
||||
export default Progress;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MultipleChoiceMultiQuestion } from "../../../types/questions";
|
||||
import { h } from "preact";
|
||||
import { useState } from "preact/hooks";
|
||||
import { useState, useRef, useEffect } from "preact/hooks";
|
||||
import { cn } from "../lib/utils";
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
@@ -22,11 +22,18 @@ export default function MultipleChoiceMultiQuestion({
|
||||
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
|
||||
const [showOther, setShowOther] = useState(false);
|
||||
const [otherSpecified, setOtherSpecified] = useState("");
|
||||
const otherInputRef = useRef(null);
|
||||
|
||||
const isAtLeastOneChecked = () => {
|
||||
return selectedChoices.length > 0 || otherSpecified.length > 0;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (showOther && otherInputRef.current) {
|
||||
otherInputRef.current.focus();
|
||||
}
|
||||
}, [showOther]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -97,11 +104,13 @@ export default function MultipleChoiceMultiQuestion({
|
||||
</span>
|
||||
{choice.id === "other" && showOther && (
|
||||
<input
|
||||
type="text"
|
||||
ref={otherInputRef}
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
placeholder="Please specify"
|
||||
className="fb-mt-3 fb-flex fb-h-10 fb-w-full fb-rounded-md fb-border fb-bg-white fb-border-slate-300 fb-bg-transparent fb-px-3 fb-py-2 fb-text-sm fb-text-slate-800 placeholder:fb-text-slate-400 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-slate-400 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50 dark:fb-border-slate-500 dark:fb-text-slate-300"
|
||||
className={cn(
|
||||
"fb-mt-3 fb-flex fb-h-10 fb-w-full fb-rounded-md fb-border fb-bg-white fb-border-slate-300 fb-bg-transparent fb-px-3 fb-py-2 fb-text-sm fb-text-slate-800 placeholder:fb-text-slate-400 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-slate-400 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50 dark:fb-border-slate-500 dark:fb-text-slate-300"
|
||||
)}
|
||||
onChange={(e) => setOtherSpecified(e.currentTarget.value)}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
required={question.required}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { h } from "preact";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
import { useRef, useState, useEffect } from "preact/hooks";
|
||||
import { cn } from "../lib/utils";
|
||||
import type { MultipleChoiceSingleQuestion } from "../../../types/questions";
|
||||
import Headline from "./Headline";
|
||||
@@ -22,6 +22,12 @@ export default function MultipleChoiceSingleQuestion({
|
||||
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||
const otherSpecify = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChoice === "other") {
|
||||
otherSpecify.current?.focus();
|
||||
}
|
||||
}, [selectedChoice]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
@@ -73,11 +79,12 @@ export default function MultipleChoiceSingleQuestion({
|
||||
{choice.id === "other" && selectedChoice === "other" && (
|
||||
<input
|
||||
ref={otherSpecify}
|
||||
id="other-specify"
|
||||
name="other-specify"
|
||||
id={`${choice.id}-label`}
|
||||
name={question.id}
|
||||
placeholder="Please specify"
|
||||
className="fb-mt-3 fb-flex fb-h-10 fb-w-full fb-rounded-md fb-border fb-bg-white fb-border-slate-300 fb-bg-transparent fb-px-3 fb-py-2 fb-text-sm fb-text-slate-800 placeholder:fb-text-slate-400 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-slate-400 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50 dark:fb-border-slate-500 dark:fb-text-slate-300"
|
||||
required={question.required}
|
||||
aria-labelledby={`${choice.id}-label`}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
|
||||
@@ -2,9 +2,9 @@ import { h } from "preact";
|
||||
|
||||
export default function Progress({ progress, brandColor }: { progress: number; brandColor: string }) {
|
||||
return (
|
||||
<div className="fb-h-1 fb-w-full fb-rounded-full bg-slate-200">
|
||||
<div className="fb-h-1 fb-w-full fb-rounded-full fb-bg-slate-200">
|
||||
<div
|
||||
className="fb-h-1 fb-rounded-full"
|
||||
className="fb-h-1 fb-rounded-full fb-transition-width fb-duration-500"
|
||||
style={{ backgroundColor: brandColor, width: `${Math.floor(progress * 100)}%` }}></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user