23
loading...
This website collects cookies to deliver better user experience
I've developed a ToDo React app with HarperDB Custom Functions.
We will create a simple ToDo React App. When we are done, it will look like this when it runs in localhost:
npx create-react-app my-app
cd my-app
npm start
Dependencies used:
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/icons-material": "^5.0.5",
"@mui/material": "^5.0.6",
"@testing-library/jest-dom": "^5.15.0",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"axios": "^0.24.0",
"classnames": "^2.3.1",
"history": "^5.1.0",
"lodash.debounce": "^4.0.8",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.0.1",
"react-scripts": "4.0.3",
"web-vitals": "^1.1.2"
Alternatively, you can clone the GitHub repository and use the start directory as your project root. It contains the basic project setup that will get you ready. In this project for the CSS you can refer to Tasks.css (src\todo-component\Tasks.css)
src\todo-component\Tasks.jsx
This component acts as a container component (which is having a task list & task search as a child component)
import React, { useEffect, useCallback, useState, useRef } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import TaskSearch from './task-search-component/TaskSearch';
import './Tasks.css';
import axios from 'axios';
import debounce from '@mui/utils/debounce';
import TaskItem from './task-list-component/TaskList';
import Snackbar from '@mui/material/Snackbar';
export default function Tasks() {
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const [taskList, setTaskList] = useState([]);
const [filteredList, setFilteredList] = useState([]);
const [open, setOpen] = useState(false);
const [msg, setMsg] = useState('')
const selectedId = useRef();
useEffect(() => {
getFilteredList();
}, [searchParams, taskList]);
const setSelectedId = (task) => {
selectedId.current = task;
};
const saveTask = async (taskName) => {
if (taskName.length > 0) {
try {
await axios.post(
'your_url_here',
{ taskTitle: taskName, taskStatus: 'ACTIVE', operation: 'sql' }
);
getTasks();
} catch (ex) {
showToast();
}
}
};
const updateTask = async (taskName) => {
if (taskName.length > 0) {
try {
await axios.put(
'your_url_here',
{
taskTitle: taskName,
operation: 'sql',
id: selectedId.current.id,
taskStatus: selectedId.current.taskStatus,
}
);
getTasks();
} catch (ex) {
showToast();
}
}
};
const doneTask = async (task) => {
try {
await axios.put(
'your_url_here',
{
taskTitle: task.taskTitle,
operation: 'sql',
id: task.id,
taskStatus: task.taskStatus,
}
);
getTasks();
} catch (ex) {
showToast();
}
};
const deleteTask = async (task) => {
try {
await axios.delete(
`your_url_here/${task.id}`
);
getTasks();
} catch (ex) {
showToast();
}
};
const getFilteredList = () => {
if (searchParams.get('filter')) {
const list = [...taskList];
setFilteredList(
list.filter(
(item) => item.taskStatus === searchParams.get('filter').toUpperCase()
)
);
} else {
setFilteredList([...taskList]);
}
};
useEffect(() => {
getTasks();
}, []);
const getTasks = async () => {
try {
const res = await axios.get(
'your_url_here'
);
console.log(res);
setTaskList(res.data);
} catch(ex) {
showToast();
}
};
const debounceSaveData = useCallback(debounce(saveTask, 500), []);
const searchHandler = async (taskName) => {
debounceSaveData(taskName);
};
const showToast = () => {
setMsg('Oops. Something went wrong!');
setOpen(true)
}
return (
<div className="main">
<TaskSearch searchHandler={searchHandler} />
<ul className="task-filters">
<li>
<a
href="javascript:void(0)"
onClick={() => navigate('/')}
className={!searchParams.get('filter') ? 'active' : ''}
>
View All
</a>
</li>
<li>
<a
href="javascript:void(0)"
onClick={() => navigate('/?filter=active')}
className={searchParams.get('filter') === 'active' ? 'active' : ''}
>
Active
</a>
</li>
<li>
<a
href="javascript:void(0)"
onClick={() => navigate('/?filter=completed')}
className={
searchParams.get('filter') === 'completed' ? 'active' : ''
}
>
Completed
</a>
</li>
</ul>
{filteredList.map((task) => (
<TaskItem
deleteTask={deleteTask}
doneTask={doneTask}
getSelectedId={setSelectedId}
task={task}
searchComponent={
<TaskSearch
searchHandler={updateTask}
defaultValue={task.taskTitle}
/>
}
/>
))}
<Snackbar
open={open}
autoHideDuration={6000}
onClose={() => setOpen(false)}
message={msg}
/>
</div>
);
}
your_url_here = you should replace this with your HarperDB endpoint URL.
src\todo-component\task-list-component\TaskList.jsx
This component is used to render all the list of tasks that we are getting from the HarperDB
import React, { useState } from 'react';
import classNames from 'classnames';
import IconButton from '@mui/material/IconButton';
import DoneIcon from '@mui/icons-material/Done';
import EditIcon from '@mui/icons-material/Edit';
import ClearIcon from '@mui/icons-material/Clear';
import DeleteIcon from '@mui/icons-material/Delete';
import TextField from '@mui/material/TextField';
export default function TaskItem({ task, searchComponent, getSelectedId, doneTask, deleteTask }) {
const [editing, setEditing] = useState(false);
const [selectedTask, setSelectedTask] = useState();
let containerClasses = classNames('task-item', {
'task-item--completed': task.completed,
'task-item--editing': editing,
});
const updateTask = () => {
doneTask({...task, taskStatus: task.taskStatus === 'ACTIVE' ? 'COMPLETED' : 'ACTIVE'});
}
const renderTitle = task => {
return (
<div className="task-item__title" tabIndex="0">
{task.taskTitle}
</div>
);
}
const resetField = () => {
setEditing(false);
}
const renderTitleInput = task => {
return (
React.cloneElement(searchComponent, {resetField})
);
}
return (
<div className={containerClasses} tabIndex="0">
<div className="cell">
<IconButton color={task.taskStatus === 'COMPLETED' ? 'success': 'secondary'} aria-label="delete" onClick={updateTask} className={classNames('btn--icon', 'task-item__button', {
active: task.completed,
hide: editing,
})} >
<DoneIcon />
</IconButton>
</div>
<div className="cell">
{editing ? renderTitleInput(task) : renderTitle(task)}
</div>
<div className="cell">
{!editing && <IconButton onClick={() => {setEditing(true); getSelectedId(task)}} aria-label="delete" className={classNames('btn--icon', 'task-item__button', {
hide: editing,
})} >
<EditIcon />
</IconButton> }
{editing && <IconButton onClick={() => {setEditing(false); getSelectedId('');}} aria-label="delete" className={classNames('btn--icon', 'task-item__button', {
hide: editing,
})} >
<ClearIcon />
</IconButton> }
{!editing && <IconButton onClick={() => deleteTask(task)} aria-label="delete" className={classNames('btn--icon', 'task-item__button', {
hide: editing,
})} >
<DeleteIcon />
</IconButton> }
</div>
</div>
);
}
src\todo-component\task-search-component\TaskSearch.jsx
This component provides a text box to users where users can enter the name of the task which they need to perform. (Same component we are using while editing a task)
import React from 'react';
import TextField from '@mui/material/TextField';
export default function TaskSearch({ searchHandler, defaultValue, resetField }) {
const handleEnterKey = event => {
if(event.keyCode === 13) {
searchHandler(event.target.value);
event.target.value = '';
if(resetField) {
resetField();
}
}
}
return (
<TextField
id="filled-required"
variant="standard"
fullWidth
hiddenLabel
placeholder="What needs to be done?"
onKeyUp={handleEnterKey}
defaultValue={defaultValue}
/>
);
}
Tip: Before creating a project, you need to enable custom functions, once you click on functions you will see a pop up like below:
Which is used to store data in DB
server.route({
url: '/saveTask',
method: 'POST',
// preValidation: hdbCore.preValidation,
handler: (request) => {
request.body= {
operation: 'sql',
sql: `insert into example_db.tasks (taskTitle, taskStatus) values('${request.body.taskTitle}', '${request.body.taskStatus}')`
};
return hdbCore.requestWithoutAuthentication(request);
},
});
This is used to edit an existing record in your DB, we are using the same endpoint as the save task but having a different method type as PUT.
server.route({
url: '/saveTask',
method: 'PUT',
// preValidation: hdbCore.preValidation,
handler: (request) => {
request.body= {
operation: 'sql',
sql: `update example_db.tasks set taskTitle='${request.body.taskTitle}', taskStatus='${request.body.taskStatus}' where id='${request.body.id}'`
};
return hdbCore.requestWithoutAuthentication(request);
},
});
server.route({
url: '/deleteTask/:id',
method: 'DELETE',
// preValidation: hdbCore.preValidation,
handler: (request) => {
request.body= {
operation: 'sql',
sql: `delete from example_db.tasks where id='${request.params.id}'`
};
return hdbCore.requestWithoutAuthentication(request);
},
});
// GET, WITH ASYNC THIRD-PARTY AUTH PREVALIDATION
server.route({
url: '/tasks',
method: 'GET',
// preValidation: (request) => customValidation(request, logger),
handler: (request) => {
request.body= {
operation: 'sql',
sql: 'select * from example_db.tasks'
};
/*
* requestWithoutAuthentication bypasses the standard HarperDB authentication.
* YOU MUST ADD YOUR OWN preValidation method above, or this method will be available to anyone.
*/
return hdbCore.requestWithoutAuthentication(request);
}
});
<Router basename="/dogs/static">
<Switch>
<Route path="/care" component={CarePage} />
<Route path="/feeding" component={FeedingPage} />
</Switch>
</Router>
{
"operation": "restart_service",
"service": "custom_functions"
}