mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-11-24 11:44:51 +03:00
Updated TODO example with new code.
This commit is contained in:
parent
f77f19a6bb
commit
0b9c72eec0
1
examples/todoApp/out/.waspinfo
Normal file
1
examples/todoApp/out/.waspinfo
Normal file
@ -0,0 +1 @@
|
||||
Generated on 2020-02-06 11:14:43.678730919 UTC by waspc version "0.1.0.0" .
|
25977
examples/todoApp/out/package-lock.json
generated
25977
examples/todoApp/out/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,15 +3,15 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.0.0-rc.0",
|
||||
"lodash": "^4.17.11",
|
||||
"react": "^16.8.5",
|
||||
"react-dom": "^16.8.5",
|
||||
"react-redux": "^7.0.3",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-scripts": "2.1.8",
|
||||
"redux": "^4.0.1",
|
||||
"redux-starter-kit": "^0.5.1",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@reduxjs/toolkit": "^1.2.3",
|
||||
"lodash": "^4.17.15",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "3.3.1",
|
||||
"redux": "^4.0.5",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
@ -23,10 +23,16 @@
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
1
examples/todoApp/out/src/About.css
Normal file
1
examples/todoApp/out/src/About.css
Normal file
@ -0,0 +1 @@
|
||||
.aboutPage { color: black; text-align: center; }
|
37
examples/todoApp/out/src/About.js
Normal file
37
examples/todoApp/out/src/About.js
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Todo from "./ext-src/Todo.js"
|
||||
|
||||
import * as taskState from './entities/task/state.js'
|
||||
import * as taskActions from './entities/task/actions.js'
|
||||
import Task from './entities/task/Task.js'
|
||||
import NewTaskForm from './entities/task/components/NewTaskForm.js'
|
||||
import TaskList from './entities/task/components/TaskList.js'
|
||||
|
||||
import './About.css'
|
||||
|
||||
|
||||
export class About extends Component {
|
||||
// TODO: Add propTypes.
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="aboutPage">
|
||||
<h1> About </h1>
|
||||
<p>This page was built with <a href="https://wasp-lang.dev">Wasp</a>!</p>
|
||||
<p> Check out source code
|
||||
<a href="https://github.com/wasp-lang/wasp/tree/master/examples/todoApp">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
taskList: taskState.selectors.all(state)
|
||||
}), {
|
||||
addTask: taskActions.add,
|
||||
updateTask: taskActions.update,
|
||||
removeTask: taskActions.remove
|
||||
})(About)
|
@ -1,17 +0,0 @@
|
||||
div {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.mainContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.taskListContainer {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.selected {
|
||||
border: 1px solid black;
|
||||
}
|
@ -1,17 +1,15 @@
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Todo from "././ext-src/Todo"
|
||||
import Todo from "./ext-src/Todo.js"
|
||||
|
||||
import * as taskState from '././entities/task/state.js'
|
||||
import * as taskActions from '././entities/task/actions.js'
|
||||
import Task from '././entities/task/Task.js'
|
||||
import * as taskState from './entities/task/state.js'
|
||||
import * as taskActions from './entities/task/actions.js'
|
||||
import Task from './entities/task/Task.js'
|
||||
import NewTaskForm from './entities/task/components/NewTaskForm.js'
|
||||
import TaskList from './entities/task/components/TaskList.js'
|
||||
|
||||
import NewTaskForm from '././entities/task/components/NewTaskForm.js'
|
||||
|
||||
import TaskList from '././entities/task/components/TaskList.js'
|
||||
|
||||
import '././Main.css'
|
||||
import './ext-src/Main.css'
|
||||
|
||||
|
||||
export class Main extends Component {
|
||||
@ -19,13 +17,19 @@ export class Main extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Todo
|
||||
<>
|
||||
{ /* Here we use Todo React component that we imported at the beginning of this file. */ }
|
||||
<Todo
|
||||
addTask={this.props.addTask}
|
||||
taskList={this.props.taskList}
|
||||
updateTask={this.props.updateTask}
|
||||
removeTask={this.props.removeTask}
|
||||
>
|
||||
</Todo>
|
||||
<div className="mainPage__nav">
|
||||
<a href="/about" className="mainPage__nav__aboutLink"> About </a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import uuidv4 from 'uuid/v4'
|
||||
|
||||
export default class Task {
|
||||
@ -7,8 +6,8 @@ export default class Task {
|
||||
constructor (data = {}) {
|
||||
this._data = {
|
||||
id: data.id || uuidv4(),
|
||||
description: data.description,
|
||||
isDone: data.isDone,
|
||||
description: data.description,
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,12 +15,12 @@ export default class Task {
|
||||
return this._data.id
|
||||
}
|
||||
|
||||
get description () {
|
||||
return this._data.description
|
||||
}
|
||||
get isDone () {
|
||||
return this._data.isDone
|
||||
}
|
||||
get description () {
|
||||
return this._data.description
|
||||
}
|
||||
|
||||
toData () {
|
||||
return this._data
|
||||
|
@ -11,12 +11,12 @@ export const add = (task) => ({
|
||||
|
||||
/**
|
||||
* @param {String} id
|
||||
* @param {Task} updatedTask
|
||||
* @param {Object} data - Partial data that will be merged with existing task.
|
||||
*/
|
||||
export const update = (id, updatedTask) => ({
|
||||
export const update = (id, data) => ({
|
||||
type: types.UPDATE,
|
||||
id,
|
||||
data: updatedTask.toData()
|
||||
data
|
||||
})
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
import _ from 'lodash'
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel'
|
||||
import Switch from '@material-ui/core/Switch'
|
||||
@ -10,12 +11,15 @@ import Task from '../Task'
|
||||
|
||||
|
||||
export default class NewTaskForm extends React.Component {
|
||||
// TODO: Add propTypes.
|
||||
static propTypes = {
|
||||
onCreate: PropTypes.func,
|
||||
submitButtonLabel: PropTypes.string
|
||||
}
|
||||
|
||||
state = {
|
||||
fields: {
|
||||
description: '',
|
||||
isDone: false,
|
||||
description: '',
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,8 +33,8 @@ export default class NewTaskForm extends React.Component {
|
||||
}
|
||||
|
||||
resetAllFields = () => {
|
||||
this.setField('description', '')
|
||||
this.setField('isDone', false)
|
||||
this.setField('description', '')
|
||||
}
|
||||
|
||||
toggleField = (name) => {
|
||||
@ -48,24 +52,20 @@ export default class NewTaskForm extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={ { margin: '20px' } }>
|
||||
<div className={this.props.className}>
|
||||
<form noValidate onSubmit={this.handleSubmit} action="javascript:void(0);">
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
label="description"
|
||||
placeholder="What needs to be done?"
|
||||
value={this.getField('description')}
|
||||
onChange={event => this.setField('description', event.target.value)}
|
||||
margin="normal"
|
||||
fullWidth
|
||||
InputLabelProps={{
|
||||
shrink: true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
@ -29,13 +29,6 @@ export class TaskList extends React.Component {
|
||||
taskBeingEdited: null
|
||||
}
|
||||
|
||||
updateTaskField = (fieldName, newFieldValue, task) => {
|
||||
const updatedTask = new Task(
|
||||
{ ...task.toData(), [fieldName]: newFieldValue }
|
||||
)
|
||||
this.props.updateTask(task.id, updatedTask)
|
||||
}
|
||||
|
||||
setAsBeingEdited = task => this.setState({
|
||||
taskBeingEdited: task.id
|
||||
})
|
||||
@ -48,41 +41,28 @@ export class TaskList extends React.Component {
|
||||
this.setState({ taskBeingEdited: null })
|
||||
}
|
||||
|
||||
renderTaskDescription =
|
||||
(task) => task.isDone ? <s>{task.description}</s> : task.description
|
||||
|
||||
render() {
|
||||
const taskListToShow = this.props.filter ?
|
||||
this.props.taskList.filter(this.props.filter) :
|
||||
this.props.taskList
|
||||
|
||||
return (
|
||||
<div style={ { margin: '20px' } }>
|
||||
<div className={this.props.className}>
|
||||
<Paper>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableHead style={{display: 'none'}}>
|
||||
<TableRow>
|
||||
<TableCell>description</TableCell>
|
||||
<TableCell>isDone</TableCell>
|
||||
<TableCell width="50%">isDone</TableCell>
|
||||
<TableCell width="50%">description</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody>
|
||||
{taskListToShow.map((task) => (
|
||||
<TableRow key={task.id}>
|
||||
<ClickAwayListener onClickAway={() => this.finishEditing(task) }>
|
||||
<TableCell
|
||||
onDoubleClick={() => this.setAsBeingEdited(task)}
|
||||
>
|
||||
{this.props.editable && this.isBeingEdited(task) ? (
|
||||
<TextField
|
||||
value={task.description}
|
||||
onChange={e => this.updateTaskField(
|
||||
'description', e.target.value, task
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
task.description
|
||||
)}
|
||||
</TableCell>
|
||||
</ClickAwayListener>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={task.isDone}
|
||||
@ -91,11 +71,27 @@ export class TaskList extends React.Component {
|
||||
'aria-label': 'checkbox'
|
||||
}}
|
||||
disabled={!this.props.editable}
|
||||
onChange={e => this.updateTaskField(
|
||||
'isDone', e.target.checked, task
|
||||
onChange={e => this.props.updateTask(
|
||||
task.id, { 'isDone': e.target.checked }
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<ClickAwayListener onClickAway={() => this.finishEditing(task) }>
|
||||
<TableCell
|
||||
onDoubleClick={() => this.setAsBeingEdited(task)}
|
||||
>
|
||||
{this.props.editable && this.isBeingEdited(task) ? (
|
||||
<TextField
|
||||
value={task.description}
|
||||
onChange={e => this.props.updateTask(
|
||||
task.id, { 'description': e.target.value }
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
this.renderTaskDescription(task)
|
||||
)}
|
||||
</TableCell>
|
||||
</ClickAwayListener>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
@ -24,7 +24,10 @@ const reducer = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
all: state.all.map(
|
||||
task => task.id === action.id ? action.data : task
|
||||
task =>
|
||||
task.id === action.id
|
||||
? { ...task, ...action.data }
|
||||
: task
|
||||
)
|
||||
}
|
||||
|
||||
|
71
examples/todoApp/out/src/ext-src/Main.css
Normal file
71
examples/todoApp/out/src/ext-src/Main.css
Normal file
@ -0,0 +1,71 @@
|
||||
div {
|
||||
color: green;
|
||||
}
|
||||
|
||||
button.selected {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.todos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.todos__container {
|
||||
width: 550px;
|
||||
}
|
||||
|
||||
.todos__toggleAndInput {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.todos__toggleButton {
|
||||
height: 40px;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 20px;
|
||||
|
||||
background-color: white;
|
||||
border: none;
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.todos__newTaskForm {
|
||||
width: 100%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.todos__footer {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.todos__footer__filters .filter:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mainPage__nav {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.mainPage__nav__aboutLink {
|
||||
color: lightgray;
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.mainPage__nav__aboutLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
108
examples/todoApp/out/src/ext-src/Todo.js
Normal file
108
examples/todoApp/out/src/ext-src/Todo.js
Normal file
@ -0,0 +1,108 @@
|
||||
// As seen here, we can import npm packages used in code generated by Wasp, which will be clearly defined.
|
||||
// In the future, we will of course also be able to specify additional packages as dependencies.
|
||||
import React from 'react'
|
||||
|
||||
// As seen here, we can import specific components/code generated by Wasp.
|
||||
// These will have well defined and documented APIs and paths.
|
||||
// Note that Task, NewTaskForm and TaskList are generated based on the declarations
|
||||
// we made in todoApp.wasp file.
|
||||
import Task from '../entities/task/Task'
|
||||
import NewTaskForm from '../entities/task/components/NewTaskForm'
|
||||
import TaskList from '../entities/task/components/TaskList'
|
||||
|
||||
const TASK_FILTER_TYPES = Object.freeze({
|
||||
ALL: 'all',
|
||||
ACTIVE: 'active',
|
||||
COMPLETED: 'completed'
|
||||
})
|
||||
|
||||
const TASK_FILTERS = Object.freeze({
|
||||
[TASK_FILTER_TYPES.ALL]: null,
|
||||
[TASK_FILTER_TYPES.ACTIVE]: task => !task.isDone,
|
||||
[TASK_FILTER_TYPES.COMPLETED]: task => task.isDone
|
||||
})
|
||||
|
||||
export default class Todo extends React.Component {
|
||||
// TODO: prop types.
|
||||
|
||||
state = {
|
||||
taskFilterName: TASK_FILTER_TYPES.ALL
|
||||
}
|
||||
|
||||
toggleIsDoneForAllTasks = () => {
|
||||
const areAllDone = this.props.taskList.every(t => t.isDone)
|
||||
this.props.taskList.map(t => this.props.updateTask(t.id, { isDone: !areAllDone }))
|
||||
}
|
||||
|
||||
deleteCompletedTasks = () => {
|
||||
this.props.taskList.map((t) => { if (t.isDone) this.props.removeTask(t.id) })
|
||||
}
|
||||
|
||||
isAnyTaskCompleted = () => this.props.taskList.some(t => t.isDone)
|
||||
|
||||
isThereAnyTask = () => this.props.taskList.length > 0
|
||||
|
||||
TaskFilterButton = ({ filterType, label }) => (
|
||||
<button
|
||||
className={'filter ' + (this.state.taskFilterName === filterType ? 'selected' : '')}
|
||||
onClick={() => this.setState({ taskFilterName: filterType })}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="todos">
|
||||
<div className="todos__container">
|
||||
<h1> Todos </h1>
|
||||
|
||||
<div className="todos__toggleAndInput">
|
||||
<button
|
||||
disabled={!this.isThereAnyTask()}
|
||||
className="todos__toggleButton"
|
||||
onClick={this.toggleIsDoneForAllTasks}>
|
||||
✓
|
||||
</button>
|
||||
|
||||
<NewTaskForm
|
||||
className="todos__newTaskForm"
|
||||
onCreate={task => this.props.addTask(task)}
|
||||
submitButtonLabel={'Create new task'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ this.isThereAnyTask() && (<>
|
||||
<TaskList
|
||||
editable
|
||||
filter={TASK_FILTERS[this.state.taskFilterName]}
|
||||
/>
|
||||
|
||||
<div className="todos__footer">
|
||||
<div className="todos__footer__itemsLeft">
|
||||
{ this.props.taskList.filter(task => !task.isDone).length } items left
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__filters">
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ALL} label="All" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ACTIVE} label="Active" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.COMPLETED} label="Completed" />
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__clearCompleted">
|
||||
<button
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
onClick={this.deleteCompletedTasks}>
|
||||
Clear completed
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import Task from '../entities/task/Task'
|
||||
import NewTaskForm from '../entities/task/components/NewTaskForm'
|
||||
import TaskList from '../entities/task/components/TaskList'
|
||||
|
||||
import * as config from './config'
|
||||
|
||||
const TASK_FILTER_TYPES = Object.freeze({
|
||||
ALL: 'all',
|
||||
ACTIVE: 'active',
|
||||
COMPLETED: 'completed'
|
||||
})
|
||||
|
||||
const TASK_FILTERS = Object.freeze({
|
||||
[TASK_FILTER_TYPES.ALL]: null,
|
||||
[TASK_FILTER_TYPES.ACTIVE]: task => !task.isDone,
|
||||
[TASK_FILTER_TYPES.COMPLETED]: task => task.isDone
|
||||
})
|
||||
|
||||
export default class Todo extends React.Component {
|
||||
|
||||
state = {
|
||||
taskFilterName: TASK_FILTER_TYPES.ALL
|
||||
}
|
||||
|
||||
toggleIsDoneForAllTasks = () => {
|
||||
const areAllDone = this.props.taskList.every(t => t.isDone)
|
||||
{/* TODO: This feels clumsy / complicated. Is there a better way than using id (maybe not)?
|
||||
Should we consider passing just data to update, not the whole object, so we don't have to
|
||||
create new object here? Maybe we can change this update, or have a second update method. */}
|
||||
this.props.taskList.map(
|
||||
(t) => this.props.updateTask(t.id, new Task ({ ...t.toData(), isDone: !areAllDone }))
|
||||
)
|
||||
}
|
||||
|
||||
deleteCompletedTasks = () => {
|
||||
this.props.taskList.map((t) => { if (t.isDone) this.props.removeTask(t.id) })
|
||||
}
|
||||
|
||||
TaskFilterButton = ({ filterType, label }) => (
|
||||
<button
|
||||
className={this.state.taskFilterName === filterType ? 'selected' : null}
|
||||
onClick={() => this.setState({ taskFilterName: filterType })}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="mainContainer">
|
||||
<h1> { config.appName } </h1>
|
||||
|
||||
<button onClick={this.toggleIsDoneForAllTasks}>
|
||||
Toggle completion {/* TODO: Use icon (but we need to either install @material-ui/icons
|
||||
or add font-awesome to the index.html. */}
|
||||
</button>
|
||||
|
||||
<NewTaskForm
|
||||
onCreate={task => this.props.addTask(task)}
|
||||
submitButtonLabel={'Create new task'}
|
||||
/>
|
||||
|
||||
<div className="taskListContainer">
|
||||
<TaskList
|
||||
editable
|
||||
filter={TASK_FILTERS[this.state.taskFilterName]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="footer">
|
||||
<div>
|
||||
{ this.props.taskList.filter(task => !task.isDone).length } items left
|
||||
|
||||
{ this.props.taskList.some(t => t.isDone) &&
|
||||
<button onClick={this.deleteCompletedTasks}>Clear completed</button>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ALL} label="All" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ACTIVE} label="Active" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.COMPLETED} label="Completed" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
|
||||
export const appName = 'Todos'
|
Binary file not shown.
Before Width: | Height: | Size: 3.4 KiB |
@ -2,12 +2,14 @@ import React from 'react'
|
||||
import { Route, BrowserRouter as Router } from 'react-router-dom'
|
||||
|
||||
import Main from './Main'
|
||||
import About from './About'
|
||||
|
||||
|
||||
const router = (
|
||||
<Router>
|
||||
<div>
|
||||
<Route exact path="/" component={ Main }/>
|
||||
<Route exact path="/about" component={ About }/>
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import * as reduxStarterKit from 'redux-starter-kit'
|
||||
import * as RTK from '@reduxjs/toolkit'
|
||||
|
||||
import loggerMiddleware from './middleware/logger'
|
||||
|
||||
@ -9,13 +9,13 @@ import loggerMiddleware from './middleware/logger'
|
||||
*/
|
||||
export const configureStore = (reducer, preloadedState) => {
|
||||
const middleware = [
|
||||
loggerMiddleware,
|
||||
...reduxStarterKit.getDefaultMiddleware()
|
||||
...RTK.getDefaultMiddleware(),
|
||||
loggerMiddleware
|
||||
]
|
||||
|
||||
const enhancers = []
|
||||
|
||||
const store = reduxStarterKit.configureStore({
|
||||
const store = RTK.configureStore({
|
||||
reducer,
|
||||
preloadedState,
|
||||
middleware,
|
||||
|
71
examples/todoApp/src/ext/Main.css
Normal file
71
examples/todoApp/src/ext/Main.css
Normal file
@ -0,0 +1,71 @@
|
||||
div {
|
||||
color: green;
|
||||
}
|
||||
|
||||
button.selected {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.todos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.todos__container {
|
||||
width: 550px;
|
||||
}
|
||||
|
||||
.todos__toggleAndInput {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.todos__toggleButton {
|
||||
height: 40px;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 20px;
|
||||
|
||||
background-color: white;
|
||||
border: none;
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.todos__newTaskForm {
|
||||
width: 100%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.todos__footer {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.todos__footer__filters .filter:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.mainPage__nav {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.mainPage__nav__aboutLink {
|
||||
color: lightgray;
|
||||
font-size: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.mainPage__nav__aboutLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
108
examples/todoApp/src/ext/Todo.js
Normal file
108
examples/todoApp/src/ext/Todo.js
Normal file
@ -0,0 +1,108 @@
|
||||
// As seen here, we can import npm packages used in code generated by Wasp, which will be clearly defined.
|
||||
// In the future, we will of course also be able to specify additional packages as dependencies.
|
||||
import React from 'react'
|
||||
|
||||
// As seen here, we can import specific components/code generated by Wasp.
|
||||
// These will have well defined and documented APIs and paths.
|
||||
// Note that Task, NewTaskForm and TaskList are generated based on the declarations
|
||||
// we made in todoApp.wasp file.
|
||||
import Task from '@wasp/entities/task/Task'
|
||||
import NewTaskForm from '@wasp/entities/task/components/NewTaskForm'
|
||||
import TaskList from '@wasp/entities/task/components/TaskList'
|
||||
|
||||
const TASK_FILTER_TYPES = Object.freeze({
|
||||
ALL: 'all',
|
||||
ACTIVE: 'active',
|
||||
COMPLETED: 'completed'
|
||||
})
|
||||
|
||||
const TASK_FILTERS = Object.freeze({
|
||||
[TASK_FILTER_TYPES.ALL]: null,
|
||||
[TASK_FILTER_TYPES.ACTIVE]: task => !task.isDone,
|
||||
[TASK_FILTER_TYPES.COMPLETED]: task => task.isDone
|
||||
})
|
||||
|
||||
export default class Todo extends React.Component {
|
||||
// TODO: prop types.
|
||||
|
||||
state = {
|
||||
taskFilterName: TASK_FILTER_TYPES.ALL
|
||||
}
|
||||
|
||||
toggleIsDoneForAllTasks = () => {
|
||||
const areAllDone = this.props.taskList.every(t => t.isDone)
|
||||
this.props.taskList.map(t => this.props.updateTask(t.id, { isDone: !areAllDone }))
|
||||
}
|
||||
|
||||
deleteCompletedTasks = () => {
|
||||
this.props.taskList.map((t) => { if (t.isDone) this.props.removeTask(t.id) })
|
||||
}
|
||||
|
||||
isAnyTaskCompleted = () => this.props.taskList.some(t => t.isDone)
|
||||
|
||||
isThereAnyTask = () => this.props.taskList.length > 0
|
||||
|
||||
TaskFilterButton = ({ filterType, label }) => (
|
||||
<button
|
||||
className={'filter ' + (this.state.taskFilterName === filterType ? 'selected' : '')}
|
||||
onClick={() => this.setState({ taskFilterName: filterType })}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="todos">
|
||||
<div className="todos__container">
|
||||
<h1> Todos </h1>
|
||||
|
||||
<div className="todos__toggleAndInput">
|
||||
<button
|
||||
disabled={!this.isThereAnyTask()}
|
||||
className="todos__toggleButton"
|
||||
onClick={this.toggleIsDoneForAllTasks}>
|
||||
✓
|
||||
</button>
|
||||
|
||||
<NewTaskForm
|
||||
className="todos__newTaskForm"
|
||||
onCreate={task => this.props.addTask(task)}
|
||||
submitButtonLabel={'Create new task'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ this.isThereAnyTask() && (<>
|
||||
<TaskList
|
||||
editable
|
||||
filter={TASK_FILTERS[this.state.taskFilterName]}
|
||||
/>
|
||||
|
||||
<div className="todos__footer">
|
||||
<div className="todos__footer__itemsLeft">
|
||||
{ this.props.taskList.filter(task => !task.isDone).length } items left
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__filters">
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ALL} label="All" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ACTIVE} label="Active" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.COMPLETED} label="Completed" />
|
||||
</div>
|
||||
|
||||
<div className="todos__footer__clearCompleted">
|
||||
<button
|
||||
className={this.isAnyTaskCompleted() ? '' : 'hidden' }
|
||||
onClick={this.deleteCompletedTasks}>
|
||||
Clear completed
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import Task from '@wasp/entities/task/Task'
|
||||
import NewTaskForm from '@wasp/entities/task/components/NewTaskForm'
|
||||
import TaskList from '@wasp/entities/task/components/TaskList'
|
||||
|
||||
import * as config from './config'
|
||||
|
||||
const TASK_FILTER_TYPES = Object.freeze({
|
||||
ALL: 'all',
|
||||
ACTIVE: 'active',
|
||||
COMPLETED: 'completed'
|
||||
})
|
||||
|
||||
const TASK_FILTERS = Object.freeze({
|
||||
[TASK_FILTER_TYPES.ALL]: null,
|
||||
[TASK_FILTER_TYPES.ACTIVE]: task => !task.isDone,
|
||||
[TASK_FILTER_TYPES.COMPLETED]: task => task.isDone
|
||||
})
|
||||
|
||||
export default class Todo extends React.Component {
|
||||
|
||||
state = {
|
||||
taskFilterName: TASK_FILTER_TYPES.ALL
|
||||
}
|
||||
|
||||
toggleIsDoneForAllTasks = () => {
|
||||
const areAllDone = this.props.taskList.every(t => t.isDone)
|
||||
{/* TODO: This feels clumsy / complicated. Is there a better way than using id (maybe not)?
|
||||
Should we consider passing just data to update, not the whole object, so we don't have to
|
||||
create new object here? Maybe we can change this update, or have a second update method. */}
|
||||
this.props.taskList.map(
|
||||
(t) => this.props.updateTask(t.id, new Task ({ ...t.toData(), isDone: !areAllDone }))
|
||||
)
|
||||
}
|
||||
|
||||
deleteCompletedTasks = () => {
|
||||
this.props.taskList.map((t) => { if (t.isDone) this.props.removeTask(t.id) })
|
||||
}
|
||||
|
||||
TaskFilterButton = ({ filterType, label }) => (
|
||||
<button
|
||||
className={this.state.taskFilterName === filterType ? 'selected' : null}
|
||||
onClick={() => this.setState({ taskFilterName: filterType })}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
render = () => {
|
||||
return (
|
||||
<div className="mainContainer">
|
||||
<h1> { config.appName } </h1>
|
||||
|
||||
<button onClick={this.toggleIsDoneForAllTasks}>
|
||||
Toggle completion {/* TODO: Use icon (but we need to either install @material-ui/icons
|
||||
or add font-awesome to the index.html. */}
|
||||
</button>
|
||||
|
||||
<NewTaskForm
|
||||
onCreate={task => this.props.addTask(task)}
|
||||
submitButtonLabel={'Create new task'}
|
||||
/>
|
||||
|
||||
<div className="taskListContainer">
|
||||
<TaskList
|
||||
editable
|
||||
filter={TASK_FILTERS[this.state.taskFilterName]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="footer">
|
||||
<div>
|
||||
{ this.props.taskList.filter(task => !task.isDone).length } items left
|
||||
|
||||
{ this.props.taskList.some(t => t.isDone) &&
|
||||
<button onClick={this.deleteCompletedTasks}>Clear completed</button>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ALL} label="All" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.ACTIVE} label="Active" />
|
||||
<this.TaskFilterButton filterType={TASK_FILTER_TYPES.COMPLETED} label="Completed" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
|
||||
export const appName = 'Todos'
|
Binary file not shown.
Before Width: | Height: | Size: 3.4 KiB |
@ -1,44 +1,21 @@
|
||||
// Goal of this file is to re-create a TODO app from http://todomvc.com
|
||||
import Todo from "@ext/Todo.js" // Imports non-wasp code from external code dir (ext/).
|
||||
|
||||
import Todo from "Todo" // Imports from external code dir (src/).
|
||||
|
||||
// -- Entities
|
||||
entity Task {
|
||||
description :: string,
|
||||
isDone :: boolean
|
||||
isDone :: boolean,
|
||||
description :: string
|
||||
}
|
||||
|
||||
// -- App and pages
|
||||
app todoApp {
|
||||
title: "ToDo App"
|
||||
}
|
||||
|
||||
// IDEA: `@connect Task as taskList` -> this would make it more obvious what is available, also we don't need to automatically try to guess what to import.
|
||||
page Main {
|
||||
route: "/",
|
||||
|
||||
style: {=css
|
||||
div {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.mainContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.taskListContainer {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.selected {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
css=},
|
||||
|
||||
content: {=jsx
|
||||
style: "@ext/Main.css",
|
||||
// TODO: We need to make this nicer / more explicit, it is not clear where is this coming from (these props).
|
||||
// Also, this wiring is not elegant.
|
||||
content: {=jsx <>
|
||||
{ /* Here we use Todo React component that we imported at the beginning of this file. */ }
|
||||
<Todo
|
||||
addTask={this.props.addTask}
|
||||
taskList={this.props.taskList}
|
||||
@ -46,27 +23,55 @@ page Main {
|
||||
removeTask={this.props.removeTask}
|
||||
>
|
||||
</Todo>
|
||||
<div className="mainPage__nav">
|
||||
<a href="/about" className="mainPage__nav__aboutLink"> About </a>
|
||||
</div>
|
||||
</> jsx=}
|
||||
}
|
||||
|
||||
page About {
|
||||
route: "/about",
|
||||
style: {=css
|
||||
.aboutPage { color: black; text-align: center; }
|
||||
css=},
|
||||
content: {=jsx
|
||||
<div className="aboutPage">
|
||||
<h1> About </h1>
|
||||
<p>This page was built with <a href="https://wasp-lang.dev">Wasp</a>!</p>
|
||||
<p> Check out source code
|
||||
<a href="https://github.com/wasp-lang/wasp/tree/master/examples/todoApp">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
jsx=}
|
||||
}
|
||||
|
||||
// Entity form definition.
|
||||
entity-form<Task> NewTaskForm {
|
||||
fields: {
|
||||
description: {
|
||||
show: true
|
||||
show: true,
|
||||
label: none,
|
||||
placeholder: "What needs to be done?"
|
||||
},
|
||||
isDone: {
|
||||
show: false,
|
||||
defaultValue: false // Although not shown, this field will be set to "false".
|
||||
defaultValue: false
|
||||
}
|
||||
},
|
||||
submit: {
|
||||
onEnter: true, // Parsed but not generated yet.
|
||||
onEnter: true,
|
||||
button: { show: false }
|
||||
}
|
||||
}
|
||||
|
||||
// Entity list definition.
|
||||
entity-list<Task> TaskList {
|
||||
// Options TBD, not supported for now.
|
||||
showHeader: false,
|
||||
fields: {
|
||||
description: {
|
||||
// The contract for render is that user must provide a function that:
|
||||
// - Receives a task as an input.
|
||||
// - Returns a React Node or something that can be rendered by JSX.
|
||||
// - Does not depend on any outer context.
|
||||
render: {=js (task) => task.isDone ? <s>{task.description}</s> : task.description js=}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user