feature: add TaskDetails component
This commit is contained in:
parent
a877cd9414
commit
90f22d322e
@ -58,7 +58,8 @@
|
|||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"storybook": "start-storybook -p 9009 -s public",
|
"storybook": "start-storybook -p 9009 -s public",
|
||||||
"build-storybook": "build-storybook -s public",
|
"build-storybook": "build-storybook -s public",
|
||||||
"generate": "graphql-codegen"
|
"generate": "graphql-codegen",
|
||||||
|
"lint": "eslint --ext js,ts,tsx src"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "react-app"
|
"extends": "react-app"
|
||||||
|
1
web/src/citadel.d.ts
vendored
1
web/src/citadel.d.ts
vendored
@ -11,6 +11,7 @@ type Task = {
|
|||||||
name: string;
|
name: string;
|
||||||
position: number;
|
position: number;
|
||||||
labels: Label[];
|
labels: Label[];
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TaskGroup = {
|
type TaskGroup = {
|
||||||
|
33
web/src/shared/components/Modal/Modal.stories.tsx
Normal file
33
web/src/shared/components/Modal/Modal.stories.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import NormalizeStyles from 'App/NormalizeStyles';
|
||||||
|
import BaseStyles from 'App/BaseStyles';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import Modal from './index';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: Modal,
|
||||||
|
title: 'Modal',
|
||||||
|
parameters: {
|
||||||
|
backgrounds: [
|
||||||
|
{ name: 'white', value: '#ffffff' },
|
||||||
|
{ name: 'gray', value: '#cdd3e1', default: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NormalizeStyles />
|
||||||
|
<BaseStyles />
|
||||||
|
<Modal
|
||||||
|
width={1040}
|
||||||
|
onClose={action('on close')}
|
||||||
|
renderContent={() => {
|
||||||
|
return <h1>Hello!</h1>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
33
web/src/shared/components/Modal/Styles.ts
Normal file
33
web/src/shared/components/Modal/Styles.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import { mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const ScrollOverlay = styled.div`
|
||||||
|
z-index: 1000000;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ClickableOverlay = styled.div`
|
||||||
|
min-height: 100%;
|
||||||
|
background: rgba(9, 30, 66, 0.54);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 50px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledModal = styled.div<{ width: number }>`
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
max-width: ${props => props.width}px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 3px;
|
||||||
|
${mixin.boxShadowMedium}
|
||||||
|
`;
|
36
web/src/shared/components/Modal/index.tsx
Normal file
36
web/src/shared/components/Modal/index.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||||
|
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
|
||||||
|
|
||||||
|
import { ScrollOverlay, ClickableOverlay, StyledModal } from './Styles';
|
||||||
|
|
||||||
|
const $root: HTMLElement = document.getElementById('root')!;
|
||||||
|
|
||||||
|
type ModalProps = {
|
||||||
|
width: number;
|
||||||
|
onClose: () => void;
|
||||||
|
renderContent: () => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Modal: React.FC<ModalProps> = ({ width, onClose: tellParentToClose, renderContent }) => {
|
||||||
|
const $modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const $clickableOverlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useOnOutsideClick($modalRef, true, tellParentToClose, $clickableOverlayRef);
|
||||||
|
useOnEscapeKeyDown(true, tellParentToClose);
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<ScrollOverlay>
|
||||||
|
<ClickableOverlay ref={$clickableOverlayRef}>
|
||||||
|
<StyledModal width={width} ref={$modalRef}>
|
||||||
|
{renderContent()}
|
||||||
|
</StyledModal>
|
||||||
|
</ClickableOverlay>
|
||||||
|
</ScrollOverlay>,
|
||||||
|
$root,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
181
web/src/shared/components/TaskDetails/Styles.ts
Normal file
181
web/src/shared/components/TaskDetails/Styles.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
import TextareaAutosize from 'react-autosize-textarea/lib';
|
||||||
|
|
||||||
|
export const TaskHeader = styled.div`
|
||||||
|
display: flex;
|
||||||
|
-webkit-box-pack: justify;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 21px 18px 0px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskMeta = styled.div`
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskActions = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskAction = styled.button`
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
font-size: 14.5px;
|
||||||
|
color: rgb(66, 82, 110);
|
||||||
|
font-family: CircularStdBook;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0px 9px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.1s ease 0s;
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
padding: 0px 30px 60px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsContent = styled.div`
|
||||||
|
width: 65%;
|
||||||
|
padding-right: 50px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsSidebar = styled.div`
|
||||||
|
width: 35%;
|
||||||
|
padding-top: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsTitleWrapper = styled.div`
|
||||||
|
height: 44px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 18px 0px 0px -8px;
|
||||||
|
display: inline-block;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsTitle = styled(TextareaAutosize)`
|
||||||
|
line-height: 1.28;
|
||||||
|
resize: none;
|
||||||
|
box-shadow: transparent 0px 0px 0px 1px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: 'Droid Sans';
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 7px 7px 8px;
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
border-image: initial;
|
||||||
|
transition: background 0.1s ease 0s;
|
||||||
|
overflow-y: hidden;
|
||||||
|
width: 100%;
|
||||||
|
color: rgb(23, 43, 77);
|
||||||
|
&:hover {
|
||||||
|
background: rgb(235, 236, 240);
|
||||||
|
}
|
||||||
|
&:focus {
|
||||||
|
box-shadow: rgb(76, 154, 255) 0px 0px 0px 1px;
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgb(76, 154, 255);
|
||||||
|
border-image: initial;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsLabel = styled.div`
|
||||||
|
padding: 20px 0px 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsAddDetailsButton = styled.div`
|
||||||
|
background-color: rgba(9, 30, 66, 0.04);
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: block;
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(9, 30, 66, 0.08);
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsEditorWrapper = styled.div`
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
padding-bottom: 9px;
|
||||||
|
z-index: 50;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsEditor = styled(TextareaAutosize)`
|
||||||
|
width: 100%;
|
||||||
|
min-height: 108px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: rgba(9, 30, 66, 0.13);
|
||||||
|
border-radius: 3px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: #fff;
|
||||||
|
border: none;
|
||||||
|
box-shadow: inset 0 0 0 2px #0079bf;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsMarkdown = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TaskDetailsControls = styled.div`
|
||||||
|
clear: both;
|
||||||
|
margin-top: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ConfirmSave = styled.div`
|
||||||
|
background-color: #5aac44;
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
float: left;
|
||||||
|
margin: 0 4px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CancelEdit = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
`;
|
@ -0,0 +1,47 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import NormalizeStyles from 'App/NormalizeStyles';
|
||||||
|
import BaseStyles from 'App/BaseStyles';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import Modal from 'shared/components/Modal';
|
||||||
|
import TaskDetails from './';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: TaskDetails,
|
||||||
|
title: 'TaskDetails',
|
||||||
|
parameters: {
|
||||||
|
backgrounds: [
|
||||||
|
{ name: 'white', value: '#ffffff' },
|
||||||
|
{ name: 'gray', value: '#cdd3e1', default: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Default = () => {
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NormalizeStyles />
|
||||||
|
<BaseStyles />
|
||||||
|
<Modal
|
||||||
|
width={1040}
|
||||||
|
onClose={action('on close')}
|
||||||
|
renderContent={() => {
|
||||||
|
return (
|
||||||
|
<TaskDetails
|
||||||
|
task={{
|
||||||
|
taskID: '1',
|
||||||
|
taskGroupID: '1',
|
||||||
|
name: 'Hello, world',
|
||||||
|
position: 1,
|
||||||
|
labels: [],
|
||||||
|
description: description,
|
||||||
|
}}
|
||||||
|
onTaskDescriptionChange={(task, desc) => setDescription(desc)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
130
web/src/shared/components/TaskDetails/index.tsx
Normal file
130
web/src/shared/components/TaskDetails/index.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Bin, Cross } from 'shared/icons';
|
||||||
|
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TaskActions,
|
||||||
|
TaskAction,
|
||||||
|
TaskMeta,
|
||||||
|
TaskHeader,
|
||||||
|
TaskDetailsContent,
|
||||||
|
TaskDetailsWrapper,
|
||||||
|
TaskDetailsSidebar,
|
||||||
|
TaskDetailsTitleWrapper,
|
||||||
|
TaskDetailsTitle,
|
||||||
|
TaskDetailsLabel,
|
||||||
|
TaskDetailsAddDetailsButton,
|
||||||
|
TaskDetailsEditor,
|
||||||
|
TaskDetailsEditorWrapper,
|
||||||
|
TaskDetailsMarkdown,
|
||||||
|
TaskDetailsControls,
|
||||||
|
ConfirmSave,
|
||||||
|
CancelEdit,
|
||||||
|
} from './Styles';
|
||||||
|
|
||||||
|
type TaskContentProps = {
|
||||||
|
onEditContent: () => void;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskContent: React.FC<TaskContentProps> = ({ description, onEditContent }) => {
|
||||||
|
return description === '' ? (
|
||||||
|
<TaskDetailsAddDetailsButton onClick={onEditContent}>Add a more detailed description</TaskDetailsAddDetailsButton>
|
||||||
|
) : (
|
||||||
|
<TaskDetailsMarkdown onClick={onEditContent}>{description}</TaskDetailsMarkdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type DetailsEditorProps = {
|
||||||
|
description: string;
|
||||||
|
onTaskDescriptionChange: (newDescription: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DetailsEditor: React.FC<DetailsEditorProps> = ({
|
||||||
|
description: initialDescription,
|
||||||
|
onTaskDescriptionChange,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const [description, setDescription] = useState(initialDescription);
|
||||||
|
const $editorWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const $editorRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const handleOutsideClick = () => {
|
||||||
|
onTaskDescriptionChange(description);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if ($editorRef && $editorRef.current) {
|
||||||
|
$editorRef.current.focus();
|
||||||
|
$editorRef.current.select();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useOnOutsideClick($editorWrapperRef, true, handleOutsideClick, null);
|
||||||
|
return (
|
||||||
|
<TaskDetailsEditorWrapper ref={$editorWrapperRef}>
|
||||||
|
<TaskDetailsEditor
|
||||||
|
ref={$editorRef}
|
||||||
|
value={description}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<TaskDetailsControls>
|
||||||
|
<ConfirmSave>Save</ConfirmSave>
|
||||||
|
<CancelEdit onClick={onCancel}>
|
||||||
|
<Cross size={16} />
|
||||||
|
</CancelEdit>
|
||||||
|
</TaskDetailsControls>
|
||||||
|
</TaskDetailsEditorWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TaskDetailsProps = {
|
||||||
|
task: Task;
|
||||||
|
onTaskDescriptionChange: (task: Task, newDescription: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskDetails: React.FC<TaskDetailsProps> = ({ task, onTaskDescriptionChange }) => {
|
||||||
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
|
const handleClick = () => {
|
||||||
|
setEditorOpen(!editorOpen);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TaskHeader>
|
||||||
|
<TaskMeta />
|
||||||
|
<TaskActions>
|
||||||
|
<TaskAction>
|
||||||
|
<Bin size={20} />
|
||||||
|
</TaskAction>
|
||||||
|
<TaskAction>
|
||||||
|
<Cross size={20} />
|
||||||
|
</TaskAction>
|
||||||
|
</TaskActions>
|
||||||
|
</TaskHeader>
|
||||||
|
<TaskDetailsWrapper>
|
||||||
|
<TaskDetailsContent>
|
||||||
|
<TaskDetailsTitleWrapper>
|
||||||
|
<TaskDetailsTitle value="Hello darkness my old friend" />
|
||||||
|
</TaskDetailsTitleWrapper>
|
||||||
|
<TaskDetailsLabel>Description</TaskDetailsLabel>
|
||||||
|
{editorOpen ? (
|
||||||
|
<DetailsEditor
|
||||||
|
description={task.description ?? ''}
|
||||||
|
onTaskDescriptionChange={newDescription => {
|
||||||
|
setEditorOpen(false);
|
||||||
|
onTaskDescriptionChange(task, newDescription);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setEditorOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TaskContent description={task.description ?? ''} onEditContent={handleClick} />
|
||||||
|
)}
|
||||||
|
</TaskDetailsContent>
|
||||||
|
<TaskDetailsSidebar />
|
||||||
|
</TaskDetailsWrapper>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskDetails;
|
22
web/src/shared/icons/Bin.tsx
Normal file
22
web/src/shared/icons/Bin.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
size: number | string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Bin = ({ size, color }: Props) => {
|
||||||
|
return (
|
||||||
|
<svg fill={color} xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16">
|
||||||
|
<path d="M2 5v10c0 0.55 0.45 1 1 1h9c0.55 0 1-0.45 1-1v-10h-11zM5 14h-1v-7h1v7zM7 14h-1v-7h1v7zM9 14h-1v-7h1v7zM11 14h-1v-7h1v7z" />
|
||||||
|
<path d="M13.25 2h-3.25v-1.25c0-0.412-0.338-0.75-0.75-0.75h-3.5c-0.412 0-0.75 0.338-0.75 0.75v1.25h-3.25c-0.413 0-0.75 0.337-0.75 0.75v1.25h13v-1.25c0-0.413-0.338-0.75-0.75-0.75zM9 2h-3v-0.987h3v0.987z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Bin.defaultProps = {
|
||||||
|
size: 16,
|
||||||
|
color: '#000',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Bin;
|
@ -1,5 +1,6 @@
|
|||||||
import Cross from './Cross';
|
import Cross from './Cross';
|
||||||
import Bell from './Bell';
|
import Bell from './Bell';
|
||||||
|
import Bin from './Bin';
|
||||||
import Pencil from './Pencil';
|
import Pencil from './Pencil';
|
||||||
import Checkmark from './Checkmark';
|
import Checkmark from './Checkmark';
|
||||||
import User from './User';
|
import User from './User';
|
||||||
@ -11,4 +12,4 @@ import Stack from './Stack';
|
|||||||
import Question from './Question';
|
import Question from './Question';
|
||||||
import Exit from './Exit';
|
import Exit from './Exit';
|
||||||
|
|
||||||
export { Cross, Bell, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };
|
export { Cross, Bell, Bin, Exit, Pencil, Stack, Question, Home, Citadel, Checkmark, User, Users, Lock };
|
||||||
|
Loading…
Reference in New Issue
Block a user