Skip to content

Commit 51ec134

Browse files
committed
Add edit functionality
1 parent 019db02 commit 51ec134

23 files changed

Lines changed: 976 additions & 125 deletions

app/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6+
"bootstrap": "^4.1.2",
67
"react": "^16.4.1",
8+
"react-cookie": "^2.2.0",
79
"react-dom": "^16.4.1",
8-
"react-scripts": "1.1.4"
10+
"react-router-dom": "^4.3.1",
11+
"react-scripts": "1.1.4",
12+
"reactstrap": "^6.3.0"
913
},
1014
"scripts": {
1115
"start": "react-scripts start",
@@ -14,4 +18,4 @@
1418
"eject": "react-scripts eject"
1519
},
1620
"proxy": "http://localhost:8080"
17-
}
21+
}

app/src/App.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,7 @@
2626
from { transform: rotate(0deg); }
2727
to { transform: rotate(360deg); }
2828
}
29+
30+
.container, .container-fluid {
31+
padding-top: 20px;
32+
}

app/src/App.js

Lines changed: 14 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,22 @@
11
import React, { Component } from 'react';
2-
import logo from './logo.svg';
32
import './App.css';
3+
import Home from './Home';
4+
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
5+
import GroupList from './GroupList';
6+
import GroupEdit from './GroupEdit';
7+
import { CookiesProvider } from 'react-cookie';
48

59
class App extends Component {
6-
state = {
7-
isLoading: true,
8-
meetups: []
9-
};
10-
11-
componentDidMount() {
12-
this.callApi()
13-
.then(response => {
14-
this.setState({ meetups: response, isLoading: false })
15-
})
16-
.catch(error => console.log(error));
17-
}
18-
19-
callApi = async () => {
20-
const response = await fetch('/api/meetups');
21-
const body = await response.json();
22-
if (response.status !== 200) {
23-
throw Error(body.message);
24-
}
25-
return body;
26-
};
27-
2810
render() {
29-
const {meetups, isLoading} = this.state;
30-
31-
if (isLoading) {
32-
return <p>Loading...</p>;
33-
}
34-
35-
return (
36-
<div className="App">
37-
<header className="App-header">
38-
<img src={logo} className="App-logo" alt="logo" />
39-
<h1 className="App-title">Welcome to React</h1>
40-
</header>
41-
<div className="App-intro">
42-
<h2>Meetup List</h2>
43-
{meetups.map(meetup =>
44-
<div key={meetup.id}>
45-
{meetup.name}
46-
</div>
47-
)}
48-
</div>
49-
</div>
50-
);
11+
return <CookiesProvider>
12+
<Router>
13+
<Switch>
14+
<Route path='/' exact={true} component={Home}/>
15+
<Route path='/groups' exact={true} component={GroupList}/>
16+
<Route path='/groups/:id' component={GroupEdit}/>
17+
</Switch>
18+
</Router>
19+
</CookiesProvider>
5120
}
5221
}
5322

app/src/AppNavbar.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React, { Component } from 'react';
2+
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
3+
import { Link } from 'react-router-dom';
4+
5+
export default class AppNavbar extends Component {
6+
constructor(props) {
7+
super(props);
8+
this.state = {isOpen: false};
9+
this.toggle = this.toggle.bind(this);
10+
}
11+
12+
toggle() {
13+
this.setState({
14+
isOpen: !this.state.isOpen
15+
});
16+
}
17+
18+
render() {
19+
return <Navbar color="dark" dark expand="md">
20+
<NavbarBrand tag={Link} to="/">Home</NavbarBrand>
21+
<NavbarToggler onClick={this.toggle}/>
22+
<Collapse isOpen={this.state.isOpen} navbar>
23+
<Nav className="ml-auto" navbar>
24+
<NavItem>
25+
<NavLink
26+
href="https://twitter.com/oktadev">@oktadev</NavLink>
27+
</NavItem>
28+
<NavItem>
29+
<NavLink href="https://github.com/oktadeveloper/okta-spring-boot-react-crud-example">GitHub</NavLink>
30+
</NavItem>
31+
</Nav>
32+
</Collapse>
33+
</Navbar>;
34+
}
35+
}

app/src/GroupEdit.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, { Component } from 'react';
2+
import { Link, withRouter } from 'react-router-dom';
3+
import { Button, Container, Form, FormGroup, Input, Label } from 'reactstrap';
4+
import AppNavbar from './AppNavbar';
5+
import { instanceOf } from 'prop-types';
6+
import { Cookies, withCookies } from 'react-cookie';
7+
8+
class GroupEdit extends Component {
9+
static propTypes = {
10+
cookies: instanceOf(Cookies).isRequired
11+
};
12+
13+
emptyItem = {
14+
name: '',
15+
address: '',
16+
city: '',
17+
stateOrProvince: '',
18+
country: '',
19+
postalCode: ''
20+
};
21+
22+
23+
constructor(props) {
24+
super(props);
25+
const {cookies} = props;
26+
this.state = {
27+
item: this.emptyItem,
28+
csrfToken: cookies.get('XSRF-TOKEN')
29+
};
30+
this.handleChange = this.handleChange.bind(this);
31+
this.handleSubmit = this.handleSubmit.bind(this);
32+
}
33+
34+
async componentDidMount() {
35+
if (this.props.match.params.id !== 'new') {
36+
try {
37+
const group = await (await fetch(`/api/group/${this.props.match.params.id}`, {credentials: 'include'})).json();
38+
this.setState({item: group});
39+
} catch (error) {
40+
this.props.history.push('/');
41+
}
42+
}
43+
}
44+
45+
handleChange(event) {
46+
const target = event.target;
47+
const value = target.value;
48+
const name = target.name;
49+
let item = {...this.state.item};
50+
item[name] = value;
51+
this.setState({item});
52+
}
53+
54+
async handleSubmit(event) {
55+
event.preventDefault();
56+
const {item, csrfToken} = this.state;
57+
58+
await fetch('/api/group', {
59+
method: (item.id) ? 'PUT' : 'POST',
60+
headers: {
61+
'X-XSRF-TOKEN': csrfToken,
62+
'Accept': 'application/json',
63+
'Content-Type': 'application/json'
64+
},
65+
body: JSON.stringify(item),
66+
credentials: 'include'
67+
});
68+
this.props.history.push('/groups');
69+
}
70+
71+
render() {
72+
const {item} = this.state;
73+
const title = <h2>{item.id ? 'Edit Group' : 'Add Group'}</h2>;
74+
75+
return <div>
76+
<AppNavbar/>
77+
<Container>
78+
{title}
79+
<Form onSubmit={this.handleSubmit}>
80+
<FormGroup>
81+
<Label for="name">Name</Label>
82+
<Input type="text" name="name" id="name" value={item.name || ''}
83+
onChange={this.handleChange} autoComplete="name"/>
84+
</FormGroup>
85+
<FormGroup>
86+
<Label for="address">Address</Label>
87+
<Input type="text" name="address" id="address" value={item.address || ''}
88+
onChange={this.handleChange} autoComplete="address-level1"/>
89+
</FormGroup>
90+
<FormGroup>
91+
<Label for="city">City</Label>
92+
<Input type="text" name="city" id="city" value={item.city || ''}
93+
onChange={this.handleChange} autoComplete="address-level1"/>
94+
</FormGroup>
95+
<div className="row">
96+
<FormGroup className="col-md-4 mb-3">
97+
<Label for="stateOrProvince">State/Province</Label>
98+
<Input type="text" name="stateOrProvince" id="stateOrProvince" value={item.stateOrProvince || ''}
99+
onChange={this.handleChange} autoComplete="address-level1"/>
100+
</FormGroup>
101+
<FormGroup className="col-md-5 mb-3">
102+
<Label for="country">Country</Label>
103+
<Input type="text" name="country" id="country" value={item.country || ''}
104+
onChange={this.handleChange} autoComplete="address-level1"/>
105+
</FormGroup>
106+
<FormGroup className="col-md-3 mb-3">
107+
<Label for="country">Postal Code</Label>
108+
<Input type="text" name="postalCode" id="postalCode" value={item.postalCode || ''}
109+
onChange={this.handleChange} autoComplete="address-level1"/>
110+
</FormGroup>
111+
</div>
112+
<FormGroup>
113+
<Button color="primary" type="submit">Save</Button>{' '}
114+
<Button color="secondary" tag={Link} to="/groups">Cancel</Button>
115+
</FormGroup>
116+
</Form>
117+
</Container>
118+
</div>
119+
}
120+
}
121+
122+
export default withCookies(withRouter(GroupEdit));

app/src/GroupList.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React, { Component } from 'react';
2+
import { Button, ButtonGroup, Container, Table } from 'reactstrap';
3+
import AppNavbar from './AppNavbar';
4+
import { Link, withRouter } from 'react-router-dom';
5+
import { instanceOf } from 'prop-types';
6+
import { withCookies, Cookies } from 'react-cookie';
7+
8+
class GroupList extends Component {
9+
static propTypes = {
10+
cookies: instanceOf(Cookies).isRequired
11+
};
12+
13+
constructor(props) {
14+
super(props);
15+
const {cookies} = props;
16+
this.state = {groups: [], csrfToken: cookies.get('XSRF-TOKEN'), isLoading: true};
17+
this.remove = this.remove.bind(this);
18+
}
19+
20+
componentDidMount() {
21+
this.setState({isLoading: true});
22+
23+
fetch('api/groups', {credentials: 'include'})
24+
.then(response => response.json())
25+
.then(data => this.setState({groups: data, isLoading: false}))
26+
.catch(() => this.props.history.push('/'))
27+
}
28+
29+
async remove(id) {
30+
await fetch(`/api/group/${id}`, {
31+
method: 'DELETE',
32+
headers: {
33+
'X-XSRF-TOKEN': this.state.csrfToken,
34+
'Accept': 'application/json',
35+
'Content-Type': 'application/json'
36+
},
37+
credentials: 'include'
38+
}).then(() => {
39+
let updatedGroups = [...this.state.groups].filter(i => i.id !== id);
40+
this.setState({groups: updatedGroups});
41+
});
42+
}
43+
44+
render() {
45+
const {groups, isLoading} = this.state;
46+
47+
if (isLoading) {
48+
return <p>Loading...</p>;
49+
}
50+
51+
const groupList = groups.map(group => {
52+
const address = `${group.address || ''} ${group.city || ''} ${group.stateOrProvince || ''}`;
53+
return <tr key={group.id}>
54+
<td style={{whiteSpace: 'nowrap'}}>{group.name}</td>
55+
<td>{address}</td>
56+
<td>{group.events.map(event => {
57+
return <div key={event.id}>{new Intl.DateTimeFormat('en-US', {
58+
year: 'numeric',
59+
month: 'long',
60+
day: '2-digit'
61+
}).format(new Date(event.date))}: {event.title}</div>
62+
})}</td>
63+
<td>
64+
<ButtonGroup>
65+
<Button size="sm" color="primary" tag={Link} to={"/groups/" + group.id}>Edit</Button>
66+
<Button size="sm" color="danger" onClick={() => this.remove(group.id)}>Delete</Button>
67+
</ButtonGroup>
68+
</td>
69+
</tr>
70+
});
71+
72+
return (
73+
<div>
74+
<AppNavbar/>
75+
<Container fluid>
76+
<div className="float-right">
77+
<Button color="success" tag={Link} to="/groups/new">Add Group</Button>
78+
</div>
79+
<h3>Java User Groups</h3>
80+
<Table className="mt-4">
81+
<thead>
82+
<tr>
83+
<th width="20%">Name</th>
84+
<th width="20%">Location</th>
85+
<th>Events</th>
86+
<th width="10%">Actions</th>
87+
</tr>
88+
</thead>
89+
<tbody>
90+
{groupList}
91+
</tbody>
92+
</Table>
93+
</Container>
94+
</div>
95+
);
96+
}
97+
}
98+
99+
export default withCookies(withRouter(GroupList));

0 commit comments

Comments
 (0)