summaryrefslogtreecommitdiff
path: root/app/src/static/js/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/static/js/app.js')
-rw-r--r--app/src/static/js/app.js179
1 files changed, 179 insertions, 0 deletions
diff --git a/app/src/static/js/app.js b/app/src/static/js/app.js
new file mode 100644
index 0000000..4ec962c
--- /dev/null
+++ b/app/src/static/js/app.js
@@ -0,0 +1,179 @@
+function App() {
+ const { Container, Row, Col } = ReactBootstrap;
+ return (
+ <Container>
+ <Row>
+ <Col md={{ offset: 3, span: 6 }}>
+ <TodoListCard />
+ </Col>
+ </Row>
+ </Container>
+ );
+}
+
+function TodoListCard() {
+ const [items, setItems] = React.useState(null);
+
+ React.useEffect(() => {
+ fetch('/items')
+ .then(r => r.json())
+ .then(setItems);
+ }, []);
+
+ const onNewItem = React.useCallback(
+ newItem => {
+ setItems([...items, newItem]);
+ },
+ [items],
+ );
+
+ const onItemUpdate = React.useCallback(
+ item => {
+ const index = items.findIndex(i => i.id === item.id);
+ setItems([
+ ...items.slice(0, index),
+ item,
+ ...items.slice(index + 1),
+ ]);
+ },
+ [items],
+ );
+
+ const onItemRemoval = React.useCallback(
+ item => {
+ const index = items.findIndex(i => i.id === item.id);
+ setItems([...items.slice(0, index), ...items.slice(index + 1)]);
+ },
+ [items],
+ );
+
+ if (items === null) return 'Loading...';
+
+ return (
+ <React.Fragment>
+ <AddItemForm onNewItem={onNewItem} />
+ {items.length === 0 && (
+ <p className="text-center">No items yet! Add one above!</p>
+ )}
+ {items.map(item => (
+ <ItemDisplay
+ item={item}
+ key={item.id}
+ onItemUpdate={onItemUpdate}
+ onItemRemoval={onItemRemoval}
+ />
+ ))}
+ </React.Fragment>
+ );
+}
+
+function AddItemForm({ onNewItem }) {
+ const { Form, InputGroup, Button } = ReactBootstrap;
+
+ const [newItem, setNewItem] = React.useState('');
+ const [submitting, setSubmitting] = React.useState(false);
+
+ const submitNewItem = e => {
+ e.preventDefault();
+ setSubmitting(true);
+ fetch('/items', {
+ method: 'POST',
+ body: JSON.stringify({ name: newItem }),
+ headers: { 'Content-Type': 'application/json' },
+ })
+ .then(r => r.json())
+ .then(item => {
+ onNewItem(item);
+ setSubmitting(false);
+ setNewItem('');
+ });
+ };
+
+ return (
+ <Form onSubmit={submitNewItem}>
+ <InputGroup className="mb-3">
+ <Form.Control
+ value={newItem}
+ onChange={e => setNewItem(e.target.value)}
+ type="text"
+ placeholder="New Item"
+ aria-describedby="basic-addon1"
+ />
+ <InputGroup.Append>
+ <Button
+ type="submit"
+ variant="success"
+ disabled={!newItem.length}
+ className={submitting ? 'disabled' : ''}
+ >
+ {submitting ? 'Adding...' : 'Add Item'}
+ </Button>
+ </InputGroup.Append>
+ </InputGroup>
+ </Form>
+ );
+}
+
+function ItemDisplay({ item, onItemUpdate, onItemRemoval }) {
+ const { Container, Row, Col, Button } = ReactBootstrap;
+
+ const toggleCompletion = () => {
+ fetch(`/items/${item.id}`, {
+ method: 'PUT',
+ body: JSON.stringify({
+ name: item.name,
+ completed: !item.completed,
+ }),
+ headers: { 'Content-Type': 'application/json' },
+ })
+ .then(r => r.json())
+ .then(onItemUpdate);
+ };
+
+ const removeItem = () => {
+ fetch(`/items/${item.id}`, { method: 'DELETE' }).then(() =>
+ onItemRemoval(item),
+ );
+ };
+
+ return (
+ <Container fluid className={`item ${item.completed && 'completed'}`}>
+ <Row>
+ <Col xs={1} className="text-center">
+ <Button
+ className="toggles"
+ size="sm"
+ variant="link"
+ onClick={toggleCompletion}
+ aria-label={
+ item.completed
+ ? 'Mark item as incomplete'
+ : 'Mark item as complete'
+ }
+ >
+ <i
+ className={`far ${
+ item.completed ? 'fa-check-square' : 'fa-square'
+ }`}
+ />
+ </Button>
+ </Col>
+ <Col xs={10} className="name">
+ {item.name}
+ </Col>
+ <Col xs={1} className="text-center remove">
+ <Button
+ size="sm"
+ variant="link"
+ onClick={removeItem}
+ aria-label="Remove Item"
+ >
+ <i className="fa fa-trash text-danger" />
+ </Button>
+ </Col>
+ </Row>
+ </Container>
+ );
+}
+
+ReactDOM.render(<App />, document.getElementById('root'));