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",
|
||||
|
19
src/App.tsx
19
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 (
|
||||
<div className="app">
|
||||
<Container>
|
||||
<CompanyForm />
|
||||
</Container>
|
||||
</div>
|
||||
<ThemeProvider theme={ studentTheme }>
|
||||
<div className="app">
|
||||
<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,69 +1,139 @@
|
||||
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>) => (
|
||||
<div className="company-item" {...props}>
|
||||
<div className="company-item" { ...props }>
|
||||
<div>{ company.name }</div>
|
||||
<Typography variant="caption">NIP: <strong>{ company.nip }</strong></Typography>
|
||||
</div>
|
||||
)
|
||||
|
||||
export const OfficeItem = ({ office, ...props }: { office: BranchOffice } & HTMLProps<any>) => (
|
||||
<div className="office-item" {...props}>
|
||||
<div>{ office.address.city }, { office.address.street }</div>
|
||||
<div className="office-item" { ...props }>
|
||||
<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 }
|
||||
renderOption={ office => <OfficeItem office={ office } /> }
|
||||
renderInput={ props => <TextField {...props} label={"Oddział"} fullWidth />}
|
||||
disabled={ disabled }
|
||||
getOptionLabel={ office => typeof office == "string" ? office : office.address.city }
|
||||
renderOption={ office => <OfficeItem office={ office }/> }
|
||||
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