Add basic company subform UX demo
This commit is contained in:
		
							parent
							
								
									a3710d4b9b
								
							
						
					
					
						commit
						ee44cbf2f7
					
				| @ -13,7 +13,6 @@ | ||||
|     "@types/node": "^12.0.0", | ||||
|     "@types/react": "^16.9.0", | ||||
|     "@types/react-dom": "^16.9.0", | ||||
|     "formik": "^2.1.4", | ||||
|     "moment": "^2.26.0", | ||||
|     "react": "^16.13.1", | ||||
|     "react-dom": "^16.13.1", | ||||
|  | ||||
							
								
								
									
										11
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/App.tsx
									
									
									
									
									
								
							| @ -1,14 +1,21 @@ | ||||
| import React from 'react'; | ||||
| import { Container } from "@material-ui/core"; | ||||
| import { Container, ThemeProvider, Typography } from "@material-ui/core"; | ||||
| import { CompanyForm } from "./forms/company"; | ||||
| import { studentTheme } from "./ui/theme"; | ||||
| 
 | ||||
| function App() { | ||||
|   return ( | ||||
|       <ThemeProvider theme={ studentTheme }> | ||||
|           <div className="app"> | ||||
|         <Container> | ||||
|               <Container maxWidth={"md"}> | ||||
|                   <Typography variant="h3">Zgłoszenie Praktyki</Typography> | ||||
|                   <Typography variant="subtitle1" style={{ marginBottom: '100px' }}>UX Demo</Typography> | ||||
| 
 | ||||
|                   <Typography variant="h5">Miejsce odbywania praktyki</Typography> | ||||
|                   <CompanyForm /> | ||||
|               </Container> | ||||
|           </div> | ||||
|       </ThemeProvider> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										3
									
								
								src/data/common.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/data/common.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| export interface Identifiable { | ||||
|     id?: string | ||||
| } | ||||
| @ -1,3 +1,5 @@ | ||||
| import { Identifiable } from "./common"; | ||||
| 
 | ||||
| export interface Address { | ||||
|     street: string; | ||||
|     building: string; | ||||
| @ -6,14 +8,14 @@ export interface Address { | ||||
|     country: string; | ||||
| } | ||||
| 
 | ||||
| export interface Company { | ||||
| export interface Company extends Identifiable { | ||||
|     name: string; | ||||
|     url?: string; | ||||
|     nip: string; | ||||
|     offices: BranchOffice[]; | ||||
| } | ||||
| 
 | ||||
| export interface BranchOffice { | ||||
| export interface BranchOffice extends Identifiable { | ||||
|     address: Address; | ||||
| } | ||||
| 
 | ||||
| @ -24,4 +26,16 @@ export const emptyCompany: Company = { | ||||
|     nip: "", | ||||
| } | ||||
| 
 | ||||
| export const emptyAddress: Address = { | ||||
|     street: "", | ||||
|     city: "", | ||||
|     country: "", | ||||
|     postalCode: "", | ||||
|     building: "" | ||||
| } | ||||
| 
 | ||||
| export const emptyBranchOffice: BranchOffice = { | ||||
|     address: emptyAddress, | ||||
| } | ||||
| 
 | ||||
| export const formatAddress = ({ postalCode, building, street, city, country }: Address): string => `${street} ${building}, ${postalCode} ${city}, ${country}` | ||||
|  | ||||
| @ -1,13 +1,15 @@ | ||||
| import React, { HTMLProps, useState } from "react"; | ||||
| import { useFormik } from "formik"; | ||||
| import { Nullable } from "../helpers"; | ||||
| import { formatAddress, Company, emptyCompany, BranchOffice } from "../data/company"; | ||||
| import React, { HTMLProps, useEffect, useMemo, useState } from "react"; | ||||
| import { BranchOffice, Company, emptyAddress, emptyBranchOffice, emptyCompany, formatAddress } from "../data/company"; | ||||
| import { sampleCompanies } from "../provider/company"; | ||||
| import { Autocomplete } from "@material-ui/lab"; | ||||
| import { Box, Grid, TextField, Typography } from "@material-ui/core"; | ||||
| import { Grid, TextField, Typography } from "@material-ui/core"; | ||||
| import { formFieldProps } from "./helpers"; | ||||
| 
 | ||||
| export type CompanyFormProps = { | ||||
| export type CompanyFormProps = {} | ||||
| 
 | ||||
| export type BranchOfficeProps = { | ||||
|     company: Company, | ||||
|     disabled?: boolean | ||||
| } | ||||
| 
 | ||||
| export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLProps<any>) => ( | ||||
| @ -19,51 +21,119 @@ export const CompanyItem = ({ company, ...props }: { company: Company } & HTMLPr | ||||
| 
 | ||||
| export const OfficeItem = ({ office, ...props }: { office: BranchOffice } & HTMLProps<any>) => ( | ||||
|     <div className="office-item" { ...props }> | ||||
|         <div>{ office.address.city }, { office.address.street }</div> | ||||
|         <div>{ office.address.city }</div> | ||||
|         <Typography variant="caption">{ formatAddress(office.address) }</Typography> | ||||
|     </div> | ||||
| ) | ||||
| 
 | ||||
| export const CompanyForm: React.FunctionComponent<CompanyFormProps> = props => { | ||||
|     const formik = useFormik<Nullable<Company>>({ onSubmit: values => console.log(values), initialValues: emptyCompany }); | ||||
| export const BranchForm: React.FC<BranchOfficeProps> = ({ company, disabled = false }) => { | ||||
|     const [office, setOffice] = useState<BranchOffice>(emptyBranchOffice) | ||||
| 
 | ||||
|     const formikProps = (prop: keyof Company) => ({ onChange: formik.handleChange, value: formik.values[prop], name: prop }) | ||||
|     const canEdit = useMemo(() => !office.id && !disabled, [office.id, disabled]); | ||||
| 
 | ||||
|     const [company, setCompany] = useState<Company | null>(null); | ||||
|     const fieldProps = formFieldProps(office.address, address => setOffice({ ...office, address })) | ||||
| 
 | ||||
|     const handleCityChange = (event: any, value: BranchOffice | string | null) => { | ||||
|         if (typeof value === "string") { | ||||
|             setOffice({ | ||||
|                 ...emptyBranchOffice, | ||||
|                 address: { | ||||
|                     ...emptyAddress, | ||||
|                     city: value, | ||||
|                 } | ||||
|             }); | ||||
|         } else if (typeof value === "object" && value !== null) { | ||||
|             setOffice(value); | ||||
|         } else { | ||||
|             setOffice(emptyBranchOffice); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     const handleCityInput = (event: any, value: string) => { | ||||
|         const base = office.id ? emptyBranchOffice : office; | ||||
|         setOffice({ | ||||
|             ...base, | ||||
|             address: { | ||||
|                 ...base.address, | ||||
|                 city: value, | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     useEffect(() => void (office.id && setOffice(emptyBranchOffice)), [company]) | ||||
| 
 | ||||
|     return ( | ||||
|         <Grid container spacing={3}> | ||||
|             <Grid item xs={12}> | ||||
|                 <Autocomplete options={ sampleCompanies } | ||||
|                               getOptionLabel={ option => option.name } | ||||
|                               renderOption={ company => <CompanyItem company={company} /> } | ||||
|                               renderInput={ props => <TextField {...props} label={"Nazwa firmy"} fullWidth />} | ||||
|                               onChange={ (ev, value: Company | null) => { | ||||
|                                   setCompany(value) | ||||
| 
 | ||||
|                                   formik.setFieldValue("name", value?.name || "") | ||||
|                                   formik.setFieldValue("url", value?.url || ""); | ||||
|                                   formik.setFieldValue("nip", value?.nip || ""); | ||||
|                               } } | ||||
|                 /> | ||||
|             </Grid> | ||||
|             <Grid container item spacing={2} xs={12}> | ||||
|                 <Grid item xs={4}> | ||||
|                     <TextField label={"NIP"} fullWidth {...formikProps("nip")} disabled={ company != null}/> | ||||
|                 </Grid> | ||||
|                 <Grid item xs={8}> | ||||
|                     <TextField label={"Url"} fullWidth {...formikProps("url")} disabled={ company != null }/> | ||||
|                 </Grid> | ||||
|             </Grid> | ||||
|             <Grid item xs={12}> | ||||
|         <Grid container> | ||||
|             <Grid item md={ 7 }> | ||||
|                 <Autocomplete options={ company?.offices || [] } | ||||
|                               disabled={ company == null} | ||||
|                               getOptionLabel={ office => office.address.city } | ||||
|                               disabled={ disabled } | ||||
|                               getOptionLabel={ office => typeof office == "string" ? office : office.address.city } | ||||
|                               renderOption={ office => <OfficeItem office={ office }/> } | ||||
|                               renderInput={ props => <TextField {...props} label={"Oddział"} fullWidth />} | ||||
|                               renderInput={ props => <TextField { ...props } label={ "Miasto" } fullWidth/> } | ||||
|                               onChange={ handleCityChange } | ||||
|                               onInputChange={ handleCityInput } | ||||
|                               inputValue={ office.address.city } | ||||
|                               value={ office.id ? office : null } | ||||
|                               freeSolo | ||||
|                 /> | ||||
|             </Grid> | ||||
|             <Grid item md={ 2 }> | ||||
|                 <TextField label={ "Kod pocztowy" } fullWidth disabled={ !canEdit } { ...fieldProps("postalCode") }/> | ||||
|             </Grid> | ||||
|             <Grid item md={ 3 }> | ||||
|                 <TextField label={ "Kraj" } fullWidth disabled={ !canEdit } { ...fieldProps("country") }/> | ||||
|             </Grid> | ||||
|             <Grid item md={ 10 }> | ||||
|                 <TextField label={ "Ulica" } fullWidth disabled={ !canEdit } { ...fieldProps("street") }/> | ||||
|             </Grid> | ||||
|             <Grid item md={ 2 }> | ||||
|                 <TextField label={ "Nr Budynku" } fullWidth disabled={ !canEdit } { ...fieldProps("building") }/> | ||||
|             </Grid> | ||||
|         </Grid> | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
| export const CompanyForm: React.FunctionComponent<CompanyFormProps> = props => { | ||||
|     const [company, setCompany] = useState<Company>(emptyCompany); | ||||
|     const canEdit = useMemo(() => !company.id, [company.id]); | ||||
| 
 | ||||
|     const fieldProps = formFieldProps(company, setCompany) | ||||
| 
 | ||||
|     const handleCompanyChange = (event: any, value: Company | string | null) => { | ||||
|         if (typeof value === "string") { | ||||
|             setCompany({ | ||||
|                 ...emptyCompany, | ||||
|                 name: value, | ||||
|             }); | ||||
|         } else if (typeof value === "object" && value !== null) { | ||||
|             setCompany(value); | ||||
|         } else { | ||||
|             setCompany(emptyCompany); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <Grid container style={ { marginBottom: 0, marginTop: 0 } }> | ||||
|                 <Grid item> | ||||
|                     <Autocomplete options={ sampleCompanies } | ||||
|                                   getOptionLabel={ option => option.name } | ||||
|                                   renderOption={ company => <CompanyItem company={ company }/> } | ||||
|                                   renderInput={ props => <TextField { ...props } label={ "Nazwa firmy" } fullWidth/> } | ||||
|                                   onChange={ handleCompanyChange } | ||||
|                                   freeSolo | ||||
|                     /> | ||||
|                 </Grid> | ||||
|                 <Grid item md={ 4 }> | ||||
|                     <TextField label={ "NIP" } fullWidth { ...fieldProps("nip") } disabled={ !canEdit }/> | ||||
|                 </Grid> | ||||
|                 <Grid item md={ 8 }> | ||||
|                     <TextField label={ "Url" } fullWidth { ...fieldProps("url") } disabled={ !canEdit }/> | ||||
|                 </Grid> | ||||
|             </Grid> | ||||
|             <Typography variant="subtitle1">Oddział</Typography> | ||||
|             <BranchForm company={ company }/> | ||||
|         </> | ||||
|     ) | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										20
									
								
								src/forms/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/forms/helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| import { DOMEvent } from "../helpers"; | ||||
| 
 | ||||
| type UpdatingEvent = "onBlur" | "onChange" | "onInput"; | ||||
| type FormFieldHelperOptions<T> = { | ||||
|     event: UpdatingEvent | ||||
| } | ||||
| 
 | ||||
| export function formFieldProps<T>(subject: T, update: (value: T) => void, options: Partial<FormFieldHelperOptions<T>> = {}) { | ||||
|     const { | ||||
|         event = "onChange" | ||||
|     } = options; | ||||
| 
 | ||||
|     return (field: keyof T) => ({ | ||||
|         value: subject[field], | ||||
|         [event]: (event: DOMEvent<HTMLInputElement>) => update({ | ||||
|             ...subject, | ||||
|             [field]: event.target.value, | ||||
|         } as T) | ||||
|     }) | ||||
| } | ||||
| @ -1 +1,5 @@ | ||||
| export type Nullable<T> = { [P in keyof T]: T[P] | null } | ||||
| 
 | ||||
| export interface DOMEvent<TTarget extends EventTarget> extends Event { | ||||
|     target: TTarget; | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,17 @@ | ||||
| import { Company } from "../data/company"; | ||||
| import { makeIdSequence } from "./helpers"; | ||||
| 
 | ||||
| const companySequence = makeIdSequence(); | ||||
| const officeSequence = makeIdSequence(); | ||||
| 
 | ||||
| export const sampleCompanies: Company[] = [ | ||||
|     { | ||||
|         id: companySequence(), | ||||
|         name: "Intel", | ||||
|         url: "https://www.intel.com/content/www/us/en/jobs/locations/poland.html", | ||||
|         nip: "9570752316", | ||||
|         offices: [{ | ||||
|             id: officeSequence(), | ||||
|             address: { | ||||
|                 city: "Gdańsk", | ||||
|                 street: "ul. Słowackiego", | ||||
| @ -16,10 +22,12 @@ export const sampleCompanies: Company[] = [ | ||||
|         }] | ||||
|     }, | ||||
|     { | ||||
|         id: companySequence(), | ||||
|         name: "IHS Markit", | ||||
|         url: "http://ihsgdansk.com/", | ||||
|         nip: "5842068320", | ||||
|         offices: [{ | ||||
|             id: officeSequence(), | ||||
|             address: { | ||||
|                 city: "Gdańsk", | ||||
|                 street: "ul. Marynarki Polskiej", | ||||
| @ -30,10 +38,12 @@ export const sampleCompanies: Company[] = [ | ||||
|         }] | ||||
|     }, | ||||
|     { | ||||
|         id: companySequence(), | ||||
|         name: "Asseco Poland", | ||||
|         url: "http://pl.asseco.com/", | ||||
|         nip: "5842068320", | ||||
|         offices: [{ | ||||
|             id: officeSequence(), | ||||
|             address: { | ||||
|                 city: "Gdynia", | ||||
|                 street: "ul. Podolska", | ||||
| @ -42,6 +52,7 @@ export const sampleCompanies: Company[] = [ | ||||
|                 country: "Poland" | ||||
|             } | ||||
|         }, { | ||||
|             id: officeSequence(), | ||||
|             address: { | ||||
|                 city: "Łódź", | ||||
|                 street: "al. Marszałka Józefa Piłsudskiego", | ||||
| @ -50,6 +61,7 @@ export const sampleCompanies: Company[] = [ | ||||
|                 country: "Poland" | ||||
|             } | ||||
|         }, { | ||||
|             id: officeSequence(), | ||||
|             address: { | ||||
|                 city: "Wrocław", | ||||
|                 street: "Traugutta", | ||||
|  | ||||
							
								
								
									
										4
									
								
								src/provider/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/provider/helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| export const makeIdSequence = (start: number = 1) => { | ||||
|     let i = start; | ||||
|     return () => i++; | ||||
| } | ||||
							
								
								
									
										10
									
								
								src/ui/theme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/ui/theme.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import { createMuiTheme } from "@material-ui/core"; | ||||
| 
 | ||||
| export const studentTheme = createMuiTheme({ | ||||
|     props: { | ||||
|         MuiGrid: { | ||||
|             spacing: 4, | ||||
|             xs: 12, | ||||
|         } | ||||
|     } | ||||
| }) | ||||
							
								
								
									
										39
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -3672,11 +3672,6 @@ deep-is@~0.1.3: | ||||
|   resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" | ||||
|   integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= | ||||
| 
 | ||||
| deepmerge@^2.1.1: | ||||
|   version "2.2.1" | ||||
|   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170" | ||||
|   integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA== | ||||
| 
 | ||||
| default-gateway@^4.2.0: | ||||
|   version "4.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" | ||||
| @ -4751,20 +4746,6 @@ form-data@~2.3.2: | ||||
|     combined-stream "^1.0.6" | ||||
|     mime-types "^2.1.12" | ||||
| 
 | ||||
| formik@^2.1.4: | ||||
|   version "2.1.4" | ||||
|   resolved "https://registry.yarnpkg.com/formik/-/formik-2.1.4.tgz#8deef07ec845ea98f75e03da4aad7aab4ac46570" | ||||
|   integrity sha512-oKz8S+yQBzuQVSEoxkqqJrKQS5XJASWGVn6mrs+oTWrBoHgByVwwI1qHiVc9GKDpZBU9vAxXYAKz2BvujlwunA== | ||||
|   dependencies: | ||||
|     deepmerge "^2.1.1" | ||||
|     hoist-non-react-statics "^3.3.0" | ||||
|     lodash "^4.17.14" | ||||
|     lodash-es "^4.17.14" | ||||
|     react-fast-compare "^2.0.1" | ||||
|     scheduler "^0.18.0" | ||||
|     tiny-warning "^1.0.2" | ||||
|     tslib "^1.10.0" | ||||
| 
 | ||||
| forwarded@~0.1.2: | ||||
|   version "0.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" | ||||
| @ -5121,7 +5102,7 @@ hmac-drbg@^1.0.0: | ||||
|     minimalistic-assert "^1.0.0" | ||||
|     minimalistic-crypto-utils "^1.0.1" | ||||
| 
 | ||||
| hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: | ||||
| hoist-non-react-statics@^3.3.2: | ||||
|   version "3.3.2" | ||||
|   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" | ||||
|   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== | ||||
| @ -6694,11 +6675,6 @@ locate-path@^5.0.0: | ||||
|   dependencies: | ||||
|     p-locate "^4.1.0" | ||||
| 
 | ||||
| lodash-es@^4.17.14: | ||||
|   version "4.17.15" | ||||
|   resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" | ||||
|   integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== | ||||
| 
 | ||||
| lodash._reinterpolate@^3.0.0: | ||||
|   version "3.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" | ||||
| @ -8832,11 +8808,6 @@ react-error-overlay@^6.0.7: | ||||
|   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108" | ||||
|   integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA== | ||||
| 
 | ||||
| react-fast-compare@^2.0.1: | ||||
|   version "2.0.4" | ||||
|   resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" | ||||
|   integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== | ||||
| 
 | ||||
| react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4: | ||||
|   version "16.13.1" | ||||
|   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" | ||||
| @ -9404,14 +9375,6 @@ saxes@^3.1.9: | ||||
|   dependencies: | ||||
|     xmlchars "^2.1.1" | ||||
| 
 | ||||
| scheduler@^0.18.0: | ||||
|   version "0.18.0" | ||||
|   resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4" | ||||
|   integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ== | ||||
|   dependencies: | ||||
|     loose-envify "^1.1.0" | ||||
|     object-assign "^4.1.1" | ||||
| 
 | ||||
| scheduler@^0.19.1: | ||||
|   version "0.19.1" | ||||
|   resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user