move tsx kit -> uikit
This commit is contained in:
542
web/uikit/DatePicker.tsx
Normal file
542
web/uikit/DatePicker.tsx
Normal file
@@ -0,0 +1,542 @@
|
||||
import { createSignal, createEffect, Show, onMount, onCleanup, For } from "solid-js";
|
||||
import { Portal } from "solid-js/web";
|
||||
import { Icon } from "./Icons.tsx";
|
||||
import { FormInput } from "./Forms.tsx";
|
||||
import { readAccessor, accessor, type MaybeAccessor } from "../utils/accessors.ts";
|
||||
import {
|
||||
CAL_PICKER_ROOT,
|
||||
CAL_HEADER_PICKER,
|
||||
CAL_NAV_BTN,
|
||||
CAL_MY_PICKER,
|
||||
CAL_WEEKDAYS_PICKER,
|
||||
CAL_WEEKDAY_PICKER,
|
||||
CAL_DAYS_PICKER,
|
||||
CAL_DAY_PICKER_BASE,
|
||||
CAL_SELECT,
|
||||
} from "./Calendar.tsx";
|
||||
|
||||
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
||||
const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||
|
||||
function getDaysInMonth(year: number, month: number): number {
|
||||
return new Date(year, month + 1, 0).getDate();
|
||||
}
|
||||
|
||||
function getFirstDayOfMonth(year: number, month: number): number {
|
||||
return new Date(year, month, 1).getDay();
|
||||
}
|
||||
|
||||
function dayClass(date: Date | null, selected: boolean, today: boolean): string {
|
||||
let c = CAL_DAY_PICKER_BASE;
|
||||
if (!date) c += " cursor-default";
|
||||
else c += " hover:bg-neutral-100";
|
||||
if (today) c += " font-bold text-primary";
|
||||
if (selected) c += " !bg-primary !text-white";
|
||||
return c;
|
||||
}
|
||||
|
||||
/** Parse typed or pasted text into YYYY-MM-DD, or "" if invalid. */
|
||||
function parseDateInput(text: string): string {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return "";
|
||||
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
||||
const [y, m, d] = trimmed.split("-").map((n) => parseInt(n, 10));
|
||||
const iso = new Date(y, m - 1, d);
|
||||
if (!isNaN(iso.getTime()) && iso.getFullYear() === y && iso.getMonth() === m - 1 && iso.getDate() === d) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = new Date(trimmed);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
const y = parsed.getFullYear();
|
||||
const m = String(parsed.getMonth() + 1).padStart(2, "0");
|
||||
const d = String(parsed.getDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function formatDisplayDate(iso: string): string {
|
||||
if (!iso) return "";
|
||||
const parts = iso.split("-");
|
||||
if (parts.length < 3) return iso;
|
||||
const d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10));
|
||||
if (isNaN(d.getTime())) return "";
|
||||
return d.toLocaleDateString();
|
||||
}
|
||||
|
||||
interface CalendarDropdownProps {
|
||||
selected?: MaybeAccessor<string>;
|
||||
onSelect?: (key: string) => void;
|
||||
}
|
||||
|
||||
function CalendarDropdown(props: CalendarDropdownProps) {
|
||||
const today = new Date();
|
||||
const selected = () => readAccessor(props.selected, "");
|
||||
const [viewDate, setViewDate] = createSignal(selected() ? new Date(selected()) : today);
|
||||
const [key, setKey] = createSignal(0);
|
||||
|
||||
createEffect(() => {
|
||||
const v = selected();
|
||||
if (v) {
|
||||
const d = new Date(v);
|
||||
if (!isNaN(d.getTime())) {
|
||||
setViewDate(d);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const currentMonth = () => viewDate().getMonth();
|
||||
const currentYear = () => viewDate().getFullYear();
|
||||
|
||||
const getDays = () => {
|
||||
const year = currentYear();
|
||||
const month = currentMonth();
|
||||
const daysInMonth = getDaysInMonth(year, month);
|
||||
const firstDay = getFirstDayOfMonth(year, month);
|
||||
|
||||
const daysArray: (Date | null)[] = [];
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
daysArray.push(null);
|
||||
}
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
daysArray.push(new Date(year, month, i));
|
||||
}
|
||||
return daysArray;
|
||||
};
|
||||
|
||||
const isSelected = (date: Date | null): boolean => {
|
||||
if (!date) return false;
|
||||
const v = selected();
|
||||
if (!v) return false;
|
||||
const sel = new Date(v);
|
||||
if (isNaN(sel.getTime())) return false;
|
||||
return date.getFullYear() === sel.getFullYear() &&
|
||||
date.getMonth() === sel.getMonth() &&
|
||||
date.getDate() === sel.getDate();
|
||||
};
|
||||
|
||||
const isToday = (date: Date | null): boolean => {
|
||||
if (!date) return false;
|
||||
return date.getFullYear() === today.getFullYear() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getDate() === today.getDate();
|
||||
};
|
||||
|
||||
const selectDate = (date: Date | null) => {
|
||||
if (!date) return;
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
props.onSelect?.(`${y}-${m}-${d}`);
|
||||
};
|
||||
|
||||
const goPrev = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setViewDate(new Date(currentYear(), currentMonth() - 1, 1));
|
||||
setKey(k => k + 1);
|
||||
};
|
||||
|
||||
const goNext = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setViewDate(new Date(currentYear(), currentMonth() + 1, 1));
|
||||
setKey(k => k + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={CAL_PICKER_ROOT} attr:key={key()}>
|
||||
<div class={CAL_HEADER_PICKER}>
|
||||
<button type="button" class={CAL_NAV_BTN} onclick={goPrev}>
|
||||
<Icon icon="chevron-left" size={16}/>
|
||||
</button>
|
||||
<span class={CAL_MY_PICKER}>{MONTHS[currentMonth()]} {currentYear()}</span>
|
||||
<button type="button" class={CAL_NAV_BTN} onclick={goNext}>
|
||||
<Icon icon="chevron-right" size={16}/>
|
||||
</button>
|
||||
</div>
|
||||
<div class={CAL_WEEKDAYS_PICKER}>
|
||||
<For each={DAYS}>{(day) => <div class={CAL_WEEKDAY_PICKER}>{day}</div>}</For>
|
||||
</div>
|
||||
<div class={CAL_DAYS_PICKER}>
|
||||
<For each={getDays()}>{(date) => (
|
||||
<button type="button" class={dayClass(date, isSelected(date), isToday(date))} onclick={(e: MouseEvent) => { e.stopPropagation(); selectDate(date); }} disabled={!date}>
|
||||
{date ? date.getDate() : ""}
|
||||
</button>
|
||||
)}</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DATE_PICKER_WRAP = "relative w-full min-w-0";
|
||||
const DATE_PICKER_FIELD = "relative w-full min-w-0 cursor-pointer [&_.ui-form]:m-0 [&_input]:cursor-text";
|
||||
const DATE_PICKER_DROPDOWN = "bg-white border border-neutral-200 rounded-default shadow-[0_4px_12px_rgba(0,0,0,0.15)] p-1 min-w-[16rem]";
|
||||
const DATE_PICKER_ICON_BTN = "absolute inset-y-0 right-0 z-[1] flex items-center justify-center bg-transparent border-0 px-2 cursor-pointer text-text-muted leading-none hover:text-text-body pointer-events-auto";
|
||||
|
||||
const DATE_PICKER_CLEAR_BTN = "absolute inset-y-0 right-8 z-[1] flex items-center justify-center bg-transparent border-0 px-1.5 cursor-pointer text-text-muted leading-none hover:text-text-body pointer-events-auto";
|
||||
|
||||
interface DatePickerProps {
|
||||
value?: MaybeAccessor<string>;
|
||||
onchange?: (value: string) => void;
|
||||
placeholder?: string;
|
||||
small?: boolean;
|
||||
clearable?: boolean;
|
||||
}
|
||||
|
||||
export function DatePicker(props: DatePickerProps) {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
const [localValue, setLocalValue] = createSignal("");
|
||||
const [editing, setEditing] = createSignal(false);
|
||||
const [draft, setDraft] = createSignal("");
|
||||
const [dropdownPos, setDropdownPos] = createSignal({ top: 0, left: 0, width: 0 });
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
let fieldRef: HTMLDivElement | undefined;
|
||||
let dropdownRef: HTMLDivElement | undefined;
|
||||
|
||||
const externalValue = () => readAccessor(props.value, "");
|
||||
|
||||
const updateDropdownPos = () => {
|
||||
if (!fieldRef) return;
|
||||
const rect = fieldRef.getBoundingClientRect();
|
||||
setDropdownPos({ top: rect.bottom + 4, left: rect.left, width: rect.width });
|
||||
};
|
||||
|
||||
const dropdownStyle = () => {
|
||||
const pos = dropdownPos();
|
||||
const maxW = Math.min(400, window.innerWidth - pos.left - 8);
|
||||
return `position:fixed;top:${pos.top}px;left:${pos.left}px;min-width:${Math.max(pos.width, 16 * 16)}px;width:max-content;max-width:${maxW}px;z-index:200;`;
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (open()) {
|
||||
updateDropdownPos();
|
||||
let rafId: number;
|
||||
const trackPosition = () => {
|
||||
updateDropdownPos();
|
||||
rafId = requestAnimationFrame(trackPosition);
|
||||
};
|
||||
rafId = requestAnimationFrame(trackPosition);
|
||||
onCleanup(() => cancelAnimationFrame(rafId));
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!editing()) {
|
||||
setLocalValue(externalValue());
|
||||
}
|
||||
});
|
||||
|
||||
const hasValue = () => !!localValue();
|
||||
|
||||
const displayValue = () => formatDisplayDate(localValue());
|
||||
|
||||
const inputValue = () => editing() ? draft() : displayValue();
|
||||
|
||||
const commitValue = (raw: string) => {
|
||||
const parsed = parseDateInput(raw);
|
||||
setLocalValue(parsed);
|
||||
setDraft(parsed ? formatDisplayDate(parsed) : "");
|
||||
if (typeof props.onchange === "function") props.onchange(parsed);
|
||||
};
|
||||
|
||||
const handleSelect = (dateStr: string) => {
|
||||
setEditing(false);
|
||||
setLocalValue(dateStr);
|
||||
setDraft(formatDisplayDate(dateStr));
|
||||
if (typeof props.onchange === "function") props.onchange(dateStr);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleClear = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setEditing(false);
|
||||
setLocalValue("");
|
||||
setDraft("");
|
||||
if (typeof props.onchange === "function") props.onchange("");
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as Node;
|
||||
const inContainer = containerRef?.contains(target);
|
||||
const inDropdown = dropdownRef?.contains(target);
|
||||
if (!inContainer && !inDropdown) {
|
||||
if (editing()) {
|
||||
commitValue(draft());
|
||||
setEditing(false);
|
||||
}
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
});
|
||||
|
||||
const openCalendar = (_e: MouseEvent) => {
|
||||
updateDropdownPos();
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const toggleCalendar = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!open()) updateDropdownPos();
|
||||
setOpen((v) => !v);
|
||||
};
|
||||
|
||||
const handleInputFocus = (_e: FocusEvent) => {
|
||||
setEditing(true);
|
||||
setDraft(displayValue());
|
||||
};
|
||||
|
||||
const handleInput = (e: InputEvent & { currentTarget: HTMLInputElement }) => {
|
||||
setDraft(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleInputBlur = (_e: FocusEvent) => {
|
||||
commitValue(draft());
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
const inputCls = () => "w-full" + (props.clearable && hasValue() ? " pr-14" : " pr-9");
|
||||
|
||||
return (
|
||||
<div class={DATE_PICKER_WRAP} ref={(el: HTMLDivElement) => containerRef = el}>
|
||||
<div class={DATE_PICKER_FIELD} ref={(el: HTMLDivElement) => fieldRef = el} onclick={openCalendar}>
|
||||
<FormInput
|
||||
type="text"
|
||||
small={props.small}
|
||||
value={inputValue()}
|
||||
placeholder={props.placeholder || "Select date"}
|
||||
onfocus={handleInputFocus}
|
||||
oninput={handleInput}
|
||||
onblur={handleInputBlur}
|
||||
class={inputCls()}
|
||||
/>
|
||||
<Show when={props.clearable && hasValue()}>
|
||||
<button type="button" class={DATE_PICKER_CLEAR_BTN} onclick={handleClear} aria-label="Clear date">
|
||||
<Icon icon="xmark" size={14} class="block leading-none"/>
|
||||
</button>
|
||||
</Show>
|
||||
<button type="button" class={DATE_PICKER_ICON_BTN} onclick={toggleCalendar} aria-label="Open calendar">
|
||||
<Icon icon="calendar" size={16} class="block leading-none"/>
|
||||
</button>
|
||||
</div>
|
||||
<Show when={open()}>
|
||||
<Portal>
|
||||
<div
|
||||
ref={(el: HTMLDivElement) => dropdownRef = el}
|
||||
data-floating-content="true"
|
||||
class={DATE_PICKER_DROPDOWN}
|
||||
style={dropdownStyle()}
|
||||
onclick={(e: MouseEvent) => e.stopPropagation()}
|
||||
>
|
||||
<CalendarDropdown selected={localValue} onSelect={handleSelect}/>
|
||||
</div>
|
||||
</Portal>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const MONTHS_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||
|
||||
function CalendarDropdownDOB(props: CalendarDropdownProps) {
|
||||
const today = new Date();
|
||||
const currentYear = today.getFullYear();
|
||||
const years = Array.from({ length: 120 }, (_, i) => currentYear - i);
|
||||
|
||||
const [viewDate, setViewDate] = createSignal(props.selected ? new Date(props.selected as string) : today);
|
||||
const [key, setKey] = createSignal(0);
|
||||
|
||||
createEffect(() => {
|
||||
if (props.selected) {
|
||||
const d = new Date(props.selected as string);
|
||||
if (!isNaN(d.getTime())) {
|
||||
setViewDate(d);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const currentMonth = () => viewDate().getMonth();
|
||||
const currentYearView = () => viewDate().getFullYear();
|
||||
|
||||
const getDays = () => {
|
||||
const year = currentYearView();
|
||||
const month = currentMonth();
|
||||
const daysInMonth = getDaysInMonth(year, month);
|
||||
const firstDay = getFirstDayOfMonth(year, month);
|
||||
|
||||
const daysArray: (Date | null)[] = [];
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
daysArray.push(null);
|
||||
}
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
daysArray.push(new Date(year, month, i));
|
||||
}
|
||||
return daysArray;
|
||||
};
|
||||
|
||||
const handleMonthChange = (e: Event) => {
|
||||
const month = parseInt((e.target as HTMLSelectElement).value);
|
||||
if (!isNaN(month)) {
|
||||
setViewDate(new Date(currentYearView(), month, 1));
|
||||
}
|
||||
};
|
||||
|
||||
const handleYearChange = (e: Event) => {
|
||||
const year = parseInt((e.target as HTMLSelectElement).value);
|
||||
if (!isNaN(year)) {
|
||||
setViewDate(new Date(year, currentMonth(), 1));
|
||||
}
|
||||
};
|
||||
|
||||
const isSelected = (date: Date | null): boolean => {
|
||||
if (!date || !props.selected) return false;
|
||||
const sel = new Date(props.selected as string);
|
||||
if (isNaN(sel.getTime())) return false;
|
||||
return date.getFullYear() === sel.getFullYear() &&
|
||||
date.getMonth() === sel.getMonth() &&
|
||||
date.getDate() === sel.getDate();
|
||||
};
|
||||
|
||||
const isToday = (date: Date | null): boolean => {
|
||||
if (!date) return false;
|
||||
return date.getFullYear() === today.getFullYear() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getDate() === today.getDate();
|
||||
};
|
||||
|
||||
const selectDate = (date: Date | null) => {
|
||||
if (!date) return;
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
props.onSelect?.(`${y}-${m}-${d}`);
|
||||
};
|
||||
|
||||
const goPrev = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setViewDate(new Date(currentYearView(), currentMonth() - 1, 1));
|
||||
setKey(k => k + 1);
|
||||
};
|
||||
|
||||
const goNext = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setViewDate(new Date(currentYearView(), currentMonth() + 1, 1));
|
||||
setKey(k => k + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={CAL_PICKER_ROOT} attr:key={key()}>
|
||||
<div class={CAL_HEADER_PICKER}>
|
||||
<button type="button" class={CAL_NAV_BTN} onclick={goPrev}>
|
||||
<Icon icon="chevron-left" size={16}/>
|
||||
</button>
|
||||
<select class={CAL_SELECT} value={currentMonth()} onchange={handleMonthChange}>
|
||||
<For each={MONTHS_SHORT}>{(m, i) => <option value={i()}>{m}</option>}</For>
|
||||
</select>
|
||||
<select class={CAL_SELECT} value={currentYearView()} onchange={handleYearChange}>
|
||||
<For each={years}>{(y) => <option value={y}>{y}</option>}</For>
|
||||
</select>
|
||||
<button type="button" class={CAL_NAV_BTN} onclick={goNext}>
|
||||
<Icon icon="chevron-right" size={16}/>
|
||||
</button>
|
||||
</div>
|
||||
<div class={CAL_WEEKDAYS_PICKER}>
|
||||
<For each={DAYS}>{(day) => <div class={CAL_WEEKDAY_PICKER}>{day}</div>}</For>
|
||||
</div>
|
||||
<div class={CAL_DAYS_PICKER}>
|
||||
<For each={getDays()}>{(date) => (
|
||||
<button type="button" class={dayClass(date, isSelected(date), isToday(date))} onclick={(e: MouseEvent) => { e.stopPropagation(); selectDate(date); }} disabled={!date}>
|
||||
{date ? date.getDate() : ""}
|
||||
</button>
|
||||
)}</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DateOfBirthPicker(props: DatePickerProps) {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
const [editing, setEditing] = createSignal(false);
|
||||
const [draft, setDraft] = createSignal("");
|
||||
let containerRef: HTMLDivElement | undefined;
|
||||
|
||||
const isoValue = () => readAccessor(props.value, "");
|
||||
|
||||
const displayValue = () => formatDisplayDate(isoValue());
|
||||
|
||||
const inputValue = () => editing() ? draft() : displayValue();
|
||||
|
||||
const commitValue = (raw: string) => {
|
||||
const parsed = parseDateInput(raw);
|
||||
setDraft(parsed ? formatDisplayDate(parsed) : "");
|
||||
props.onchange?.(parsed);
|
||||
};
|
||||
|
||||
const handleSelect = (dateStr: string) => {
|
||||
setEditing(false);
|
||||
setDraft(formatDisplayDate(dateStr));
|
||||
props.onchange?.(dateStr);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (containerRef && !containerRef.contains(e.target as Node)) {
|
||||
if (editing()) {
|
||||
commitValue(draft());
|
||||
setEditing(false);
|
||||
}
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
});
|
||||
|
||||
const openCalendar = (_e: MouseEvent) => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const toggleCalendar = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setOpen((v) => !v);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class={DATE_PICKER_WRAP} ref={(el: HTMLDivElement) => containerRef = el}>
|
||||
<div class={DATE_PICKER_FIELD} onclick={openCalendar}>
|
||||
<FormInput
|
||||
type="text"
|
||||
value={inputValue()}
|
||||
placeholder={props.placeholder || "Select date of birth"}
|
||||
onfocus={(_e: FocusEvent) => { setEditing(true); setDraft(displayValue()); }}
|
||||
oninput={(e: InputEvent & { currentTarget: HTMLInputElement }) => setDraft(e.currentTarget.value)}
|
||||
onblur={(_e: FocusEvent) => { commitValue(draft()); setEditing(false); }}
|
||||
class="w-full pr-9"
|
||||
/>
|
||||
<button type="button" class={DATE_PICKER_ICON_BTN} onclick={toggleCalendar} aria-label="Open calendar">
|
||||
<Icon icon="calendar" size={16} class="block leading-none"/>
|
||||
</button>
|
||||
</div>
|
||||
<Show when={open()}>
|
||||
<div class={DATE_PICKER_DROPDOWN} onclick={(e: MouseEvent) => e.stopPropagation()}>
|
||||
<CalendarDropdownDOB selected={isoValue} onSelect={handleSelect}/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user