shopping list

This commit is contained in:
cupcakearmy 2019-05-23 16:55:02 +02:00
parent 63d4b4a0fe
commit c50a37b08a
10 changed files with 207 additions and 4 deletions

24
api/src/entities/item.ts Normal file
View File

@ -0,0 +1,24 @@
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'
import UUID from 'uuid/v4'
@Entity()
export default class Item extends BaseEntity {
@PrimaryColumn()
id!: string
@Column()
text: string
@Column()
done: boolean
constructor(text: string, done: boolean = false) {
super()
this.id = UUID()
this.text = text
this.done = done
}
}

View File

@ -1,13 +1,15 @@
import Router from 'koa-router'
import item from './item'
import purchase from './purchase'
import user from './user'
const r = new Router({
prefix: '/api'
prefix: '/api',
})
r.use(user.routes(), user.allowedMethods())
r.use(purchase.routes(), purchase.allowedMethods())
r.use(item.routes(), purchase.allowedMethods())
export default r

60
api/src/routes/item.ts Normal file
View File

@ -0,0 +1,60 @@
import Router from 'koa-router'
import Item from '../entities/item'
import { withAuth } from '../lib/auth'
import { Success } from '../lib/responses'
const r = new Router({
prefix: '/items',
})
r.get('/', withAuth(async ctx => {
return Success(ctx, await Item.find())
}))
r.post('/', withAuth(async ctx => {
const { text, done } = ctx.request.body
return Success(ctx, await new Item(String(text), Boolean(done)).save())
}))
r.delete('/', withAuth(async ctx => {
return Success(ctx, await Item.clear())
}))
r.get('/:id', withAuth(async ctx => {
const { id } = ctx.params
const item = await Item.findOne(id)
// 404
if (!item) return
return Success(ctx, item)
}))
r.delete('/:id', withAuth(async ctx => {
const { id } = ctx.params
const item = await Item.findOne(id)
// 404
if (!item) return
await item.remove()
return Success(ctx)
}))
r.patch('/:id', withAuth(async ctx => {
const { id } = ctx.params
const item = await Item.findOne(id)
// 404
if (!item) return
const { text, done } = ctx.request.body
if (text !== undefined) item.text = String(text)
if (done !== undefined) item.done = Boolean(done)
return Success(ctx, await item.save())
}))
export default r

View File

@ -5,6 +5,7 @@ import Parser from 'koa-bodyparser'
import { join } from 'path'
import { createConnection } from 'typeorm'
import Item from './entities/item'
import Purchase from './entities/purchase'
import User from './entities/user'
import { Config } from './lib/config'
@ -15,7 +16,7 @@ import router from './routes'
createConnection({
type: 'sqlite',
database: join(process.cwd(), 'db.sqlite'),
entities: [User, Purchase],
entities: [User, Purchase, Item],
synchronize: true,
}).then(async () => {

114
www/pages/list.jsx Normal file
View File

@ -0,0 +1,114 @@
import React, { useEffect, useRef, useState } from 'react'
import { withAuthSync } from '../utils/auth'
import Layout from '../components/layout'
import { callAPI } from '../utils/api'
const Item = ({ id, text, done }) => {
const handleDone = async (e) => {
await callAPI(null, {
url: `/api/items/${id}`,
method: 'patch',
data: {
done: e.target.checked,
},
})
window.location.reload()
}
const handleDelete = async (e) => {
await callAPI(null, {
url: `/api/items/${id}`,
method: 'delete',
})
window.location.reload()
}
return <tr className="item">
<td className="item-done">
<div className="form-group">
<label className="form-checkbox">
<input type="checkbox" checked={done} onChange={handleDone}/>
<i className="form-icon"/>
</label>
</div>
</td>
<td>{text}</td>
<td className="item-menu">
<div className="dropdown dropdown-right">
<a className="btn btn-link dropdown-toggle" tabIndex="0">
<i className="icon icon-more-vert"/>
</a>
<ul className="menu">
<li className="menu-item">
<a onClick={handleDelete}>
<i className="icon icon-delete"/> Delete
</a>
</li>
</ul>
</div>
</td>
{/* language=CSS */}
<style jsx>{`
.item-done,
.item-menu {
width: 2em;
}
`}</style>
</tr>
}
const List = ({ items }) => {
const input = useRef(undefined)
const [text, setText] = useState('')
useEffect(() => {
if (!input.current) return
input.current.focus()
}, [input])
const submit = async () => {
await callAPI(null, {
url: `/api/items/`,
method: 'post',
data: { text },
})
window.location.reload()
}
const deleteAll = async () => {
await callAPI(null, {
url: `/api/items/`,
method: 'delete',
})
window.location.reload()
}
return <Layout>
<form onSubmit={submit}>
<div className="input-group">
<input
type="text" className="form-input" placeholder="..." ref={input}
value={text} onChange={e => setText(e.target.value)}/>
<button type="submit" className="btn btn-primary input-group-btn">Add</button>
</div>
</form>
<br/>
<table className="table table-hover">
<tbody>
{items.map((item, i) => <Item key={i} {...item} />)}
</tbody>
</table>
<br/>
<button onClick={deleteAll} className="btn btn-error">Delete All</button>
</Layout>
}
List.getInitialProps = async ctx => ({
items: await callAPI(ctx, { url: `/api/items` }),
})
export default withAuthSync(List)

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M346.5 240H272v-74.5c0-8.8-7.2-16-16-16s-16 7.2-16 16V240h-74.5c-8.8 0-16 6-16 16s7.5 16 16 16H240v74.5c0 9.5 7 16 16 16s16-7.2 16-16V272h74.5c8.8 0 16-7.2 16-16s-7.2-16-16-16z"/><path d="M256 76c48.1 0 93.3 18.7 127.3 52.7S436 207.9 436 256s-18.7 93.3-52.7 127.3S304.1 436 256 436c-48.1 0-93.3-18.7-127.3-52.7S76 304.1 76 256s18.7-93.3 52.7-127.3S207.9 76 256 76m0-28C141.1 48 48 141.1 48 256s93.1 208 208 208 208-93.1 208-208S370.9 48 256 48z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M368.5 240H272v-96.5c0-8.8-7.2-16-16-16s-16 7.2-16 16V240h-96.5c-8.8 0-16 7.2-16 16 0 4.4 1.8 8.4 4.7 11.3 2.9 2.9 6.9 4.7 11.3 4.7H240v96.5c0 4.4 1.8 8.4 4.7 11.3 2.9 2.9 6.9 4.7 11.3 4.7 8.8 0 16-7.2 16-16V272h96.5c8.8 0 16-7.2 16-16s-7.2-16-16-16z"/></svg>

Before

Width:  |  Height:  |  Size: 524 B

After

Width:  |  Height:  |  Size: 330 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M387.9 373.7h49.2l17.5-75.4h-66.7zM387.9 448h.5c18.7 0 33.4-12.5 38.3-29.5l6-25.9h-44.8V448zM265.4 392.5h103.7V448H265.4zM75 373.7h49v-75.4H57.5zM142.9 192h103.7v87.5H142.9zM265.4 192h103.7v87.5H265.4zM85.5 418.3c4.7 17 19.4 29.7 38.1 29.7h.5v-55.5H79.4l6.1 25.8zM142.9 392.5h103.7V448H142.9zM265.4 298.3h103.7v75.4H265.4zM142.9 298.3h103.7v75.4H142.9z"/><path d="M464 192h-47.9V96c0-17.6-14.4-32-32-32H127.9c-17.6 0-32 14.4-32 32v96H48c-10.3 0-17.9 9.6-15.6 19.6l19.7 67.9H124V106c0-7.7 6.3-14 14-14h236c7.7 0 14 6.3 14 14v173.5h72l19.6-67.9c2.3-10-5.3-19.6-15.6-19.6z"/></svg>

After

Width:  |  Height:  |  Size: 649 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M362.6 192.9L345 174.8c-.7-.8-1.8-1.2-2.8-1.2-1.1 0-2.1.4-2.8 1.2l-122 122.9-44.4-44.4c-.8-.8-1.8-1.2-2.8-1.2-1 0-2 .4-2.8 1.2l-17.8 17.8c-1.6 1.6-1.6 4.1 0 5.7l56 56c3.6 3.6 8 5.7 11.7 5.7 5.3 0 9.9-3.9 11.6-5.5h.1l133.7-134.4c1.4-1.7 1.4-4.2-.1-5.7z"/></svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M184 448h48c4.4 0 8-3.6 8-8V72c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v368c0 4.4 3.6 8 8 8zM88 448h48c4.4 0 8-3.6 8-8V296c0-4.4-3.6-8-8-8H88c-4.4 0-8 3.6-8 8v144c0 4.4 3.6 8 8 8zM280.1 448h47.8c4.5 0 8.1-3.6 8.1-8.1V232.1c0-4.5-3.6-8.1-8.1-8.1h-47.8c-4.5 0-8.1 3.6-8.1 8.1v207.8c0 4.5 3.6 8.1 8.1 8.1zM368 136.1v303.8c0 4.5 3.6 8.1 8.1 8.1h47.8c4.5 0 8.1-3.6 8.1-8.1V136.1c0-4.5-3.6-8.1-8.1-8.1h-47.8c-4.5 0-8.1 3.6-8.1 8.1z"/></svg>

Before

Width:  |  Height:  |  Size: 501 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M464.9 128H344.1c-8.3 0-15.1 6.6-15.1 14.8s6.8 14.8 15.1 14.8h83.7l-138 142.2-85.9-84.1c-2.9-2.8-6.6-4.3-10.7-4.3-4 0-7.8 1.5-10.7 4.3L36.2 358.8c-1.9 1.9-4.2 5.2-4.2 10.7 0 4.1 1.4 7.5 4.2 10.2 2.9 2.8 6.6 4.3 10.7 4.3 4 0 7.8-1.5 10.7-4.3L193.2 247l85.9 84.1c2.9 2.8 6.6 4.3 10.7 4.3 4 0 7.8-1.5 10.7-4.3l149.4-151.9v81.7c0 8.1 6.8 14.8 15.1 14.8s15.1-6.6 15.1-14.8V142.8c-.1-8.2-6.9-14.8-15.2-14.8z"/></svg>

After

Width:  |  Height:  |  Size: 481 B