🐱😒💋
This commit is contained in:
parent
486955a861
commit
c73bcf8a57
10512
frontend/package-lock.json
generated
10512
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -14,7 +14,7 @@
|
||||||
"@types/react-dom": "^17.0.0",
|
"@types/react-dom": "^17.0.0",
|
||||||
"antd": "^4.14.0",
|
"antd": "^4.14.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"node-sass": "^5.0.0",
|
"node-sass": "^4.0.0",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Api } from './api'
|
||||||
import './scss/app.scss'
|
import './scss/app.scss'
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const api = new Api({ baseURL: '/api' })
|
const api = new Api({ mock: true, baseURL: '/api' })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|
49
frontend/src/api/data/index.ts
Normal file
49
frontend/src/api/data/index.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { DataBuddy } from '@dank-inc/data-buddy'
|
||||||
|
import { Account, Stack, Transaction, User } from '../../types'
|
||||||
|
|
||||||
|
export const users = new DataBuddy<User>([
|
||||||
|
{
|
||||||
|
id: 'mock-user',
|
||||||
|
name: 'TestUser42',
|
||||||
|
email: 'testuser@email.com',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
export const accounts = new DataBuddy<Account>([
|
||||||
|
{
|
||||||
|
id: 'home',
|
||||||
|
name: 'Home Expenses',
|
||||||
|
details: 'ya',
|
||||||
|
users: ['42'],
|
||||||
|
income: 1000,
|
||||||
|
expenses: 500,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
export const stacks = new DataBuddy<Stack>([
|
||||||
|
{
|
||||||
|
id: 'ccrap',
|
||||||
|
name: 'crap',
|
||||||
|
account: 'asdf',
|
||||||
|
amount: 200,
|
||||||
|
details: 'for all my crap!',
|
||||||
|
transactions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shit',
|
||||||
|
name: 'shit',
|
||||||
|
account: 'home',
|
||||||
|
amount: 500,
|
||||||
|
details: 'for all my shit!',
|
||||||
|
transactions: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'poo',
|
||||||
|
name: 'poo',
|
||||||
|
account: 'home',
|
||||||
|
amount: 800,
|
||||||
|
details: 'for all my poo!',
|
||||||
|
transactions: [],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
export const transactions = new DataBuddy<Transaction>([])
|
|
@ -1,14 +0,0 @@
|
||||||
import { DataBuddy } from '@dank-inc/data-buddy'
|
|
||||||
import { Account, Stack, Transaction, User } from '../../types'
|
|
||||||
|
|
||||||
export const users = new DataBuddy<User>([
|
|
||||||
{
|
|
||||||
id: '42',
|
|
||||||
name: 'TestUser42',
|
|
||||||
email: 'testuser@email.com',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
export const accounts = new DataBuddy<Account>([])
|
|
||||||
export const stacks = new DataBuddy<Stack>([])
|
|
||||||
export const transactions = new DataBuddy<Transaction>([])
|
|
|
@ -2,7 +2,7 @@ import Axios, { AxiosInstance } from 'axios'
|
||||||
import { Account, Password, Stack, Transaction, User, uuid } from '../types'
|
import { Account, Password, Stack, Transaction, User, uuid } from '../types'
|
||||||
import { JWT, setJWT, wipeJWT } from '../utils/jwt'
|
import { JWT, setJWT, wipeJWT } from '../utils/jwt'
|
||||||
import { DataBuddy } from '@dank-inc/data-buddy'
|
import { DataBuddy } from '@dank-inc/data-buddy'
|
||||||
import { users } from './data/users'
|
import { users, accounts, stacks, transactions } from './data'
|
||||||
|
|
||||||
export type ApiParams = {
|
export type ApiParams = {
|
||||||
baseURL?: string
|
baseURL?: string
|
||||||
|
@ -14,7 +14,7 @@ export interface Api {
|
||||||
users: DataBuddy<User>
|
users: DataBuddy<User>
|
||||||
accounts: DataBuddy<Account>
|
accounts: DataBuddy<Account>
|
||||||
stacks: DataBuddy<Stack>
|
stacks: DataBuddy<Stack>
|
||||||
Transactions: DataBuddy<Transaction>
|
transactions: DataBuddy<Transaction>
|
||||||
axios: AxiosInstance
|
axios: AxiosInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,19 +22,25 @@ export class Api {
|
||||||
constructor({ mock, baseURL }: ApiParams) {
|
constructor({ mock, baseURL }: ApiParams) {
|
||||||
this.mock = mock
|
this.mock = mock
|
||||||
this.users = users
|
this.users = users
|
||||||
|
this.accounts = accounts
|
||||||
|
this.stacks = stacks
|
||||||
|
this.transactions = transactions
|
||||||
this.axios = Axios.create({ baseURL })
|
this.axios = Axios.create({ baseURL })
|
||||||
}
|
}
|
||||||
|
|
||||||
login = async (username: string, password: string): Promise<JWT> => {
|
login = async (name: string, password: string): Promise<JWT> => {
|
||||||
if (this.mock)
|
if (this.mock) {
|
||||||
return {
|
const jwt = {
|
||||||
id: 'mock-id',
|
id: 'mock-user',
|
||||||
token: 'token-token-token',
|
token: 'token-token-token',
|
||||||
exp: +new Date(),
|
exp: +new Date(),
|
||||||
}
|
}
|
||||||
|
setJWT(jwt)
|
||||||
|
return jwt
|
||||||
|
}
|
||||||
|
|
||||||
const { data } = await this.axios.post<JWT>(`/api/dj-rest-auth/login/`, {
|
const { data } = await this.axios.post<JWT>(`/api/dj-rest-auth/login/`, {
|
||||||
username,
|
name,
|
||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -65,11 +71,13 @@ export class Api {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccounts = async () => {
|
getAccounts = async () => {
|
||||||
|
if (this.mock) return this.accounts.get()
|
||||||
const { data } = await this.axios.get<Account[]>('accounts')
|
const { data } = await this.axios.get<Account[]>('accounts')
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
getAccount = async (id: uuid) => {
|
getAccount = async (id: uuid) => {
|
||||||
|
if (this.mock) return this.accounts.getOne(id)
|
||||||
const data = await this.axios.get<Account>(`accounts/${id}`)
|
const data = await this.axios.get<Account>(`accounts/${id}`)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
@ -84,20 +92,25 @@ export class Api {
|
||||||
deleteAccount = async () => {}
|
deleteAccount = async () => {}
|
||||||
|
|
||||||
getStacks = async (): Promise<Stack[]> => {
|
getStacks = async (): Promise<Stack[]> => {
|
||||||
|
if (this.mock) return this.stacks.get()
|
||||||
const { data } = await this.axios.get('stacks')
|
const { data } = await this.axios.get('stacks')
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStack = async (id: uuid, body: Partial<Stack>) => {
|
updateStack = async (id: uuid, body: Partial<Stack>) => {
|
||||||
const { data } = await this.axios.patch<Stack>(`stacks/${id}`, body)
|
const { data } = await this.axios.patch<Stack>(`stacks/${id}`, body)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
createStack = async () => {}
|
createStack = async () => {}
|
||||||
deleteStack = async () => {}
|
deleteStack = async () => {}
|
||||||
|
|
||||||
getTransactions = async () => {
|
getTransactions = async () => {
|
||||||
|
if (this.mock) return this.transactions.get()
|
||||||
const { data } = await this.axios.get('transactions')
|
const { data } = await this.axios.get('transactions')
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTransaction = async (id: uuid, body: Partial<Transaction>) => {
|
updateTransaction = async (id: uuid, body: Partial<Transaction>) => {
|
||||||
const { data } = await this.axios.patch<Transaction>(
|
const { data } = await this.axios.patch<Transaction>(
|
||||||
`transactions/${id}`,
|
`transactions/${id}`,
|
||||||
|
@ -105,6 +118,7 @@ export class Api {
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
createTransaction = async (body: Omit<Transaction, 'id'>) => {
|
createTransaction = async (body: Omit<Transaction, 'id'>) => {
|
||||||
const { data } = await this.axios.post<Transaction>('transactions', body)
|
const { data } = await this.axios.post<Transaction>('transactions', body)
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import { useUserContext } from '../contexts/UserContext'
|
import { useUserContext } from '../contexts/UserContext'
|
||||||
import { Redirect, Route, Switch } from 'react-router'
|
import { Route, Switch } from 'react-router'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { Dashboard } from './pages/Dashboard'
|
import { Dashboard } from './pages/Dashboard'
|
||||||
import { UserForm } from './components/UserForm'
|
import { UserForm } from './components/UserForm'
|
||||||
import { TransactionList } from './components/TransactionList'
|
import { TransactionList } from './components/TransactionList'
|
||||||
import { AccountForm } from './components/AccountForm'
|
import { AccountForm } from './components/AccountForm'
|
||||||
|
import { Login } from './components/Login'
|
||||||
|
|
||||||
export const CoreLayout = () => {
|
export const CoreLayout = () => {
|
||||||
const { user, accounts, selectedAccount } = useUserContext()
|
const { user, selectedAccount } = useUserContext()
|
||||||
|
|
||||||
if (!accounts?.length) <Redirect to="/account/new" />
|
if (!user) return <Login />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app" id="appElement">
|
<div className="app">
|
||||||
<nav>
|
<nav>
|
||||||
<Link to="/">Home</Link>
|
<Link to="/">Home</Link>
|
||||||
<Link to="/select">Select Budget</Link>
|
<Link to="/select">Select Budget</Link>
|
||||||
|
|
|
@ -72,7 +72,7 @@ export const AccountForm = ({ account }: Props) => {
|
||||||
</label>
|
</label>
|
||||||
<h3>Budgets</h3>
|
<h3>Budgets</h3>
|
||||||
{stacks.data?.map((stack) => (
|
{stacks.data?.map((stack) => (
|
||||||
<div className="form-item">
|
<div key={stack.details} className="form-item">
|
||||||
<label>{stack.name}</label>
|
<label>{stack.name}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
|
|
|
@ -1,71 +1,40 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import { Button, Form, Input } from 'antd'
|
||||||
import Axios from 'axios'
|
import FormItem from 'antd/lib/form/FormItem'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import { User } from '../../types'
|
||||||
|
|
||||||
type Props = {
|
import '../../scss/login.scss'
|
||||||
handleLogin: (v: string) => void
|
import { useUserContext } from '../../contexts/UserContext'
|
||||||
}
|
|
||||||
|
|
||||||
export const Login = ({ handleLogin }: Props) => {
|
type FormValues = Pick<User, 'name'> & { password: string }
|
||||||
const [email, setEmail] = useState('')
|
|
||||||
const [password, setPassword] = useState('')
|
|
||||||
const [valid, setValid] = useState(false)
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
export const Login = () => {
|
||||||
if (window.localStorage.userId) handleLogin(window.localStorage.userId)
|
const userContext = useUserContext()
|
||||||
}, [handleLogin])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const [form] = Form.useForm<FormValues>()
|
||||||
email && password ? setValid(true) : setValid(false)
|
|
||||||
}, [email, password])
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleFinish = ({ name, password }: FormValues) => {
|
||||||
e.preventDefault()
|
userContext.handleLogin(name, password)
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
handleLogin(email)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!email || !password) {
|
|
||||||
setError('please fill out both fields')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { data } = await Axios.post('/api/login', { email, password })
|
|
||||||
handleLogin(data.id)
|
|
||||||
window.localStorage.userId = data.id
|
|
||||||
} catch (err) {
|
|
||||||
setError(err.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login">
|
<div className="login">
|
||||||
<form onSubmit={handleSubmit}>
|
<h1>Log In</h1>
|
||||||
<label>
|
<Form onFinish={handleFinish} form={form}>
|
||||||
Type Yo Email:
|
<FormItem label="Username" name="username">
|
||||||
<input
|
<Input />
|
||||||
autoFocus
|
</FormItem>
|
||||||
onChange={e => setEmail(e.target.value)}
|
<FormItem label="Password" name="password">
|
||||||
value={email}
|
<Input type="password" />
|
||||||
></input>
|
</FormItem>
|
||||||
</label>
|
|
||||||
<label>
|
<div className="form-footer">
|
||||||
Type Yo Password:
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
value={password}
|
|
||||||
></input>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<button disabled={!valid} type="submit">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</label>
|
|
||||||
{error && <p>{error}</p>}
|
|
||||||
<Link to="/sign-up">No Account? Sign Up!</Link>
|
<Link to="/sign-up">No Account? Sign Up!</Link>
|
||||||
</form>
|
<Button type="primary" htmlType="submit">
|
||||||
|
Log In!
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ export const UserForm = () => {
|
||||||
const { api } = useAppContext()
|
const { api } = useAppContext()
|
||||||
const { user } = useUserContext()
|
const { user } = useUserContext()
|
||||||
|
|
||||||
|
console.log(user)
|
||||||
|
|
||||||
const [name, setName] = useState(user?.name)
|
const [name, setName] = useState(user?.name)
|
||||||
const [email, setEmail] = useState(user?.email)
|
const [email, setEmail] = useState(user?.email)
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const Dashboard = () => {
|
||||||
<h1>Remaining Balances</h1>
|
<h1>Remaining Balances</h1>
|
||||||
<div className="funds">
|
<div className="funds">
|
||||||
{stacks.data.map((stack, i) => (
|
{stacks.data.map((stack, i) => (
|
||||||
<FundBar stack={stack} col={i + 1} />
|
<FundBar key={stack.id} stack={stack} col={i + 1} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -9,7 +9,6 @@ type Credentials = {
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
const { handleLogin } = useUserContext()
|
const { handleLogin } = useUserContext()
|
||||||
|
|
||||||
const [form] = useForm<Credentials>()
|
const [form] = useForm<Credentials>()
|
||||||
|
|
||||||
const handleSubmit = ({ username, password }: Credentials) => {
|
const handleSubmit = ({ username, password }: Credentials) => {
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export const Profile = () => {
|
|
||||||
return <p>Look, A user profile!</p>
|
|
||||||
}
|
|
|
@ -13,7 +13,7 @@ type Context = {
|
||||||
user: User | null
|
user: User | null
|
||||||
accounts: Account[] | null
|
accounts: Account[] | null
|
||||||
selectedAccount: Account | null
|
selectedAccount: Account | null
|
||||||
handleLogin: (username: string, password: string) => void
|
handleLogin: (name: string, password: string) => void
|
||||||
handleLogout: () => void
|
handleLogout: () => void
|
||||||
handleSelectAccount: (id: uuid) => void
|
handleSelectAccount: (id: uuid) => void
|
||||||
}
|
}
|
||||||
|
@ -28,11 +28,14 @@ export const UserContextProvider = ({ children }: Props) => {
|
||||||
const [accounts, setAccounts] = useState<Account[] | null>(null)
|
const [accounts, setAccounts] = useState<Account[] | null>(null)
|
||||||
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null)
|
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null)
|
||||||
|
|
||||||
const handleLogin = async (username: string, password: string) => {
|
const handleLogin = async (name: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
const { id } = await api.login(username, password)
|
const { id } = await api.login(name, password)
|
||||||
if (!id) throw new Error('Problem logging in!')
|
if (!id) throw new Error('Problem logging in!')
|
||||||
setUser(await api.getUser(id))
|
const user = await api.getUser(id)
|
||||||
|
|
||||||
|
if (!user) message.error(`Couldn't find user`)
|
||||||
|
setUser(user)
|
||||||
|
|
||||||
const accounts = await api.getAccounts()
|
const accounts = await api.getAccounts()
|
||||||
setAccounts(accounts)
|
setAccounts(accounts)
|
||||||
|
|
|
@ -4,6 +4,35 @@
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col1 {
|
||||||
|
grid-column: 1/1;
|
||||||
|
grid-row: 1/1;
|
||||||
|
}
|
||||||
|
.col2 {
|
||||||
|
grid-column: 2/2;
|
||||||
|
grid-row: 1/1;
|
||||||
|
}
|
||||||
|
.col3 {
|
||||||
|
grid-column: 3/3;
|
||||||
|
grid-row: 1/1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5 {
|
||||||
|
color: #222;
|
||||||
|
text-shadow: -1px -1px #444;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
|
@ -1,26 +1,3 @@
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5 {
|
|
||||||
color: #222;
|
|
||||||
text-shadow: -1px -1px #444;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col1 {
|
|
||||||
grid-column: 1/1;
|
|
||||||
grid-row: 1/1;
|
|
||||||
}
|
|
||||||
.col2 {
|
|
||||||
grid-column: 2/2;
|
|
||||||
grid-row: 1/1;
|
|
||||||
}
|
|
||||||
.col3 {
|
|
||||||
grid-column: 3/3;
|
|
||||||
grid-row: 1/1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app {
|
.app {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -89,29 +66,6 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login {
|
|
||||||
color: #ccc;
|
|
||||||
form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
height: 100%;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
label {
|
|
||||||
input {
|
|
||||||
margin-left: 2ch;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 70vmin;
|
|
||||||
height: 70vmin;
|
|
||||||
margin: auto;
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo {
|
.todo {
|
||||||
color: #111a;
|
color: #111a;
|
||||||
}
|
}
|
||||||
|
|
33
frontend/src/scss/login.scss
Normal file
33
frontend/src/scss/login.scss
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
.login {
|
||||||
|
display: flex;
|
||||||
|
box-shadow: 5px 5px #111, 2px 2px #111;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: auto;
|
||||||
|
background: #222;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
padding: 2rem 2rem;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.ant-row {
|
||||||
|
margin: 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 50%;
|
||||||
|
margin: auto 0 auto 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1802
frontend/yarn.lock
1802
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user