first commit

This commit is contained in:
= 2024-09-07 07:52:09 -04:00
commit 823b31e3fe
48 changed files with 1690 additions and 0 deletions

38
App.css Normal file
View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

18
App.jsx Normal file
View File

@ -0,0 +1,18 @@
import * as React from "react";
import { connect } from "react-redux";
import Layout from "./components/layout";
import { toggleCompleted } from "./redux/actions";
function App() {
return (
<>
<Layout />
</>
);
}
export default connect(null, { toggleCompleted })(App);

8
App.test.jsx Normal file
View File

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

36
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,36 @@
pipeline {
agent {
node {
label 'master'
}
}
options {
buildDiscarder logRotator(
daysToKeepStr: '16',
numToKeepStr: '10'
)
}
stages {
stage(' Unit Testing') {
steps {
sh """
echo "Running Unit Tests"
"""
}
}
stage('Code Analysis') {
steps {
sh """
echo "Running Code Analysis"
"""
}
}
}
}

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

72
PseudoCode.txt Normal file
View File

@ -0,0 +1,72 @@
Alpha 0.01 Create couchdb/pouchdb databases as well as redux flow and access to django authentication/user management API (Status - WIP)
start
load index.html automatically into a login page
After login is succesful use security token to pull list of access and permissions information
From this point on to load to pouchdb any new database use project access id + user access token
Each project has an id which is required to find it in the couchdb and unexpired access id requried to read it
# I am not sure how I am going to synchronize security per project information with django server which has all the security
# information - most probably every 15 minutes django will contact couchdb and update the access tokens for each project
# since they are just certificates it should be pretty quick
# I will also investigate use of a common authenticator like oauth
Now user has their security paradigm loaded we need to load what they can see based on their clearance
play intro animation
While playing intro animation initiate pouchdb from couchdb and throw connection error if cannot pull from couchdb
From pouchdb initialize state and load redux
Check length of companies array in projects registry
This is a sanity check as each user should be part of at least one company and this list should not be empty
Else exit program and let user know that their security information could not be pulled (contact admin)
From redux check length of projects array in projects registry with filter for currentSelectedCompanyId
This is a sanity check as each user should be part of at least one project and this list should not be empty
Else exit program and let user know that their security information could not be pulled (contact admin)
Open project document using currentSelectedProjectId in dashboard
Load security tokens from API for project/company/tenant management django cloud app
Dashboard should contain a pull down for current company
Dashboard should contain a pull down for current project filtered based on company
Dashboard should have a new company button if they are cleared to created companies (otherwise don't show button)
Dashboard should contain a new project button based on company selected if they are cleared to create projects (otherwise don't show button)
Dashboard should contain a button for managing/viewing selected project
Dashboard should contain a button to manage company security if cleared (othewise no button)
Dashboard should contain a botton to manage PMO security if cleared (otherwise no button)
This concludes initial load - At this point user have following options
-Can select company or add a new company and activate it as selected company (if companyAdmin - otherwise new company is greyed out )
-Can select project or add a new project into company project registry for the company (if companyAdmin - otherwise new project is greyed out)
-If has permissions can initiate roles and responsibilties module for tenant adminitration to exit dasboard to go to tenant security module
-If has permissions can initiate roles and responsibilities module for company administration to exit dashboard to go to company security module
-Can chose to manage selected project and exits to that projects management module
Project Management Modules are dynamically created as each project has a different management
paradigm associated with project management model template. Each project has two different databases
meta-data database with contains relations based project management model as well as security. And another
unstructured document for each "leaf" in WBS structure. This allows flexibility to be managed on
schema created in metadata. That is to say each atomic work package at end of of WBS tree is a free form
document that can contain many data types including attachments, pictures,...
-Once in project management module we have different layout and options loaded based on management module template chosen during creation
-All KPIs and reporting of relations is built dynamically as each "object" of WBS tree only contains parents. Reason for multiple parents is that
an object can be related multiple objects. Objects are created based on project management model chosen.
Alpha 0.02 Use user profile model to load company/project dashboard as per user profile
Instead of writing to console, write to logs
Load with new project window a selector for template
Template will contain project schema for NoSQL for data that will be captured - Template will be versioned
Template will contain task definitions, relation definition, methodology and reporting framework
Template will also be mother of project specific workers, actioners, collectors and other artifacts
Create default template which is "DITSWBS instance configuration" project
Proces expected as follows:
-Since we don't have cloud instantiation defined yet user logs into dev DITSWBS environment
-User logs in
-If no company defined default admin will create the first company
-If no project is defined it will default to initial project which is configuration of DITSWBS
-If both company and project is defined user will default to last used company/project combination or to the default
-User presented with project dashboard defined as per template used
-User continues to work through drilling down this dashboard or selects different company/project pair (if user pulls down company change selector, upon selection project selector pops up. If user selects another project in same company this is only one step of selecting project. Cannot commit selections without both company and project being defined as selection)
Alpha 0.03 Create default project management model (PMP6) and couchdb database for PModels
Alpha 0.04 Synch django with couchdb for list of projects and security paradigm
Alpha 0.05 Create company form with upward sync
Alpha 0.06 Project creation form using PMP6 model as default with upward sync
Alpha 0.07 Create initial project management pages for a project created using PMP6 model
At this point admin can create users in django and projects created with PMP6 model will appear django as well as new database in couchdb (user has full rights to do everything)
...
Alpha 0.49 Create production build for react WBS app container
Alpha 0.50 Create container deployment scripts for kubernetes
Alpha 0.51 Create cton

6
README.md Normal file
View File

@ -0,0 +1,6 @@
# p00021
# MIT License initially
# subject to change
#
#

66
components/Dashboard.jsx Normal file
View File

@ -0,0 +1,66 @@
import React from "react";
import { connect } from "react-redux";
import ProjectList from "./projectList";
import TaskList from "./taskList";
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
// Dashboard shows projects loaded from cloud based NoSqL which is in sync with a relational
// database which holds security and other relationship based information about the projects
// When a project is selected various artifacts in the project are shown based on template
// it was based on.
function Dashboard(props) {
if (props.selectedProjectId !== "0") {
const selectedProject = (projects, selectedProjectId) => {
return projects.filter((project) => project.id === selectedProjectId);
};
const project = props.projects.find(
(project) => project.projectId === props.selectedProjectId
);
// return TaskList as a table with two columns WBS numbers and Tasks when a project is selected
// WBS number is recalculated when change occurs
// regardless of the template used task decomposition is utilized until task can be assigned to
// one and only one resource. Resource can be a person, team, departement or company.
return (
<>
<div>
<Box
sx={{
display: 'flex',
'& > :not(style)': {
m: 1,
width: 480,
height: 1/2,
},
}}
>
<Paper elevation={2}>
<h4> {project ? project.projectName : "No Project Selected - Please select one"} </h4>
<ProjectList />
<TaskList />
</Paper>
</Box>
</div>
</>
);
} else {
return (
<div>
<h2> Please select a project </h2>
<ProjectList />
</div>
);
}
}
export default connect(mapStateToProps)(Dashboard);
function mapStateToProps(state) {
return {
projects: state.projectsReducer.projects,
selectedProjectId: state.projectsReducer.selectedProjectId,
};
}

14
components/about.jsx Normal file
View File

@ -0,0 +1,14 @@
import * as React from "react";
function About() {
return ( <>
<div>
<h3>DITSWBS is mobile first client application with a cloud backend:</h3>
<p>Goal of the application is to separate concerns. Administration has its own portal that manages users, permissions, and project initiation.</p>
<p>Templates are static, open, currated, and JSON based. </p>
<p>Main program that client uses helps project manager, coordinator, user or guest to interract with project to break down work packages which come from Templates.</p>
</div>
</> );
}
export default About;

41
components/addTask.jsx Normal file
View File

@ -0,0 +1,41 @@
import React from "react";
import { connect } from "react-redux";
function TaskList(props) {
const handleAddTask = () => {
const text = prompt("Enter a new task:");
if (text) {
props.dispatch({ type: "ADD_TASK", text, id });
}
};
const handleToggleTask = (id) => {
props.dispatch({ type: "TOGGLE_TASK", id });
};
return (
<div>
<ul>
{props.tasks.map((task) => (
<li key={task.id}>
<input
type="checkbox"
checked={task.completed}
onChange={() => handleToggleTask(task.id)}
/>
{task.text}
</li>
))}
</ul>
<button onClick={handleAddTask}>Add Task</button>
</div>
);
}
function mapStateToProps(state) {
return {
tasks: state.tasksReducer.tasks,
};
}
export default connect(mapStateToProps)(TaskList);

View File

@ -0,0 +1,70 @@
import React, { useState } from "react";
import PouchDB from "pouchdb";
import { connect } from 'react-redux';
import { store } from "../redux/store";
import { useEffect } from "react";
function CommitToPouchDB(props) {
const project = props.projects.find(
(project) => project.projectId === props.selectedProjectId
);
const currentProject = project.projectName
// use selected project to commit
// please note project creation and management is done through project system administration web app
const projectLink = "http://192.168.0.234:5982/" + currentProject
// get latest version of the project from nosql database
console.log({projectLink})
const localTasksDB = new PouchDB({currentProject})
const remoteTasksDB = new PouchDB( {projectLink} , {
auth: {
username: "ditswbsclient",
password: "bluebeard",
},
});
const syncHandler = localTasksDB
.sync(remoteTasksDB, {
live: true,
retry: true,
})
.on("paused", (info) => {
console.log("Task replication paused");
})
.on("active", (info) => {
console.log("Task replication resumed");
})
.on("change", (change) => {
console.log("Task change detected");
});
const commitTasks = () => {
try {
const result = localTasksDB
.bulkDocs(props.tasks)
} catch (error) {
console.error("Error uploading new tasks to cloud: ", error);
}
return () => console.log('unmounting...');
}
useEffect(() => {
commitTasks();
}, []);
}
export default connect(mapStateToProps)(CommitToPouchDB);
function mapStateToProps(state) {
return {
tasks: state.tasksReducer.tasks,
selectedProjectId: state.projectsReducer.selectedProjectId,
projects: state.projectsReducer.projects,
};
}

View File

View File

@ -0,0 +1,11 @@
import React from 'react';
import PouchDB from 'pouchdb';
function createDatabaseCDB (props.databaseName) {
const db = new PouchDB(props.databaseName);
try {
await db.replicate.to('http://192.168.0.234:5894/'+{props.databaseName});
} catch (error) {
console.error('Error synchronizing local database with remote: ', error);
}
}

16
components/home.jsx Normal file
View File

@ -0,0 +1,16 @@
import * as React from "react";
function Home() {
return ( <>
<div>
<h2>Welcome to DITSWBS application</h2>
</div>
<div>
<h3>Please use administrative portal to manage users and projects</h3>
<h3>Dashboard is where you will coordinate your project tasks and work breakdown</h3>
<p>Templates that will play an important part in growth of DITSWBS are under development. This section will contain description of each curated template.</p>
</div>
</>);
}
export default Home;

194
components/layout.jsx Normal file
View File

@ -0,0 +1,194 @@
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Grow from '@mui/material/Grow';
import Paper from '@mui/material/Paper';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import Stack from '@mui/material/Stack';
import IconButton from '@mui/material/IconButton';
import MenuIcon from '@mui/icons-material/Menu';
//import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
//import Divider from '@mui/material/Divider';
import PropTypes from 'prop-types';
//import Tabs from '@mui/material/Tabs';
//import Tab from '@mui/material/Tab';
import {
Link as RouterLink,
Route,
Routes,
MemoryRouter,
} from 'react-router-dom';
import { StaticRouter } from 'react-router-dom/server';
import Home from "./home";
import Dashboard from "./Dashboard";
import About from "./about";
import PrivateRoute from "./privateRoute";
import Login from "./login";
//Start in root folder
function Router(props) {
const { children } = props;
if (typeof window === 'undefined') {
return <StaticRouter location="/">{children}</StaticRouter>;
}
return (
<MemoryRouter initialEntries={['/']} initialIndex={0}>
{children}
</MemoryRouter>
);
}
Router.propTypes = {
children: PropTypes.node,
};
const Link = React.forwardRef(function Link(itemProps, ref) {
return <RouterLink ref={ref} {...itemProps} role={undefined} />;
});
function ListItemLink(props) {
const { icon, primary, to } = props;
return (
<li>
<ListItem button component={Link} to={to}>
{icon ? <ListItemIcon>{icon}</ListItemIcon> : null}
<ListItemText primary={primary} />
</ListItem>
</li>
);
}
ListItemLink.propTypes = {
icon: PropTypes.element,
primary: PropTypes.string.isRequired,
to: PropTypes.string.isRequired,
};
export default function Layout() {
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
function handleListKeyDown(event) {
if (event.key === 'Tab') {
event.preventDefault();
setOpen(false);
} else if (event.key === 'Escape') {
setOpen(false);
}
}
// return focus to the button when we transitioned from !open -> open
const prevOpen = React.useRef(open);
React.useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus();
}
prevOpen.current = open;
}, [open]);
return (
<>
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static">
<Toolbar>
<Stack direction="row" spacing={2}>
<IconButton
size="large"
edge="start"
color="inherit"
aria-label="menu"
sx={{ mr: 2 }}
ref={anchorRef}
id="composition-button"
aria-controls={open ? 'composition-menu' : undefined}
aria-expanded={open ? 'true' : undefined}
aria-haspopup="true"
onClick={handleToggle}
>
<MenuIcon />
<Popper
open={open}
anchorEl={anchorRef.current}
role={undefined}
placement="bottom-start"
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom-start' ? 'left top' : 'left bottom',
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList
autoFocusItem={open}
id="composition-menu"
aria-labelledby="composition-button"
onKeyDown={handleListKeyDown}
>
<MenuItem onClick={handleClose}>
<Link component={RouterLink} to="/"> Home </Link>
</MenuItem>
<MenuItem onClick={handleClose}>
<PrivateRoute component={RouterLink} />
</MenuItem>
<MenuItem onClick={handleClose}>
<Link component={RouterLink} to="/about"> About </Link>
</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</IconButton>
</Stack>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
DITSWBS
</Typography>
</Toolbar>
</AppBar>
</Box>
<div>
<Routes>
<Route path="/" element=<Home /> >Home</Route>
<Route path="/about" element=<About /> >About</Route>
<Route path="/login" element=<Login /> >Login</Route>
<Route path="/dashboard" element=<Dashboard /> >Dashboard</Route>
</Routes>
</div>
</>
);
}

129
components/ldbcfg.jsx Normal file
View File

@ -0,0 +1,129 @@
import PouchDB from "pouchdb";
import { ADD_PROJECT,ADD_CDB_TASK } from "../redux/actionTypes";
import { store } from "../redux/store";
import { useEffect } from "react";
function LoadDB() {
const projectsDB = new PouchDB("projectsDB");
const remoteDB = new PouchDB("http://192.168.0.234:5984/ditswbs", {
auth: {
username: "ditswbsclient",
password: "bluebeard",
},
});
const syncHandler = projectsDB
.sync(remoteDB, {
live: true,
retry: true,
})
.on("paused", (info) => {
console.log("Project replication paused");
})
.on("active", (info) => {
console.log("Project replication resumed");
})
.on("change", (change) => {
console.log("Project change detected");
});
const initializeRedux = () => {
try {
const result = projectsDB
.allDocs({ include_docs: true })
.then((result) => {
const projects = result.rows.map((row) => row.doc);
projects.forEach((project) =>
{
store.dispatch({ type: ADD_PROJECT, project });
let cdbsite = "http://192.168.0.234:5984"+project.projectId
const remoteTasksDB = new PouchDB(cdbsite, {
auth: {
username: "ditswbsclient",
password: "bluebeard",
},
})
const taskResult = remoteTasksDB
.allDocs({ include_docs: true })
.then((taskResult) => {
const tasks = taskResult.rows.map((row) => row.doc);
tasks.forEach((task) => {
if(task.taskName !== null && task.taskName !== undefined) {
store.dispatch({ type: ADD_CDB_TASK, task })
}
}
)
});
});
}
);
} catch (error) {
console.error("Error initialiazing redux: ", error);
}
return () => console.log('unmounting...');
}
useEffect(() => {
initializeRedux();
}, []);
}
export default LoadDB;
//if(!ignore) {
//return () => {
// ignore = true;
// };
// Fetch initial projects
// const [documents, setDocuments] = useState([]);
// const db = new PouchDB('projectDocs');
// console.log ("Database created Successfully.");
//
// useEffect(() => {
// const initializeDatabase = async () => {
// try {
// const initialProject = await db.put ({
// _id: "99",
// projects: [
// {
// id: 0,
// projectName: "Default",
// description: "Default project for testing",
// status: "initiated" },
// ]}
// )
// } catch (error) {
// console.error('Error initializing database: ',error)
// }
// }
// const fetchDocuments = async () => {
// try {
// const result = await db.allDocs({ include_docs: true });
// const documents = result.rows.map(row => row.doc);
// setDocuments(documents);
// } catch (error) {
// console.error('Error fetching documents: ', error);
// }
// };
//
// initializeDatabase();
// fetchDocuments();
// }, []);

2
components/logger.jsx Normal file
View File

@ -0,0 +1,2 @@
# logger service - will log what is going on
# write to log instead of console out

59
components/login.jsx Normal file
View File

@ -0,0 +1,59 @@
import React, { useState } from "react";
import AUTH_API from "../constants";
//import AdminSite from "../configs/admincfgs";
const Login = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
try {
// Make a POST request to the external REST API for authentication
const ditswbsapisite = AUTH_API;
const response = await fetch(
ditswbsapisite,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
},
{ mode: "cors" }
);
if (response.ok) {
const data = await response.json();
// Store the authentication token in local storage or cookies
localStorage.setItem("authToken", data.token);
// Redirect to the authenticated page
window.location.href = "/dashboard";
} else {
// Handle login error (e.g., show error message to the user)
console.log("Login failed.");
}
} catch (error) {
console.error("Error during login:", error);
}
};
return (
<div>
<input
label="Username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default Login;

79
components/managePDB.js Normal file
View File

@ -0,0 +1,79 @@
import PouchDB from 'pouchdb';
import { connect } from 'react-redux'
function PDB(){
const projectsDB = new PouchDB("projectDocs");
const wbsDB = new PouchDB("wbsDocs");
const tasksDB = new PouchDB("taskDocs");
const remoteDB = new PouchDB('http://192.168.0.234:5984/ditswbs');
projectsDB.sync(remoteDB, { live: true, retry: true });
tasksDB.sync(remoteDB, { live: true, retry: true });
wbsDB.sync(remoteDB, { live: true, retry: true });
function GetDoc(docID) {
return new Promise((resolve, reject)=> {
projectsDB.get(docID)
.then((result)=> {return resolve(result)})
.catch((err)=> {
if (err.status === 404) {
console.log('error 404')
return resolve(null)
} else {
console.log('other:', err)
return reject(err)
}
})
})
}
//function commitProjects() {
// db.put(
// projects: props.projects,
// )
// }
// function commitWBS() {
// evaluate parent child relation based on IDs
// and determine WBS level of each task in selected project
// this is dictionary where WBS level against project ID
// reorder the dictionary
// put the dictionary in local pouchdb
// db.put(
// projects: props.projects,
// )
// }
// function commitTasks(Tasks) {
// first check if a project is selected. This is a requirement before tasks can be
// commmitted
// if (!props.selectedProjectId) {
// commit tasks for selected projectID equals to one of the entries in task parentID
// Step1 verify if current input list of tasks have selected selected projectID as
// parentCount = 0
// const parents = Tasks.parentId
// parents.map ( parent => {if ( parent seletecProjectId eq ) {
// parentCount = parentCount+1 }}
//
// parent. If one or more do not have selected tasks fail the check return their
// with the a descriptive error message.
//...
// Step2
// This step is nested in step 1. If initial check is successful then commit inputed
// if (parents.length = parentCount ) {
// ... commit code}
// exit with error
// }
// }
}
export default connect(mapStateToProps)(PDB);
function mapStateToProps(state) {
return {
tasks: state.tasksReducer.tasks,
projects: state.projectsReducer.projects,
selectedProjectId: state.projectsReducer.selectedProjectId,
};
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
navigation

View File

@ -0,0 +1,25 @@
import React from "react";
//import { StaticRouter } from "react-router-dom/server";
import { Link } from "react-router-dom";
import Login from "./login";
// Function to check if the user is authenticated (You can implement this based on your authentication method)
const PrivateRoute = ({ component: Component }) => {
const isAuthenticated = () => {
// Check if the user is authenticated (e.g., check for a valid authentication token in local storage)
return localStorage.getItem("authToken") !== null;
};
return isAuthenticated() ? (
<Link component={Component} to="/dashboard">
{" "}
Dashboard{" "}
</Link>
) : (
<Link component={Login} to="/login">
{" "}
Login{" "}
</Link>
);
};
export default PrivateRoute;

17
components/project.jsx Normal file
View File

@ -0,0 +1,17 @@
import React from "react";
import { connect } from "react-redux";
import cx from "classnames";
import { selectProject } from "../redux/actions";
const Task = ({ id, status, assigned2, projectName, description }) => (
<li className="project-item">
<span>
{status} {assigned2} {project} {description}
</span>
</li>
);
export default connect(null, { projectSelected })(Task);

View File

@ -0,0 +1,54 @@
import React from "react";
import { connect } from "react-redux";
import Box from '@mui/material/Box';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
function ProjectList(props) {
function handleSelectProject(id) {
props.dispatch({ type: "SELECT_PROJECT", id });
}
const loadedProjects = props.projects
const loadedProjectID = props.selectedProjectId
console.log(loadedProjects)
return (
<>
<div>
<Box sx={{ minWidth: 100 }}>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">Projects</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
// value={JSON.stringify(loadedProjectID)}
label="Projects"
>
{loadedProjects.map(({ projectId, projectName }) => (
<MenuItem
key={projectId}
onClick={() => handleSelectProject(projectId)}
value={projectName}
// className={loadedProjectID === projectId ? "selected" : ""}
>
{projectName}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
</div>
</>
);
}
export default connect(mapStateToProps)(ProjectList);
function mapStateToProps(state) {
return {
projects: state.projectsReducer.projects,
selectedProjectId: state.projectsReducer.selectedProjectId,
};
}

0
components/routes.jsx Normal file
View File

24
components/task.jsx Normal file
View File

@ -0,0 +1,24 @@
import React from "react";
import { connect } from "react-redux";
import cx from "classnames";
import { toggleCompleted } from "../redux/actions";
const Task = ({ taskId, completed, assigned2, taskName, description, parentIds, status }) => (
<li className="task-item" onClick={() => toggleCompleted(id)}>
{"["} {completed ? true : false } {"]"}
<span
className={cx(
"task-item__text",
completed && "task-item__text--completed"
)}
>
{taskId} {assigned2} {task} {description} {parentIds} {status}
</span>
</li>
);
export default connect(null, { toggleCompleted })(Task);
// {(item.assigned2, item.task, item.description)}

226
components/taskList.jsx Normal file
View File

@ -0,0 +1,226 @@
import React from "react";
import { connect } from "react-redux";
import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import Paper from "@mui/material/Paper";
import { styled } from "@mui/material/styles";
import PouchDB from "pouchdb";
import { v4 as uuidv4 } from "uuid";
function TaskList(props) {
// ToDo: Setup alerting for various actions through MaterialToas
const project = props.projects.find(
(project) => project.projectId === props.selectedProjectId
);
const currentProject = project.projectId;
const currentProjectStr = JSON.stringify(project.projectId);
const projectLink = "http://192.168.0.234:5984/" + currentProject;
// Setup the data persistence
const localTasksDB = new PouchDB(currentProjectStr);
const remoteTasksDB = new PouchDB(projectLink, {
auth: {
username: "ditswbsclient",
password: "bluebeard",
},
});
const syncHandler = localTasksDB
.sync(remoteTasksDB, {
live: true,
retry: true,
})
.on("paused", (info) => {
console.log("Task replication paused");
})
.on("active", (info) => {
console.log("Task replication resumed");
})
.on("change", (change) => {
console.log("Task change detected");
});
const handleAddTask = () => {
const taskName = prompt("Enter a new task name:");
const description = prompt("Enter description for the task");
const uniqueTaskId = uuidv4();
console.log(uniqueTaskId);
const commitToPDB = () => {
console.log("entering commit to pouchdb");
try {
const response = localTasksDB.put({
_id: uniqueTaskId,
taskId: uniqueTaskId,
taskName: taskName,
description: description,
status: "not started",
parentIds: [project.projectId],
completed: false,
});
console.log("added task", taskName);
} catch (err) {
console.log(err);
}
};
const commitToRedux = () => {
console.log("entering commit to redux");
try {
if (taskName) {
props.dispatch({
type: "ADD_TASK",
taskId: uniqueTaskId,
taskName: taskName,
description: description,
status: "not started",
parentIds: [project.projectId],
completed: false,
});
}
} catch (error) {
console.error("Could not add task to redux", error);
}
return () => {
console.log("Task added to local store!");
};
};
commitToRedux();
commitToPDB();
};
function compareArraysAndFilterChanges(oldArray, newArray) {
const changedTasks = [];
oldArray.forEach((oldItem, index) => {
const newItem = newArray[index];
// Compare items based on your criteria
if (!isEqual(oldItem, newItem)) {
changedTasks.push(newItem);
}
});
return changedTasks;
}
function isEqual(a, b) {
// Implement your own comparison logic here
// This can be a deep equality check using a library like lodash or a custom function
return (
JSON.stringify(a.completed) === JSON.stringify(b.completed) ||
JSON.stringify(a.status) === JSON.stringify(b.status) ||
JSON.stringify(a.description) === JSON.stringify(b.description) ||
JSON.stringify(a.taskName) === JSON.stringify(b.taskName)
);
}
//Handle modifications to tasks
const handleCommitChangesToPDB = () => {
//identify if any field changed in
//list of tasks in current redux state
try {
const filteredTasks = props.tasks.filter(
(task) => task.parentIds[0] === project.projectId
);
localTasksDB
.allDocs({ include_docs: true })
.then((result) => {
const existingTasks = result.rows.map((row) => row.doc);
const uniqueChangedTasks = compareArraysAndFilterChanges(existingTasks,filteredTasks);
console.log("changes", uniqueChangedTasks);
if (uniqueChangedTasks !== undefined) {
console.log("task change detected");
uniqueChangedTasks.map((u) => {
localTasksDB.get(u.taskId).then(function(doc) {
return localTasksDB.put({
_id: u.taskId,
_rev: doc._rev,
taskId: u.taskId,
taskName: u.taskName,
description: u.description,
status: u.status,
parentIds: u.parentIds,
completed: u.completed,
});
}).then(function(response) {
console.log("Some tasks have been changed ")
}).catch(function (err) {
console.log(err);
});
});
} else {
console.log("no task change detected");
}
})
.then((result) => {
console.log("Data change is successful.", result);
})
.catch((error) => {
console.error("Error during change staging:", error);
});
} catch (error) {
console.error("Error during data committement from staging: ", error);
}
return () => console.log("unmounting...");
};
const handleToggleTask = (id) => {
props.dispatch({
type: "TOGGLE_COMPLETED",
id,
});
};
const Item = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === "dark" ? "#1A2027" : "#fff",
...theme.typography.body2,
padding: theme.spacing(1),
textAlign: "center",
color: theme.palette.text.secondary,
}));
const filterByParentId = (taskList) => {
return taskList
.map((task) => {
console.log(props.selectedProjectId);
if (task.parentIds[0] === props.selectedProjectId) {
return (
<div>
<Box sx={{ flexGrow: 1 }}>
<Grid item xs="auto" key={task.taskId}>
<input
type="checkbox"
checked={task.completed}
onChange={() => handleToggleTask(task.taskId)}
/>
<Item key={task.taskId} value={task.taskName}>
{task.taskName}
</Item>
</Grid>
</Box>
</div>
);
}
return null;
})
.filter(Boolean);
};
return (
<div>
<ul>{filterByParentId(props.tasks)}</ul>
<button onClick={handleAddTask}>Add Task</button>
<button onClick={handleCommitChangesToPDB}>
Commit Changes To Tasks
</button>
</div>
);
}
export default connect(mapStateToProps)(TaskList);
function mapStateToProps(state) {
return {
tasks: state.tasksReducer.tasks,
selectedProjectId: state.projectsReducer.selectedProjectId,
projects: state.projectsReducer.projects,
};
}

46
components/test Normal file
View File

@ -0,0 +1,46 @@
www
task list
import React from "react";
import Task from "./task";
import { connect } from "react-redux";
import { getTasksByVisibilityFilter } from "../redux/selector";
import AddTask from "./addTask";
import VisibilityFilters from "./visibilityFilters";
//import { toHaveDescription } from "@testing-library/jest-dom/dist/matchers";
const TaskList = ({ Tasks }) => (
<>
<div>
<AddTask />
</div>
<ul className="task-list">
{ Tasks && Tasks.length ? Tasks.map((item, index) => {
return <Task key={item.id} id={item.id} completed={item.completed} assigned2={item.assigned2} task={item.task} description={item.description} />;
})
: "List empty"}
</ul>
<VisibilityFilters />
</>
);
//
const mapStateToProps = (state) => {
const { visibilityFilter } = state;
const Tasks = getTasksByVisibilityFilter(state, visibilityFilter);
return { Tasks };
};
export default connect(mapStateToProps)(TaskList);
// return ;Tasks ?

39
components/viewDocs.jsx Normal file
View File

@ -0,0 +1,39 @@
import { useState, useEffect } from "react";
import db from "./ldbcfg";
import ModifyPDB from "./modifypdb";
const ShowDocs = () => {
let [flatListItems, setFlatListItems] = useState([]);
useEffect(() => {
db.allDocs({ include_docs: true, descending: true })
.then((results) => {
let temp = results.rows.map((row) => row.doc);
setFlatListItems(temp);
})
.catch((err) => alert("Unable to get data"));
}, [flatListItems]);
const handleClick = (doc) => {
<ModifyPDB />
console.log({doc})
}
return (
<>
<ModifyPDB />
<ul>
{flatListItems.map(doc => (
<li key={doc._id} onClick={() => handleClick({doc})} > {doc.task} {doc.description} {doc.assigned2} </li>
)) }
</ul>
</>
);
};
export default ShowDocs;

View File

@ -0,0 +1,38 @@
import React from "react";
import cx from "classnames";
import { connect } from "react-redux";
import { setFilter } from "../redux/actions";
import { VISIBILITY_FILTERS } from "../constants";
const VisibilityFilters = ({ activeFilter, setFilter }) => {
return (
<div className="visibility-filters">
{Object.keys(VISIBILITY_FILTERS).map(filterKey => {
const currentFilter = VISIBILITY_FILTERS[filterKey];
return (
<span
key={`visibility-filter-${currentFilter}`}
className={cx(
"filter",
currentFilter === activeFilter && "filter--active"
)}
onClick={() => {
setFilter(currentFilter);
}}
>
{currentFilter}
</span>
);
})}
</div>
);
};
const mapStateToProps = state => {
return { activeFilter: state.visibilityFilter };
};
// export default VisibilityFilters;
export default connect(
mapStateToProps,
{ setFilter }
)(VisibilityFilters);

5
configs/admincfgs.jsx Normal file
View File

@ -0,0 +1,5 @@
const admconfig = {
ditswbsApiUrl: process.env.REACT_APP_DITSWBS_API_URL,
};
export default admconfig;

8
constants.jsx Normal file
View File

@ -0,0 +1,8 @@
export const VISIBILITY_FILTERS = {
ALL: " all ",
COMPLETED: true,
INCOMPLETE: false
};
export const AUTH_API = {
GET_TOKEN = "http://192.168.0.234:8000/api/token/"
};

1
ditswbs.env Normal file
View File

@ -0,0 +1 @@
REACT_APP_DITSWBS_API_URL=http://192.168.0.234

13
index.css Normal file
View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

25
index.jsx Normal file
View File

@ -0,0 +1,25 @@
import React from "react";
import ReactDOM from "react-dom/client";
import reportWebVitals from "./reportWebVitals";
import "./index.css";
import "bootstrap/dist/css/bootstrap.min.css";
import App from "./App";
import { Provider } from "react-redux";
import { store } from "./redux/store";
import LoadDB from "./components/ldbcfg";
import { BrowserRouter } from "react-router-dom";
ReactDOM.createRoot(document.getElementById("root")).render(
// <React.StrictMode>
<Provider store={store}>
<LoadDB />
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
// </React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

11
redux/actionTypes.jsx Normal file
View File

@ -0,0 +1,11 @@
export const ADD_TASK = "ADD_TASK";
export const ADD_CDB_TASK = "ADD_CDB_TASK";
export const ADD_TASK_PARENT = "ADD_TASK_PARENT";
export const DELETE_TASK = "DELETE_TASK";
export const TOGGLE_COMPLETED = "TOGGLE_COMPLETED";
export const SET_FILTER = "SET_FILTER";
export const ADD_PROJECT = "ADD_PROJECT";
export const DELETE_PROJECT = "DELETE_PROJECT";
export const ARCHIVE_PROJECT = "ARCHIVE_PROJECT";
export const SELECT_PROJECT = "SELECT_PROJECT";

66
redux/actions.jsx Normal file
View File

@ -0,0 +1,66 @@
import {
ADD_TASK,
ADD_CDB_TASK,
ADD_TASK_PARENT,
TOGGLE_COMPLETED,
SET_FILTER,
SELECT_PROJECT,
ADD_PROJECT,
DELETE_PROJECT,
ARCHIVE_PROJECT,
DELETE_TASK,
} from "./actionTypes";
export const addTask = (taskName) => ({
type: ADD_TASK,
payload: { taskName },
});
export const addCdbTask = (taskName) => ({
type: ADD_CDB_TASK,
payload: { taskName },
});
export const addTaskParent = (id) => ({
type: ADD_TASK_PARENT,
payload: { id },
});
export const deleteTask = (id) => ({
type: DELETE_TASK,
payload: { id },
});
export const toggleCompleted = (id) => ({
type: TOGGLE_COMPLETED,
payload: { id },
});
export const setFilter = (filter) => ({
type: SET_FILTER,
payload: { filter },
});
export const addProject = (projectName) => ({
type: ADD_PROJECT,
payload: { projectName },
});
export const deleteProject = (id) => ({
type: DELETE_PROJECT,
payload: { id },
});
export const selectProject = (id) => ({
type: SELECT_PROJECT,
payload: { id },
});
export const archiveProject = (id) => ({
type: ARCHIVE_PROJECT,
payload: { id },
});

15
redux/connect.jsx Normal file
View File

@ -0,0 +1,15 @@
const mapStateToProps = (state, ownProps) => ({
// ... computed data from state and optionally ownProps
})
const mapDispatchToProps = {
// ... normally is an object full of action creators
}
// `connect` returns a new function that accepts the component to wrap:
const connectToStore = connect(mapStateToProps, mapDispatchToProps)
// and that function returns the connected, wrapper component:
const ConnectedComponent = connectToStore(Component)
// We normally do both in one step, like this:
connect(mapStateToProps, mapDispatchToProps)(Component)

6
redux/reducers/index.jsx Normal file
View File

@ -0,0 +1,6 @@
import { combineReducers } from "redux";
import visibilityFilter from "./visibilityFilter";
import tasksReducer from "./tasksReducer";
import projectsReducer from "./projectsReducer";
export default combineReducers({ tasksReducer, projectsReducer, visibilityFilter });

View File

@ -0,0 +1,55 @@
import {
ADD_PROJECT,
SELECT_PROJECT,
ARCHIVE_PROJECT,
} from "../actionTypes";
const initialState = {
projects: [],
selectedProjectId: "0",
};
export default function projectReducer(state = initialState, action) {
switch (action.type) {
// Projects are added only during initialization of the program
// New projects and resources it can use are defined outside of the client program
// Project initiation and closure are considered a separate endeavors
// to project planning, execution, and control in this application.
case ADD_PROJECT: {
return {
...state,
projects: [
...state.projects,
{
projectId: action.project.projectId,
projectName: action.project.projectName,
description: action.project.projectDescription,
},
],
selectedProjectId: action.project.projectId,
};
}
// This is an important switch as everything user does in the session is tied to
// selected project. Selected project must always be prominently displayed in every
// view.
case SELECT_PROJECT: {
return {
...state,
projects: [...state.projects],
selectedProjectId: action.id,
};
}
// Default action when project is closed. Not yet implemented. Archived projects are just
// moved to another view. Project deletion can only be done through admin program and will
// only become noticeable to user after client program restart.
case ARCHIVE_PROJECT: {
return {
...state,
projects: state.projects.map((project) => project.id === action.id),
selectedProjectId: 0,
};
}
default:
return state;
}
}

View File

@ -0,0 +1,61 @@
import { ADD_TASK, ADD_CDB_TASK, TOGGLE_COMPLETED } from "../actionTypes";
import { v4 as uuidv4 } from "uuid";
const initialState = { tasks: [] };
export default function taskReducer(state = initialState, action) {
switch (action.type) {
// Used when task is added during session but not committed to cloud
case ADD_TASK: {
return {
...state,
tasks: [
...state.tasks,
{
taskId: action.taskId,
taskName: action.taskName,
description: action.description,
status: action.status,
parentIds: action.parentIds,
completed: action.completed,
},
],
};
}
// Used during initial synchronization of the local storage using cloud data
// a local pouchdb database is also created into which task commits can be done
// during initialization of the database
case ADD_CDB_TASK: {
return {
...state,
tasks: [
...state.tasks,
{
_id: action.task.taskId,
taskId: action.task.taskId,
taskName: action.task.taskName,
description: action.task.description,
status: action.task.status,
parentIds: action.task.parentIds,
completed: action.task.completed,
},
],
};
}
// Toggle completed
case TOGGLE_COMPLETED: {
console.log(action.id);
return {
...state,
tasks: state.tasks.map((task) =>
task.taskId === action.id
? { ...task, completed: !task.completed }
: task
),
};
}
default:
return state;
}
}

View File

@ -0,0 +1,17 @@
import { SET_FILTER } from "../actionTypes";
import { VISIBILITY_FILTERS } from "../../constants";
const initialState = VISIBILITY_FILTERS.ALL;
const visibilityFilter = (state = initialState, action) => {
switch (action.type) {
case SET_FILTER: {
return action.payload.filter;
}
default: {
return state;
}
}
};
export default visibilityFilter;

27
redux/selector.jsx Normal file
View File

@ -0,0 +1,27 @@
import { VISIBILITY_FILTERS } from "../constants";
export const getTasksState = (store) => store.tasks;
export const getTaskList = (store) =>
getTasksState(store) ? getTasksState(store).allIds : [];
export const getTasksById = (store, id) =>
getTasksState(store) ? { ...getTasksState(store).byIds[id], id } : {};
export const getTasks = (store) =>
getTaskList(store).map((id) => getTasksById(store, id));
export const getTasksByVisibilityFilter = (store, visibilityFilter) => {
const allTasks = getTasks(store);
switch (visibilityFilter) {
case VISIBILITY_FILTERS.COMPLETED:
console.log("contains completed tasks");
return allTasks.filter((task) => task.completed);
case VISIBILITY_FILTERS.INCOMPLETE:
console.log("contains uncompleted tasks");
return allTasks.filter((task) => !task.completed);
case VISIBILITY_FILTERS.ALL:
default:
return allTasks;
}
};

10
redux/store.jsx Normal file
View File

@ -0,0 +1,10 @@
import { createStore } from "redux";
import rootReducer from "./reducers";
export const store = createStore(rootReducer, /* preLoadedState, */
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

View File

@ -0,0 +1,12 @@
import { connect } from "react-redux";
import { setFilter } from "../redux/actions";
const VisibilityFilters = // ... component implementation
const mapStateToProps = state => {
return { activeFilter: state.visibilityFilter };
};
export default connect(
mapStateToProps,
{ setFilter }
)(VisibilityFilters);

13
reportWebVitals.js Normal file
View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
setupTests.js Normal file
View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';