extensible inputs on recipe add page
This commit is contained in:
@@ -51,7 +51,7 @@ const Friends: FC<{ targetUser?: IUser }> = ({ targetUser }) => {
|
||||
<>
|
||||
{ userList.length ?
|
||||
(
|
||||
<Card extraStyles="flex-row">
|
||||
<Card extraClasses="flex-row">
|
||||
<h2>Friends ({ userList?.length ?? "0" }):</h2>
|
||||
|
||||
<div className="friends-list">
|
||||
|
||||
47
client/src/components/derived/IngredientSelector.tsx
Normal file
47
client/src/components/derived/IngredientSelector.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Autocomplete, TextField } from "@mui/material"
|
||||
import { useRef, useState } from "react";
|
||||
import { IIngredient } from "../../schemas";
|
||||
import { Button } from "../ui";
|
||||
|
||||
interface IngredientSelectorProps {
|
||||
position: number
|
||||
ingredients: IIngredient[]
|
||||
destroy: (position: number) => void
|
||||
}
|
||||
|
||||
function IngredientSelector({ position, ingredients, destroy }: IngredientSelectorProps) {
|
||||
const [options, setOptions] = useState(ingredients.map(each => each.name));
|
||||
const [newOptions, setNewOptions] = useState(new Array<string>());
|
||||
const [selected, setSelected] = useState(new Array<string>());
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<Autocomplete
|
||||
autoHighlight
|
||||
options={options}
|
||||
className="ui-creatable-component"
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
variant="filled"
|
||||
placeholder="Ingredient Name"
|
||||
/>
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code == 'Enter') {
|
||||
const inputVal: string = e.target['value' as keyof EventTarget].toString();
|
||||
console.log(inputVal)
|
||||
if (inputVal.length) {
|
||||
setSelected(prev => [...prev, inputVal])
|
||||
setOptions((prev) => [...prev, inputVal]);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/* @ts-ignore */}
|
||||
<Button onClick={() => destroy(position)}>Close</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IngredientSelector
|
||||
@@ -1,34 +1,29 @@
|
||||
import { useCallback, useRef, useEffect, useState, createRef } from "react";
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { LegacyRef, MutableRefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Button, Card, Divider, Form, Page, Panel } from "../ui"
|
||||
import { IIngredient, IRecipe } from "../../schemas";
|
||||
import API from "../../util/API";
|
||||
import Creatable from "react-select/creatable";
|
||||
import { OptionType } from "../../util/types";
|
||||
import { createOptionFromText, useSelectorContext } from "../../context/SelectorContext";
|
||||
import { MultiValue } from "react-select";
|
||||
import { Autocomplete, Chip, TextField } from "@mui/material";
|
||||
import IngredientSelector from "../derived/IngredientSelector";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
const AddRecipe = () => {
|
||||
const { user, token } = useAuthContext();
|
||||
const {
|
||||
data, setData, selector, setSelector,
|
||||
options, setOptions, selected, setSelected,
|
||||
onChange, onCreateOption
|
||||
} = useSelectorContext();
|
||||
|
||||
const { data, setData, options, setOptions } = useSelectorContext();
|
||||
const [ingredientFields, setIngredientFields] = useState<Array<JSX.Element>>([]);
|
||||
const [triggerChange, setTriggerChange] = useState(false);
|
||||
const [optionCount, setOptionCount] = useState(0);
|
||||
const [form, setForm] = useState<JSX.Element>();
|
||||
const [toast, setToast] = useState(<></>)
|
||||
const [input, setInput] = useState<IRecipe>({ name: '', preptime: '', description: '', authoruserid: '' })
|
||||
|
||||
const initialIngredient = useRef(null);
|
||||
|
||||
// clear out selector state on page load
|
||||
useEffect(() => {
|
||||
/* useEffect(() => {
|
||||
setData(new Array<IIngredient>());
|
||||
setSelected(new Array<string>());
|
||||
setOptions(new Array<OptionType>());
|
||||
}, [])
|
||||
}, []) */
|
||||
|
||||
// store all ingredients on page mount
|
||||
useEffect(() => {
|
||||
@@ -44,18 +39,14 @@ const AddRecipe = () => {
|
||||
return { label: each.name, value: each.id }
|
||||
}));
|
||||
|
||||
setOptionCount(result.length);
|
||||
setIngredientFields([<IngredientSelector key={v4()} position={optionCount} ingredients={result} destroy={destroySelector} />]);
|
||||
}
|
||||
})();
|
||||
}, [token])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(selected);
|
||||
}, [selected])
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length) {
|
||||
const autocompleteInstance = (
|
||||
/* const autocompleteInstance = (
|
||||
<Autocomplete
|
||||
multiple
|
||||
freeSolo
|
||||
@@ -87,7 +78,7 @@ const AddRecipe = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
) */
|
||||
|
||||
// create dropdown from new data
|
||||
/*
|
||||
@@ -102,13 +93,13 @@ const AddRecipe = () => {
|
||||
/>
|
||||
*/
|
||||
|
||||
data.length && setSelector(autocompleteInstance);
|
||||
// data.length && setSelector(autocompleteInstance);
|
||||
setTriggerChange(true);
|
||||
}
|
||||
}, [data, options, selected])
|
||||
}, [data, options])
|
||||
|
||||
// once the dropdown data has populated, mount it within the full form
|
||||
useEffect(() => {
|
||||
/* useEffect(() => {
|
||||
triggerChange && setForm(
|
||||
<Form<IRecipe> _config={{
|
||||
parent: "AddRecipe",
|
||||
@@ -118,10 +109,9 @@ const AddRecipe = () => {
|
||||
initialState: input,
|
||||
getState: getFormState,
|
||||
richTextInitialValue: "<p>Enter recipe details here!</p>",
|
||||
selectorInstance: selector
|
||||
}} />
|
||||
)
|
||||
}, [triggerChange])
|
||||
}, [triggerChange]) */
|
||||
|
||||
useEffect(() => {
|
||||
console.log(options);
|
||||
@@ -139,6 +129,7 @@ const AddRecipe = () => {
|
||||
}, [user])
|
||||
|
||||
// store input data from form
|
||||
/*
|
||||
const getFormState = useCallback((data: IRecipe) => {
|
||||
setInput(data);
|
||||
}, [input])
|
||||
@@ -153,7 +144,11 @@ const AddRecipe = () => {
|
||||
setSelected((prev) => {
|
||||
return prev.filter(option => option !== target);
|
||||
})
|
||||
}
|
||||
} */
|
||||
|
||||
useEffect(() => {
|
||||
return;
|
||||
}, [ingredientFields])
|
||||
|
||||
// submit handler
|
||||
const handleCreate = async () => {
|
||||
@@ -179,14 +174,70 @@ const AddRecipe = () => {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const destroySelector = useCallback((position: number) => {
|
||||
setIngredientFields((prev) => {
|
||||
const newState = new Array<JSX.Element>();
|
||||
|
||||
for (let i = 0; i < prev.length; i++) {
|
||||
if (i === position) {
|
||||
continue;
|
||||
} else {
|
||||
newState.push(prev[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
})
|
||||
}, [ingredientFields]);
|
||||
|
||||
function handleNewOption() {
|
||||
setIngredientFields((prev) => [...prev, <IngredientSelector position={optionCount + 1} key={v4()} ingredients={data} destroy={destroySelector} />])
|
||||
setOptionCount(prev => prev + 1);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log(optionCount);
|
||||
}, [optionCount])
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<h1>Add a New Recipe</h1>
|
||||
<Divider />
|
||||
|
||||
<Panel extraStyles="width-80">
|
||||
{ form }
|
||||
<Panel id="create-recipe-panel" extraClasses="ui-form-component width-80">
|
||||
<div className="form-row">
|
||||
<label>Recipe Name:</label>
|
||||
<input />
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label>Prep Time:</label>
|
||||
<input />
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<label>Course:</label>
|
||||
<input />
|
||||
</div>
|
||||
|
||||
{ data && (
|
||||
<Card extraClasses="form-row flex-row ingredient-card">
|
||||
<label>Ingredients:</label>
|
||||
<div id="ingredient-container">
|
||||
{ ingredientFields }
|
||||
<Button onClick={handleNewOption}>Add Ingredient</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="form-row">
|
||||
<label>Description:</label>
|
||||
{ "description here" }
|
||||
</div>
|
||||
|
||||
<Button onClick={handleCreate}>Create Recipe!</Button>
|
||||
|
||||
<div id="toast">{ toast }</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AuthContext, useAuthContext } from "../../context/AuthContext";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { IUser, IUserAuth } from "../../schemas";
|
||||
import { Button, Form, Page, Panel } from "../ui";
|
||||
import { FormConfig } from "../ui/Form";
|
||||
import { FormConfig } from "../ui/Form/Form";
|
||||
import API from "../../util/API";
|
||||
|
||||
export default function Login() {
|
||||
@@ -41,7 +41,7 @@ export default function Login() {
|
||||
<Page>
|
||||
<h1>Hello! Nice to see you again.</h1>
|
||||
|
||||
<Panel extraStyles="form-panel">
|
||||
<Panel extraClasses="form-panel">
|
||||
|
||||
<Form<IUserAuth> _config={{
|
||||
parent: 'login',
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IUser } from "../../../schemas";
|
||||
import API from "../../../util/API";
|
||||
import { Button, Page, Panel } from "../../ui";
|
||||
import Divider from "../../ui/Divider";
|
||||
import Form from "../../ui/Form";
|
||||
import Form from "../../ui/Form/Form";
|
||||
|
||||
const blankUser: IUser = {
|
||||
firstname: '',
|
||||
@@ -52,7 +52,7 @@ const AboutYou: RegisterVariantType = ({ transitionDisplay }) => {
|
||||
|
||||
<h2>Tell us a bit about yourself:</h2>
|
||||
|
||||
<Panel extraStyles="form-panel two-columns">
|
||||
<Panel extraClasses="form-panel two-columns">
|
||||
|
||||
<Form<IUser> _config={{
|
||||
parent: "register",
|
||||
|
||||
@@ -8,7 +8,7 @@ const Welcome = () => {
|
||||
const { user } = useAuthContext();
|
||||
|
||||
const authUserActions = (
|
||||
<Panel extraStyles="inherit-background c-papyrus uppercase flexrow">
|
||||
<Panel extraClasses="inherit-background c-papyrus uppercase flexrow">
|
||||
<Button onClick={() => navigate('/explore')}>Browse Recipes</Button>
|
||||
<Button onClick={() => navigate('/subscriptions')}>Subscriptions</Button>
|
||||
<Button onClick={() => navigate('/grocery-list')}>Grocery Lists</Button>
|
||||
@@ -16,27 +16,27 @@ const Welcome = () => {
|
||||
)
|
||||
|
||||
const callToRegister = (
|
||||
<Panel extraStyles="inherit-background c-papyrus uppercase">
|
||||
<Panel extraClasses="inherit-background c-papyrus uppercase">
|
||||
<h2>Ready to get started?</h2>
|
||||
<Button onClick={() => navigate('/register')}>Register</Button>
|
||||
</Panel>
|
||||
)
|
||||
|
||||
return (
|
||||
<Page extraStyles="narrow-dividers">
|
||||
<Panel extraStyles='inherit-background c-papyrus uppercase'>
|
||||
<Page extraClasses="narrow-dividers">
|
||||
<Panel extraClasses='inherit-background c-papyrus uppercase'>
|
||||
<h1>Welcome to Recipin</h1>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel extraStyles="inherit-background c-papyrus uppercase">
|
||||
<Panel extraClasses="inherit-background c-papyrus uppercase">
|
||||
<h2>Simple Recipe Management and Sharing for the Home</h2>
|
||||
</Panel>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Panel extraStyles="inherit-background c-papyrus uppercase">
|
||||
<Panel extraClasses="inherit-background c-papyrus uppercase">
|
||||
<h2>Build Shopping Lists Directly from Your Recipes</h2>
|
||||
</Panel>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import Protect from "../../util/Protect";
|
||||
import Form from "./Form";
|
||||
import Form from "./Form/Form";
|
||||
|
||||
interface BrowserProps {
|
||||
children?: JSX.Element[]
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ButtonComponent } from "../../util/types"
|
||||
import "/src/sass/components/Button.scss";
|
||||
|
||||
const Button: ButtonComponent = ({ onClick = (() => {}), children, extraStyles, disabled = false, disabledText = null }) => {
|
||||
const Button: ButtonComponent = ({ onClick = (() => {}), children, extraClasses, disabled = false, disabledText = null }) => {
|
||||
return (
|
||||
<button onClick={onClick} disabled={disabled} className={`ui-button ${extraStyles || ''}`}>
|
||||
<button onClick={onClick} disabled={disabled} className={`ui-button ${extraClasses || ''}`}>
|
||||
{ disabled ? (disabledText || children || "Button") : (children || "Button") }
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FC } from "react";
|
||||
|
||||
|
||||
const Card: FC<{ children?: JSX.Element | JSX.Element[], extraStyles?: string }> = ({ children = <></>, extraStyles = ""}) => {
|
||||
const Card: FC<{ children?: JSX.Element | JSX.Element[], extraClasses?: string }> = ({ children = <></>, extraClasses = ""}) => {
|
||||
return (
|
||||
<div className={`ui-card ${extraStyles}`}>
|
||||
<div className={`ui-card ${extraClasses}`}>
|
||||
{ Array.isArray(children) ? <>{children}</> : children }
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -4,9 +4,9 @@ import { PortalBase } from "../../util/types";
|
||||
import "/src/sass/components/Dropdown.scss";
|
||||
|
||||
// expects to receive buttons as children
|
||||
const Dropdown: FC<PortalBase> = ({ children, extraStyles = null }) => {
|
||||
const Dropdown: FC<PortalBase> = ({ children, extraClasses = null }) => {
|
||||
return (
|
||||
<Panel extraStyles={`ui-dropdown ${extraStyles}`}>
|
||||
<Panel extraClasses={`ui-dropdown ${extraClasses}`}>
|
||||
{ children }
|
||||
</Panel>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ChangeEvent, FC, useEffect, useState } from "react"
|
||||
import { v4 } from "uuid"
|
||||
import { useAuthContext } from "../../context/AuthContext";
|
||||
import { useSelectorContext } from "../../context/SelectorContext";
|
||||
import SelectorProvider from "../../context/SelectorProvider";
|
||||
import { IIngredient, IUser } from "../../schemas";
|
||||
import API from "../../util/API";
|
||||
import RichText from "./RichText"
|
||||
import Selector from "./Selector";
|
||||
import { useAuthContext } from "../../../context/AuthContext";
|
||||
import { useSelectorContext } from "../../../context/SelectorContext";
|
||||
import SelectorProvider from "../../../context/SelectorProvider";
|
||||
import { IIngredient, IUser } from "../../../schemas";
|
||||
import API from "../../../util/API";
|
||||
import RichText from "../RichText"
|
||||
import Selector from "../Selector";
|
||||
import "/src/sass/components/Form.scss";
|
||||
|
||||
export interface FormConfig<T> {
|
||||
@@ -17,7 +17,7 @@ export interface FormConfig<T> {
|
||||
labels?: string[]
|
||||
dataTypes?: string[]
|
||||
richTextInitialValue?: string
|
||||
extraStyles?: string
|
||||
extraClasses?: string
|
||||
selectorInstance?: JSX.Element
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ function Form<T>({ _config }: FormProps) {
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<div className={`ui-form-component ${_config.extraStyles ?? ""}`}>
|
||||
<div className={`ui-form-component ${_config.extraClasses ?? ""}`}>
|
||||
{ contents }
|
||||
</div>
|
||||
)
|
||||
38
client/src/components/ui/Form/FormRow.tsx
Normal file
38
client/src/components/ui/Form/FormRow.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
interface FormRowConfig {
|
||||
parent: any
|
||||
labelText: string
|
||||
idx?: number
|
||||
dataType?: string
|
||||
}
|
||||
|
||||
function FormRow({ parent, labelText, idx, dataType = "string" }) {
|
||||
const [row, setRow] = useState<JSX.Element>();
|
||||
|
||||
useEffect(() => {
|
||||
switch (dataType) {
|
||||
case "TINYMCE":
|
||||
break;
|
||||
case "string":
|
||||
default:
|
||||
setRow(
|
||||
<div className="form-row" id={`${parent}-row-${idx || v4()}`} key={v4()}>
|
||||
<label htmlFor={`${parent}-${each}`}>{labelText}</label>
|
||||
<input
|
||||
type={dataType}
|
||||
id={`${parent}-${each}`}
|
||||
onChange={(e) => update(e, idx)}
|
||||
value={state[i as keyof T] as string}>
|
||||
</input>
|
||||
</div>
|
||||
)
|
||||
break;
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ const LoggedIn = () => {
|
||||
</div>
|
||||
{
|
||||
dropdownActive && (
|
||||
<Dropdown extraStyles="top-menu-bar actions-bar">
|
||||
<Dropdown extraClasses="top-menu-bar actions-bar">
|
||||
<Button onClick={() => handleOptionSelect('/add-recipe')}>Add a Recipe</Button>
|
||||
<Button onClick={() => handleOptionSelect("/add-friends")}>Add Friends</Button>
|
||||
<Button onClick={() => handleOptionSelect('/collections')}>My Collections</Button>
|
||||
@@ -65,7 +65,7 @@ const LoggedIn = () => {
|
||||
}
|
||||
{
|
||||
searchActive && (
|
||||
<Dropdown extraStyles="top-menu-bar search-bar">
|
||||
<Dropdown extraClasses="top-menu-bar search-bar">
|
||||
<Button>Run Search</Button>
|
||||
</Dropdown>
|
||||
)
|
||||
|
||||
@@ -5,10 +5,10 @@ import { PageComponent } from "../../util/types"
|
||||
import Navbar from "./Navbar";
|
||||
import "/src/sass/components/Page.scss";
|
||||
|
||||
const Page: PageComponent = ({ extraStyles, children }) => {
|
||||
const Page: PageComponent = ({ extraClasses, children }) => {
|
||||
return (
|
||||
<main id="view">
|
||||
<section className={`Page ${extraStyles || null}`}>
|
||||
<section className={`Page ${extraClasses || null}`}>
|
||||
{ children || null }
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { PanelComponent } from "../../util/types";
|
||||
import "/src/sass/components/Panel.scss";
|
||||
|
||||
const Panel: PanelComponent = ({ children, extraStyles }) => {
|
||||
return (
|
||||
<div className={`Panel ${extraStyles || ''}`}>
|
||||
{ children || null }
|
||||
</div>
|
||||
)
|
||||
const Panel: PanelComponent = ({ children, extraClasses, id }) => {
|
||||
if (id) {
|
||||
return (
|
||||
<div id={id} className={`Panel ${extraClasses || ''}`}>
|
||||
{ children || null }
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div className={`Panel ${extraClasses || ''}`}>
|
||||
{ children || null }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Panel;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormConfig } from "./Form"
|
||||
import { FormConfig } from "./Form/Form"
|
||||
|
||||
interface OptionType {
|
||||
value: number
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FC } from "react";
|
||||
import { PortalBase } from "../../util/types";
|
||||
|
||||
const Tooltip: FC<PortalBase> = ({ children, extraStyles = null }) => {
|
||||
const Tooltip: FC<PortalBase> = ({ children, extraClasses = null }) => {
|
||||
return (
|
||||
<aside className={`ui-tooltip ${extraStyles}`}>
|
||||
<aside className={`ui-tooltip ${extraClasses}`}>
|
||||
{ children }
|
||||
</aside>
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import API from "../../util/API";
|
||||
import Button from "./Button";
|
||||
import Card from "./Card";
|
||||
|
||||
const UserCard: UserCardType = ({ extraStyles, targetUser }) => {
|
||||
const UserCard: UserCardType = ({ extraClasses, targetUser }) => {
|
||||
const [buttonVariant, setButtonVariant] = useState(<></>);
|
||||
const { token } = useAuthContext();
|
||||
|
||||
@@ -45,7 +45,7 @@ const UserCard: UserCardType = ({ extraStyles, targetUser }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card extraStyles={'user-card' + extraStyles}>
|
||||
<Card extraClasses={'user-card' + extraClasses}>
|
||||
<>
|
||||
<div className="avatar"></div>
|
||||
<h3><a href={`/profile?id=${targetUser.id}`}>{targetUser.firstname} {targetUser.lastname.substring(0,1)}.</a></h3>
|
||||
|
||||
@@ -2,7 +2,7 @@ import Button from "./Button";
|
||||
import Card from "./Card";
|
||||
import Divider from "./Divider";
|
||||
import Dropdown from "./Dropdown";
|
||||
import Form from "./Form";
|
||||
import Form from "./Form/Form";
|
||||
import Navbar from "./Navbar";
|
||||
import Page from "./Page";
|
||||
import Panel from "./Panel";
|
||||
|
||||
@@ -7,7 +7,3 @@
|
||||
text-align: center;
|
||||
background-color: $richblack;
|
||||
}
|
||||
|
||||
.ui-creatable-component {
|
||||
margin-bottom: 10rem;
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import { IUser } from "../schemas";
|
||||
|
||||
export interface PortalBase {
|
||||
children?: ReactNode | ReactNode[]
|
||||
extraStyles?: string
|
||||
extraClasses?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
interface ButtonParams extends PortalBase {
|
||||
|
||||
Reference in New Issue
Block a user