mirror of
https://github.com/usatiuk/writer.git
synced 2025-10-29 00:17:48 +01:00
create new documents
This commit is contained in:
33
frontend/package-lock.json
generated
33
frontend/package-lock.json
generated
@@ -3315,14 +3315,12 @@
|
|||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
@@ -3337,20 +3335,17 @@
|
|||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@@ -3467,8 +3462,7 @@
|
|||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
@@ -3480,7 +3474,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -3495,7 +3488,6 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
@@ -3503,14 +3495,12 @@
|
|||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
@@ -3529,7 +3519,6 @@
|
|||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
@@ -3610,8 +3599,7 @@
|
|||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@@ -3623,7 +3611,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
@@ -3745,7 +3732,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
@@ -4638,6 +4624,11 @@
|
|||||||
"sshpk": "^1.7.0"
|
"sshpk": "^1.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"http2": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/http2/-/http2-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-puSi8M8WNlFJm9Pk4c/Mbz9Gwparuj3gO9/RRO5zv6piQ0FY+9Qywp0PdWshYgsMJSalixFY7eC6oPu0zRxLAQ=="
|
||||||
|
},
|
||||||
"https-browserify": {
|
"https-browserify": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blueprintjs/core": "^3.13.0",
|
"@blueprintjs/core": "^3.13.0",
|
||||||
"@blueprintjs/icons": "^3.6.0",
|
"@blueprintjs/icons": "^3.6.0",
|
||||||
|
"http2": "^3.3.7",
|
||||||
"rea": "0.0.1",
|
"rea": "0.0.1",
|
||||||
"react": "^16.8.1",
|
"react": "^16.8.1",
|
||||||
"react-dom": "^16.8.1",
|
"react-dom": "^16.8.1",
|
||||||
|
|||||||
@@ -34,6 +34,13 @@
|
|||||||
color: $dark-gray5;
|
color: $dark-gray5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.card.newDocumentCard {
|
||||||
|
background-color: $light-gray5;
|
||||||
|
color: $dark-gray2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +71,6 @@
|
|||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp3-dark {
|
.bp3-dark {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { IDocumentJSON } from "~../../src/entity/Document";
|
|
||||||
|
|
||||||
import { DocumentCard } from "./DocumentCard";
|
|
||||||
|
|
||||||
export function DocsList({ docs }: { docs: IDocumentJSON[] }) {
|
|
||||||
const cards = docs.map(doc => <DocumentCard key={doc.id} doc={doc} />);
|
|
||||||
return <div className="list">{cards}</div>;
|
|
||||||
}
|
|
||||||
25
frontend/src/Documents/DocumentsList.tsx
Normal file
25
frontend/src/Documents/DocumentsList.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { IDocumentJSON } from "~../../src/entity/Document";
|
||||||
|
|
||||||
|
import { DocumentCard } from "./DocumentCard";
|
||||||
|
import { NewDocumentCard } from "./NewDocumentCard";
|
||||||
|
|
||||||
|
export interface IDocumentListProps {
|
||||||
|
docs: IDocumentJSON[];
|
||||||
|
newDocument?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DocumentsList(props: IDocumentListProps) {
|
||||||
|
const cards = props.docs.map(doc => (
|
||||||
|
<DocumentCard key={doc.id} doc={doc} />
|
||||||
|
));
|
||||||
|
if (props.newDocument) {
|
||||||
|
return (
|
||||||
|
<div className="list">
|
||||||
|
{<NewDocumentCard />}
|
||||||
|
{cards}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div className="list">{cards}</div>;
|
||||||
|
}
|
||||||
35
frontend/src/Documents/NewDocumentCard.tsx
Normal file
35
frontend/src/Documents/NewDocumentCard.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Card, Icon } from "@blueprintjs/core";
|
||||||
|
import * as React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { RouteChildrenProps, withRouter } from "react-router";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import { newDocStart } from "~redux/docs/actions";
|
||||||
|
|
||||||
|
export interface INewDocumentCardComponentProps extends RouteChildrenProps {
|
||||||
|
createNewDoc: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewDocumentCardComponent(
|
||||||
|
props: INewDocumentCardComponentProps,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="card newDocumentCard"
|
||||||
|
interactive={true}
|
||||||
|
onClick={() => props.createNewDoc()}
|
||||||
|
>
|
||||||
|
<Icon icon="add" iconSize={40} className="newDocumentIcon" />
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
|
return { createNewDoc: () => dispatch(newDocStart()) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NewDocumentCard = withRouter(
|
||||||
|
connect(
|
||||||
|
null,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(NewDocumentCardComponent),
|
||||||
|
);
|
||||||
@@ -9,7 +9,7 @@ import { LoadingStub } from "~LoadingStub";
|
|||||||
import { fetchDocsStart } from "~redux/docs/actions";
|
import { fetchDocsStart } from "~redux/docs/actions";
|
||||||
import { IAppState } from "~redux/reducers";
|
import { IAppState } from "~redux/reducers";
|
||||||
|
|
||||||
import { DocsList } from "./DocsList";
|
import { DocumentsList } from "./DocumentsList";
|
||||||
|
|
||||||
export interface IOverviewComponentProps {
|
export interface IOverviewComponentProps {
|
||||||
allDocs: { [key: number]: IDocumentJSON };
|
allDocs: { [key: number]: IDocumentJSON };
|
||||||
@@ -43,12 +43,12 @@ export class OverviewComponent extends React.PureComponent<
|
|||||||
<div id="overview">
|
<div id="overview">
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<H3>Recent</H3>
|
<H3>Recent</H3>
|
||||||
<DocsList docs={recentCut} />
|
<DocumentsList docs={recentCut} />
|
||||||
</div>
|
</div>
|
||||||
<span className="separator" />
|
<span className="separator" />
|
||||||
<div className="section">
|
<div className="section">
|
||||||
<H3>All documents</H3>
|
<H3>All documents</H3>
|
||||||
<DocsList docs={docs} />
|
<DocumentsList docs={docs} newDocument={true} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function fetchAllDocs(): Promise<IAPIResponse<IDocumentJSON[]>> {
|
|||||||
export async function fetchDoc(
|
export async function fetchDoc(
|
||||||
id: number,
|
id: number,
|
||||||
): Promise<IAPIResponse<IDocumentJSON>> {
|
): Promise<IAPIResponse<IDocumentJSON>> {
|
||||||
return fetchJSONAuth(`docs/byID/${id}`, "GET");
|
return fetchJSONAuth(`/docs/byID/${id}`, "GET");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function patchDoc(
|
export async function patchDoc(
|
||||||
@@ -24,9 +24,13 @@ export async function patchDoc(
|
|||||||
name?: string,
|
name?: string,
|
||||||
content?: string,
|
content?: string,
|
||||||
): Promise<IAPIResponse<IDocumentJSON>> {
|
): Promise<IAPIResponse<IDocumentJSON>> {
|
||||||
return fetchJSONAuth(`docs/byID/${id}`, "PATCH", { name, content });
|
return fetchJSONAuth(`/docs/byID/${id}`, "PATCH", { name, content });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteDoc(id: number): Promise<IAPIResponse<boolean>> {
|
export async function deleteDoc(id: number): Promise<IAPIResponse<boolean>> {
|
||||||
return fetchJSONAuth(`docs/byID/${id}`, "DELETE");
|
return fetchJSONAuth(`/docs/byID/${id}`, "DELETE");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createNewDoc(): Promise<IAPIResponse<IDocumentJSON>> {
|
||||||
|
return fetchJSONAuth(`/docs/new`, "POST", { name: "New Document" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ export enum DocsTypes {
|
|||||||
DOCS_FETCH_START = "DOCS_FETCH_START",
|
DOCS_FETCH_START = "DOCS_FETCH_START",
|
||||||
DOCS_FETCH_FAIL = "DOCS_FETCH_FAIL",
|
DOCS_FETCH_FAIL = "DOCS_FETCH_FAIL",
|
||||||
DOCS_FETCH_SUCCESS = "DOCS_FETCH_SUCCESS",
|
DOCS_FETCH_SUCCESS = "DOCS_FETCH_SUCCESS",
|
||||||
|
|
||||||
|
DOC_NEW_START = "DOC_NEW_START",
|
||||||
|
DOC_NEW_FAIL = "DOC_NEW_FAIL",
|
||||||
|
DOC_NEW_SUCCESS = "DOC_NEW_SUCCESS",
|
||||||
|
|
||||||
DOCS_SHOW_SPINNER = "DOCS_SHOW_SPINNER",
|
DOCS_SHOW_SPINNER = "DOCS_SHOW_SPINNER",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +53,41 @@ export function fetchDocsSuccess(
|
|||||||
return { type: DocsTypes.DOCS_FETCH_SUCCESS, payload: { all } };
|
return { type: DocsTypes.DOCS_FETCH_SUCCESS, payload: { all } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDocNewStartAction extends Action {
|
||||||
|
type: DocsTypes.DOC_NEW_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDocNewFailAction extends Action {
|
||||||
|
type: DocsTypes.DOC_NEW_FAIL;
|
||||||
|
payload: {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDocNewSuccessAction extends Action {
|
||||||
|
type: DocsTypes.DOC_NEW_SUCCESS;
|
||||||
|
payload: {
|
||||||
|
doc: IDocumentJSON;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newDocStart(): IDocNewStartAction {
|
||||||
|
return { type: DocsTypes.DOC_NEW_START };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newDocFail(error: string): IDocNewFailAction {
|
||||||
|
return { type: DocsTypes.DOC_NEW_FAIL, payload: { error } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function newDocSuccess(doc: IDocumentJSON): IDocNewSuccessAction {
|
||||||
|
return { type: DocsTypes.DOC_NEW_SUCCESS, payload: { doc } };
|
||||||
|
}
|
||||||
|
|
||||||
export type DocsAction =
|
export type DocsAction =
|
||||||
| IDocsFetchStartAction
|
| IDocsFetchStartAction
|
||||||
| IDocsFetchFailAction
|
| IDocsFetchFailAction
|
||||||
| IDocsFetchSuccessAction
|
| IDocsFetchSuccessAction
|
||||||
| IDocsShowSpinnerAction;
|
| IDocsShowSpinnerAction
|
||||||
|
| IDocNewFailAction
|
||||||
|
| IDocNewStartAction
|
||||||
|
| IDocNewSuccessAction;
|
||||||
|
|||||||
@@ -26,12 +26,19 @@ export const docsReducer: Reducer<IDocsState, DocsAction> = (
|
|||||||
return { ...state, spinner: true };
|
return { ...state, spinner: true };
|
||||||
case DocsTypes.DOCS_FETCH_START:
|
case DocsTypes.DOCS_FETCH_START:
|
||||||
return { ...defaultDocsState, fetching: true };
|
return { ...defaultDocsState, fetching: true };
|
||||||
case DocsTypes.DOCS_FETCH_SUCCESS:
|
case DocsTypes.DOCS_FETCH_SUCCESS: {
|
||||||
const all: { [key: number]: IDocumentJSON } = {};
|
const all: { [key: number]: IDocumentJSON } = {};
|
||||||
action.payload.all.forEach(doc => {
|
action.payload.all.forEach(doc => {
|
||||||
all[doc.id] = doc;
|
all[doc.id] = doc;
|
||||||
});
|
});
|
||||||
return { ...defaultDocsState, all };
|
return { ...defaultDocsState, all };
|
||||||
|
}
|
||||||
|
case DocsTypes.DOC_NEW_SUCCESS: {
|
||||||
|
const all = { ...state.all };
|
||||||
|
const doc = action.payload.doc;
|
||||||
|
all[doc.id] = doc;
|
||||||
|
return { ...state, all };
|
||||||
|
}
|
||||||
case DocsTypes.DOCS_FETCH_FAIL:
|
case DocsTypes.DOCS_FETCH_FAIL:
|
||||||
return { ...defaultDocsState, ...action.payload };
|
return { ...defaultDocsState, ...action.payload };
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ import {
|
|||||||
race,
|
race,
|
||||||
takeLatest,
|
takeLatest,
|
||||||
} from "redux-saga/effects";
|
} from "redux-saga/effects";
|
||||||
import { fetchAllDocs } from "~redux/api/docs";
|
import { createNewDoc, fetchAllDocs } from "~redux/api/docs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DocsTypes,
|
DocsTypes,
|
||||||
fetchDocsFail,
|
fetchDocsFail,
|
||||||
fetchDocsSuccess,
|
fetchDocsSuccess,
|
||||||
|
IDocNewStartAction,
|
||||||
IDocsFetchStartAction,
|
IDocsFetchStartAction,
|
||||||
|
newDocFail,
|
||||||
|
newDocSuccess,
|
||||||
showDocsSpinner,
|
showDocsSpinner,
|
||||||
} from "./actions";
|
} from "./actions";
|
||||||
|
|
||||||
@@ -52,6 +55,38 @@ function* docsFetchStart(action: IDocsFetchStartAction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* docsSaga() {
|
function* docNewStart(action: IDocNewStartAction) {
|
||||||
yield all([takeLatest(DocsTypes.DOCS_FETCH_START, docsFetchStart)]);
|
try {
|
||||||
|
const spinner = yield fork(startSpinner);
|
||||||
|
|
||||||
|
const { response, timeout } = yield race({
|
||||||
|
response: call(createNewDoc),
|
||||||
|
timeout: delay(10000),
|
||||||
|
});
|
||||||
|
|
||||||
|
yield cancel(spinner);
|
||||||
|
|
||||||
|
if (timeout) {
|
||||||
|
yield put(newDocFail("Timeout"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
if (response.data == null) {
|
||||||
|
yield put(newDocFail(response.error));
|
||||||
|
} else {
|
||||||
|
const newDoc = response.data;
|
||||||
|
yield put(newDocSuccess(newDoc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
yield put(newDocFail("Internal error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* docsSaga() {
|
||||||
|
yield all([
|
||||||
|
takeLatest(DocsTypes.DOCS_FETCH_START, docsFetchStart),
|
||||||
|
takeLatest(DocsTypes.DOC_NEW_START, docNewStart),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class Document extends BaseEntity {
|
|||||||
@Column()
|
@Column()
|
||||||
public name: string;
|
public name: string;
|
||||||
|
|
||||||
@Column({ type: "text" })
|
@Column({ type: "text", default: "" })
|
||||||
public content: string;
|
public content: string;
|
||||||
|
|
||||||
@Column({ type: "timestamp", default: null })
|
@Column({ type: "timestamp", default: null })
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ docsRouter.post("/docs/new", async ctx => {
|
|||||||
content: string | undefined;
|
content: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!(name && content)) {
|
if (!name) {
|
||||||
ctx.throw(400);
|
ctx.throw(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user