Browse Source

chore: cleanup

master
Garrit Franke 3 years ago
parent
commit
f92ae04720
Signed by: garrit
GPG Key ID: 65586C4DDA55EA2C
  1. 2
      .dockerignore
  2. 25
      client/Dockerfile
  3. 21
      client/LICENSE
  4. 7
      client/README.md
  5. 51
      client/app.js
  6. 13
      client/babel.config.json
  7. 14
      client/conf/conf.d/default.conf
  8. 3
      client/contexts/UserContext.js
  9. 37773
      client/package-lock.json
  10. 36
      client/package.json
  11. 7
      client/public/bootstrap.min.css
  12. 7
      client/public/bootstrap.min.js
  13. BIN
      client/public/favicon.ico
  14. 16
      client/public/index.html
  15. 5540
      client/public/jquery.min.js
  16. 30
      client/public/styles.css
  17. 18
      client/routes/auth.js
  18. 62
      client/routes/clients.js
  19. 7
      client/routes/index.js
  20. 7
      client/routes/landing.js
  21. 50
      client/service/ClientService.js
  22. 40
      client/service/UserService.js
  23. 14
      client/service/api-client.js
  24. 103
      client/src/components/App.js
  25. 35
      client/src/components/Breadcrumbs.js
  26. 101
      client/src/components/Game.js
  27. 1
      client/src/components/Header.jsx
  28. 5
      client/src/components/Landing.jsx
  29. 3
      client/src/components/Login.jsx
  30. 0
      client/src/components/Register.jsx
  31. 28
      client/src/components/Sidebar.js
  32. 28
      client/src/components/StatusTimeline.js
  33. 3
      client/src/index.js
  34. 72
      client/src/pages/ClientPage.js
  35. 76
      client/src/pages/ClientsListPage.js
  36. 73
      client/src/pages/NewClientPage.js
  37. 5
      client/src/pages/ProjectsListPage.js
  38. 61
      client/src/service/ClientService.ts
  39. 6
      client/src/util/statusTagMap.json
  40. 13
      client/views/404.jsx
  41. 47
      client/views/Index.jsx
  42. 30
      client/views/Landing.jsx
  43. 44
      client/views/Login.jsx
  44. 53
      client/views/Register.jsx
  45. 46
      client/views/clients/Detail.jsx
  46. 39
      client/views/clients/Index.jsx
  47. 74
      client/views/clients/New.jsx
  48. 29
      client/views/clients/Timeline.jsx
  49. 73
      client/views/components/ClientDetailHeader.jsx
  50. 57
      client/views/layouts/Header.jsx
  51. 25
      client/views/layouts/Main.jsx

2
.dockerignore

@ -0,0 +1,2 @@
node_modules/
README.md

25
client/Dockerfile

@ -1,13 +1,16 @@
FROM node:15.0.1-alpine3.11
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
# build environment
FROM node:13.12.0-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json ./
COPY package-lock.json ./
RUN npm ci --silent
RUN npm install react-scripts@3.4.1 -g --silent
COPY . ./
RUN npm run build
# production environment
FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
EXPOSE 443
CMD [ "npm", "start" ]
CMD ["nginx", "-g", "daemon off;"]

21
client/LICENSE

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 garritfra
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.

7
client/README.md

@ -1,6 +1,3 @@
## Configuration
# react-parcel-boilerplate
To configure the client, you need these fields in your `.env` file:
- API_BASE_PATH - Backend URL
- FRONTEND_BASE_PATH - Frontend URL
A minimal react boilerplate that uses parcel to bundle all of your assets

51
client/app.js

@ -1,51 +0,0 @@
const express = require("express");
const jwt = require("jsonwebtoken");
const fs = require("fs");
const app = express();
require("dotenv").config();
app.use("/", express.static(__dirname + "/public"));
app.set("views", __dirname + "/views");
app.set("view engine", "jsx");
app.engine(
"jsx",
require("express-react-views").createEngine({
babel: {
presets: [
"@babel/preset-react",
["@babel/preset-env", { targets: { node: "current" } }],
],
},
plugins: ["@babel/plugin-proposal-object-rest-spread"],
})
);
app.use(require("cookie-parser")());
// Attach user
app.use((req, res, next) => {
const token = req.cookies.token;
req.user = { ...jwt.decode(token), token };
next();
});
app.use("/landing", require("./routes/landing"));
app.use("/auth", require("./routes/auth"));
// Redirect if not authenticated
app.use((req, res, next) => {
if (!req.user.token) return res.redirect("/landing");
next();
});
app.use("/", require("./routes/index"));
app.use("/clients", require("./routes/clients"));
app.get("/*", (req, res) => {
res.render("404");
});
app.listen(process.env.PORT || 80);

13
client/babel.config.json

@ -0,0 +1,13 @@
{
"presets": [
[
"@babel/preset-react",
{
"targets": {
"node": "current"
}
}
],
"@babel/preset-env"
]
}

14
client/conf/conf.d/default.conf

@ -1,14 +0,0 @@
server {
listen 80;
server_name localhost;
location / {
if (!-e $request_filename){
rewrite http://$server_name break;
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}

3
client/contexts/UserContext.js

@ -1,3 +0,0 @@
const { createContext } = require("react");
module.exports = createContext({});

37773
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

36
client/package.json

@ -4,33 +4,31 @@
"description": "React Parcel Boilerplate",
"main": "index.js",
"scripts": {
"start": "NODE_ENV=production node app.js",
"start:dev": "nodemon app.js",
"build": "sh ./build.sh",
"start": "parcel public/index.html",
"build": "npm test && parcel build public/index.html",
"test": "jest --coverage"
},
"author": "Garrit Franke",
"license": "MIT",
"dependencies": {
"@types/node": "^14.14.6",
"@types/jest": "^24.0.22",
"antd": "^4.16.6",
"axios": "^0.21.1",
"cookie-parser": "^1.4.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-react-views": "^0.11.0",
"jsonwebtoken": "^8.5.1",
"moment": "^2.29.1",
"nodemon": "^2.0.6",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-p5": "^1.3.19",
"react-router-dom": "^5.2.0",
"styled-components": "^4.4.1"
"styled-components": "^5.3.0"
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"@babel/preset-react": "^7.12.1",
"@babel/preset-typescript": "^7.12.1"
"@babel/core": "^7.14.6",
"@babel/preset-env": "^7.14.7",
"@babel/preset-react": "^7.14.5",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.2",
"babel-jest": "^27.0.5",
"enzyme": "^3.10.0",
"jest": "^27.0.5",
"node-sass": "^6.0.1",
"parcel": "^2.0.0-beta.2"
}
}

7
client/public/bootstrap.min.css vendored

File diff suppressed because one or more lines are too long

7
client/public/bootstrap.min.js vendored

File diff suppressed because one or more lines are too long

BIN
client/public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

16
client/public/index.html

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>My React App</title>
</head>
<body>
<div id="root" />
<script type="module" src="../src/index.js"></script>
</body>
</html>

5540
client/public/jquery.min.js vendored

File diff suppressed because it is too large Load Diff

30
client/public/styles.css

@ -1,30 +0,0 @@
ul.timeline {
list-style-type: none;
position: relative;
}
ul.timeline:before {
content: " ";
background: #d4d9df;
display: inline-block;
position: absolute;
left: 29px;
width: 2px;
height: 100%;
z-index: 400;
}
ul.timeline > li {
margin: 20px 0;
padding-left: 20px;
}
ul.timeline > li:before {
content: " ";
background: white;
display: inline-block;
position: absolute;
border-radius: 50%;
border: 3px solid #22c0e8;
left: 20px;
width: 20px;
height: 20px;
z-index: 400;
}

18
client/routes/auth.js

@ -1,18 +0,0 @@
const router = require("express").Router();
const axios = require("axios");
const basePath = process.env.API_BASE_PATH;
router.get("/login", (req, res) => {
res.render("Login");
});
router.get("/register", (req, res) => {
res.render("Register");
});
router.get("/logout", (req, res) => {
res.clearCookie("token").redirect("/");
});
module.exports = router;

62
client/routes/clients.js

@ -1,62 +0,0 @@
const router = require("express").Router();
const axios = require("axios");
const basePath = process.env.API_BASE_PATH;
router.get("/", async (req, res) => {
const clients = await axios
.get(basePath + "/clients", {
headers: { Authorization: "Bearer " + req.cookies.token },
})
.then((response) => response.data)
.then((clients) =>
clients.map((c) => {
return { ...c, id: c._id };
})
);
res.render("clients/Index", {
clients,
user: req.user,
});
});
router.get("/new", async (req, res) => {
res.render("clients/New", {
user: req.user,
});
});
router.get("/:id", async (req, res) => {
const client = await axios
.get(basePath + "/clients/" + req.params.id, {
headers: { Authorization: "Bearer " + req.cookies.token },
})
.then((res) => res.data);
if (res.status === 404) {
res.render("404");
} else {
res.render("clients/Detail", { user: req.user, client });
}
});
router.get("/:id/timeline", async (req, res) => {
const client = await axios
.get(basePath + "/clients/" + req.params.id, {
headers: { Authorization: "Bearer " + req.cookies.token },
})
.then((res) => res.data);
if (res.status === 404) {
res.render("404");
} else {
res.render("clients/Timeline", {
user: req.user,
events: client.events,
client,
});
}
});
module.exports = router;

7
client/routes/index.js

@ -1,7 +0,0 @@
const router = require("express").Router();
router.get("/", (req, res) => {
res.render("Index", { user: req.user });
});
module.exports = router;

7
client/routes/landing.js

@ -1,7 +0,0 @@
const router = require("express").Router();
router.get("/", (req, res) => {
res.render("Landing");
});
module.exports = router;

50
client/service/ClientService.js

@ -1,50 +0,0 @@
const axios = require("axios");
const basepath = process.env.API_BASE_PATH;
module.exports = {
getClients() {
return axios
.get(basepath + "/clients")
.then((res) => res.data)
.then((data) =>
data.map((client) => {
return { id: client._id, name: client.name };
})
);
},
addClient(client) {
return axios
.post(basepath + "/clients", client)
.then((res) => res.data)
.then((client) => {
return { ...client, id: client._id };
});
},
getClientById(id) {
return axios
.get(basepath + "/clients/" + id)
.then((res) => res.data)
.then((client) => {
return { ...client, id: client._id };
});
},
updateStatus(id, status) {
return axios.post(basepath + "/clients/" + id + "/events", {
eventType: "status_changed",
value: status,
});
},
deleteMany(ids) {
console.log("To delete:", ids);
return axios({
method: "delete",
url: basepath + "/clients",
data: ids,
});
},
};

40
client/service/UserService.js

@ -1,40 +0,0 @@
const axios = require("axios");
const basepath = process.env.API_BASE_PATH;
module.exports = {
async getUser() {
console.debug("Getting user");
const response = await axios.get(basepath + "/auth/profile", {
headers: { Authorization: "Bearer " + document.cookie },
});
const data = response.data;
return {
id: data._id,
fullName: data.full_name,
email: data.email,
};
},
async login(email, password) {
const response = await axios.post(basepath + "/auth/login", {
email,
password,
});
const token = response.data.token;
document.cookie = "token=" + token;
return token;
},
async register(email, password, fullName) {
const response = await axios.post(basepath + "/auth/register", {
email,
password,
full_name: fullName,
});
const user = response.data;
return { id: user._id, fullName: user.full_name, email: user.email };
},
};

14
client/service/api-client.js

@ -1,14 +0,0 @@
/* Custom axios instance that adds the token to the headers, if existent */
const axios = require("axios");
const UserService = require("./UserService");
axios.interceptors.request.use(function (config) {
const token = UserService.getToken();
if (token) {
config.headers.Authorization = "Bearer " + token;
}
return config;
});
export default axios;

103
client/src/components/App.js

@ -1,79 +1,32 @@
import React, { useState } from "react";
import { Layout, Empty } from "antd";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import ClientsListPage from "../pages/ClientsListPage";
import RegisterPage from "../pages/RegisterPage";
import NewClientPage from "../pages/NewClientPage";
import LoginPage from "../pages/LoginPage";
import ClientPage from "../pages/ClientPage";
import Sidebar from "./Sidebar";
import React from "react";
import styled from "styled-components";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import Header from "./Header";
import Breadcrumbs from "./Breadcrumbs";
import ProjectsListPage from "../pages/ProjectsListPage";
const { Footer, Sider } = Layout;
export default function App() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
import Landing from "./Landing";
import Login from "./Login";
import Register from "./Register";
const onCollapse = (collapsed) => {
setSidebarCollapsed(collapsed);
};
const Container = styled.div`
text-align: center;
`;
return (
<Router>
<Layout style={{ minWidth: "100vw", minHeight: "100vh" }}>
<Sider collapsible collapsed={sidebarCollapsed} onCollapse={onCollapse}>
<div
style={{
height: "32px",
background: "rgba(255, 255, 255, 0.2)",
margin: "16px",
}}
/>
<Sidebar />
</Sider>
<Layout>
<Header />
<Breadcrumbs />
<Layout.Content
style={{
background: "#fff",
margin: "24px 16px",
padding: 24,
minHeight: 280,
}}
>
<Switch>
<Route exact path="/">
<Empty />
</Route>
<Route exact path="/clients">
<ClientsListPage />
</Route>
<Route exact path="/projects">
<ProjectsListPage />
</Route>
<Route exact path="/clients/new">
<NewClientPage />
</Route>
<Route
path="/clients/:id"
render={({ match }) => <ClientPage id={match.params.id} />}
/>
<Route exact path="/login">
<LoginPage />
</Route>
<Route path="/register">
<RegisterPage />
</Route>
</Switch>
</Layout.Content>
<Footer style={{ textAlign: "center" }}>
Omega CRM ©2020 Created by Garrit Franke
</Footer>
</Layout>
</Layout>
</Router>
);
export default function App() {
return (
<Container>
<BrowserRouter>
<Header />
<Switch>
<Route exact path="/login">
<Login />
</Route>
<Route exact path="/register">
<Register />
</Route>
<Route path="/">
<Landing />
</Route>
</Switch>
</BrowserRouter>
</Container>
);
}

35
client/src/components/Breadcrumbs.js

@ -1,35 +0,0 @@
import React from "react";
import { Breadcrumb } from "antd";
import { useLocation, Link } from "react-router-dom";
export default function Breadcrumbs() {
const location = useLocation();
const breadcrumbNameMap = {
"/": "Home",
"/clients": "Clients",
"/clients/new": "New",
"/clients/*": "Detail",
"/projects": "Projects",
};
const pathSnippets = location.pathname.split("/").filter((i) => i);
const extraBreadcrumbItems = pathSnippets.map((_, index) => {
const url = `/${pathSnippets.slice(0, index + 1).join("/")}`;
return (
<Breadcrumb.Item key={url}>
<Link to={url}>{breadcrumbNameMap[url]}</Link>
</Breadcrumb.Item>
);
});
const breadcrumbItems = [
<Breadcrumb.Item key="home">
<Link to="/">Home</Link>
</Breadcrumb.Item>,
].concat(extraBreadcrumbItems);
return (
<Breadcrumb style={{ marginLeft: "16px", marginTop: "24px" }}>
{breadcrumbItems}
</Breadcrumb>
);
}

101
client/src/components/Game.js

@ -0,0 +1,101 @@
import React from "react";
import Sketch from "react-p5";
let positions = [];
let initialPos;
let playerPath = [];
let stepX;
let stepY;
let numCols = 50;
let numRows = 25;
export default (props) => {
const setup = (p5, canvasParentRef) => {
p5.createCanvas(1000, 500).parent(canvasParentRef);
const setupGrid = () => {
stepX = p5.width / numCols; //to make sure they are evenly spaced, we divide the width and height by numberOfColumns
stepY = p5.height / numRows; //and numberOfRows respectively
for (let x = 0; x < p5.width; x += stepX) {
//start at the first column, where x = 0
for (let y = 0; y < p5.height; y += stepY) {
//go through all the rows (y = 0, y = yStep * 1, y = yStep * 2, etc.)
const p = p5.createVector(x, y); //we create a vector at this location
positions.push(p); // and then we put the vector into the array
}
//at the end of the inner for loop, we go back to the first loop, and we increment x
//now our column is going to b x = xStep*1, and we populate all the rows with the inner for loop
//and again, and again until x > width
}
};
const setupPlayer = () => {
initialPos = p5.createVector(5, 5);
playerPath.push(p5.createVector(1, 0));
playerPath.push(p5.createVector(1, 1));
playerPath.push(p5.createVector(1, 1));
playerPath.push(p5.createVector(0, 1));
};
setupGrid();
setupPlayer();
};
const draw = (p5) => {
const drawGrid = () => {
p5.push();
p5.fill(100, 100, 100);
p5.noStroke();
for (let i = 0; i < positions.length; i++) {
//go through all our positions
p5.ellipse(positions[i].x, positions[i].y, 2, 2); //put a circle at each of them
}
p5.pop();
};
const drawPlayer = () => {
p5.push();
p5.fill(255, 0, 0); // Red
p5.noStroke();
// Need to clone here to not override initial position
let currentPos = { ...initialPos };
// Draw initial position
p5.ellipse(currentPos.x * stepX, currentPos.y * stepY, 5, 5);
for (let pos of playerPath) {
const previous = { ...currentPos };
currentPos.x += pos.x;
currentPos.y += pos.y;
p5.push();
p5.stroke(150);
p5.line(previous.x * stepX, previous.y * stepY, currentPos.x * stepX, currentPos.y * stepY);
p5.pop();
p5.ellipse(currentPos.x * stepX, currentPos.y * stepY, 5, 5);
}
p5.pop();
};
drawGrid();
drawPlayer();
};
return <Sketch setup={setup} draw={draw} />;
};

1
client/src/components/Header.js → client/src/components/Header.jsx

@ -4,6 +4,7 @@ import { Layout, Menu, notification } from "antd";
import UserService from "../service/UserService";
const { Header } = Layout;
export default function Head() {
const [username, setUsername] = useState("");
const token = UserService.getToken();

5
client/src/components/Landing.jsx

@ -0,0 +1,5 @@
import React from "react";
const Landing = () => <h1>Landing</h1>
export default Landing;

3
client/src/pages/LoginPage.js → client/src/components/Login.jsx

@ -1,7 +1,6 @@
import React, { useState } from "react";
import { Form, Input, Button, Alert } from "antd";
import UserService from "../service/UserService";
import ErrorBoundary from "antd/lib/alert/ErrorBoundary";
import { useHistory } from "react-router-dom";
export default function LoginPage() {
@ -26,7 +25,7 @@ export default function LoginPage() {
return (
<>
{error && <Alert message={"Login failed:" + error} type="error" />}
{error && <Alert message={"Login faild:" + error} type="error" />}
<br />
<Form onFinish={onSubmit} {...layout}>
<Form.Item label="Email" name="email">

0
client/src/pages/RegisterPage.js → client/src/components/Register.jsx

28
client/src/components/Sidebar.js

@ -1,28 +0,0 @@
import { Menu } from "antd";
import React, { useEffect } from "react";
import { Link, useLocation } from "react-router-dom";
/**
*
* @param {{activeItem: string}} activeItem
*/
export default function Sidebar() {
const location = useLocation();
return (
<Menu
defaultSelectedKeys={[location.pathname.split("/")[1] || "home"]}
mode="inline"
theme="dark"
>
<Menu.Item key="home">
<Link to="/">Home</Link>
</Menu.Item>
<Menu.Item key="clients">
<Link to="/clients">Clients</Link>
</Menu.Item>
<Menu.Item key="projects">
<Link to="/projects">Projects</Link>
</Menu.Item>
</Menu>
);
}

28
client/src/components/StatusTimeline.js

@ -1,28 +0,0 @@
import React from "react";
import { Timeline, Tag } from "antd";
import statusTagMap from "../util/statusTagMap.json";
export default function StatusTimeline({ events }) {
console.log(events);
const eventItems = events.reverse().map((event) => {
switch (event.eventType) {
case "status_changed":
return (
<Timeline.Item label={new Date(event.createdAt).toUTCString()}>
Status:{" "}
<Tag color={statusTagMap[event.value].color}>{event.value}</Tag>
</Timeline.Item>
);
case "created":
return (
<Timeline.Item label={new Date(event.createdAt).toUTCString()}>
Created
</Timeline.Item>
);
default:
break;
}
});
return <Timeline mode="left">{eventItems}</Timeline>;
}

3
client/src/index.js

@ -1,6 +1,7 @@
import React from "react";
import React, { Component } from "react";
import ReactDOM from "react-dom";
import App from "../src/components/App";
import "antd/dist/antd.css";
require("dotenv").config();

72
client/src/pages/ClientPage.js

@ -1,72 +0,0 @@
import React, { useState, useEffect } from "react";
import { Select, Tag, Space, List, Typography, Row, Col } from "antd";
import { useHistory } from "react-router";
import ClientService from "../service/ClientService";
import StatusTimeline from "../components/StatusTimeline";
export default function ClientPage({ id }) {
const [client, setClient] = useState({});
const history = useHistory();
useEffect(() => {
ClientService.getClientById(id).then((client) => {
console.log(client);
setClient(client);
});
}, []);
const updateStatus = (value) => {
ClientService.updateStatus(client.id, value).then(() => {
setClient({ ...client, status: value });
history.go(0);
});
};
return (
<>
<Space direction="vertical" size="large">
<Typography.Title level={4}>Client Info</Typography.Title>
<Row gutter={16}>
<Col>
<List bordered split="true">
<List.Item actions={[client.name]}>Name:</List.Item>
<List.Item actions={[client.id]}>Identifier:</List.Item>
<List.Item
actions={[
(() => (
<Select value={client.status} onSelect={updateStatus}>
<Select.Option value="potential">
<Tag color="default">Potential</Tag>
</Select.Option>
<Select.Option value="active">
<Tag color="success">Active</Tag>
</Select.Option>
<Select.Option value="on_hold">
<Tag color="purple">On Hold</Tag>
</Select.Option>
<Select.Option value="inactive">
<Tag color="error">Inactive</Tag>
</Select.Option>
</Select>
))(),
]}
>
Status:
</List.Item>
<List.Item actions={[client.email]}>Email:</List.Item>
<List.Item actions={[client.address]}>Address:</List.Item>
<List.Item actions={[client.telephone]}>Telephone:</List.Item>
</List>
</Col>
</Row>
<Row>
<Col span={24}>
{client.events ? <StatusTimeline events={client.events} /> : <></>}
</Col>
<Col span={8}></Col>
</Row>
</Space>
</>
);
}

76
client/src/pages/ClientsListPage.js

@ -1,76 +0,0 @@
import React, { useState, useEffect } from "react";
import { Table, Button, Space } from "antd";
import { Link, useHistory } from "react-router-dom";
import { UserAddOutlined } from "@ant-design/icons";
import ClientService from "../service/ClientService";
export default function ClientsPage() {
const history = useHistory();
const [clients, setClients] = useState([]);
const [selectedClients, setSelectedClients] = useState([]);
const [actionsVisible, setActionsVisible] = useState(false);
useEffect(() => {
ClientService.getClients().then(setClients);
}, []);
const onSelectChange = (selectedRowKeys) => {
setSelectedClients(selectedRowKeys);
if (selectedRowKeys.length >= 1) {
setActionsVisible(true);
} else {
setActionsVisible(false);
}
};
const columns = [
{
title: "Name",
dataIndex: "id",
render: (id) => (
<Link to={"/clients/" + id}>
{
/* TODO: is there a simpler way? */
clients.filter((c) => c.id == id)[0].name
}
</Link>
),
},
];
return (
<div>
<Space direction="horizontal" style={{ marginBottom: 16 }}>
<Button
onClick={() => history.push("/clients/new")}
type="primary"
icon={<UserAddOutlined />}
>
Add
</Button>
<Button
type="primary"
danger
disabled={!actionsVisible}
onClick={() => {
ClientService.deleteMany(selectedClients).then(() =>
window.location.reload()
);
}}
>
Delete
</Button>
</Space>
<Table
rowSelection={{
type: "checkbox",
onChange: onSelectChange,
}}
columns={columns}
dataSource={clients.map((client) => {
return { ...client, key: client.id };
})}
/>
</div>
);
}

73
client/src/pages/NewClientPage.js

@ -1,73 +0,0 @@
import React from "react";
import { Form, Input, Button, Row, Col, Select, Tag } from "antd";
import ClientService from "../service/ClientService";
import { useHistory } from "react-router-dom";
const { Option } = Select;
export default function NewClientPage() {
const history = useHistory();
const onSubmit = (values) => {
ClientService.addClient(values).then((client) =>
history.push("/clients/" + client.id)
);
};
const tailLayout = {
wrapperCol: { offset: 8, span: 4 },
};
return (
<>
<Form onFinish={onSubmit}>
<Row gutter={16}>
<Col>
<Form.Item label="Name" name="name" required="true">
<Input type="name"></Input>
</Form.Item>
</Col>
<Col>
<Form.Item label="Email" name="email">
<Input type="email"></Input>
</Form.Item>
</Col>
<Col>
<Form.Item label="status" name="status">
<Select defaultValue="">
<Option value="">Pick</Option>
<Option value="potential">
<Tag color="default">Potential</Tag>
</Option>
<Option value="active">
<Tag color="success">Active</Tag>
</Option>
<Option value="on_hold">
<Tag color="purple">On Hold</Tag>
</Option>
<Option value="inactive">
<Tag color="error">Inactive</Tag>
</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col>
<Form.Item label="Address" name="address">
<Input.TextArea></Input.TextArea>
</Form.Item>
</Col>
<Col>
<Form.Item label="Telephone" name="telephone">
<Input type="tel"></Input>
</Form.Item>
</Col>
</Row>
<Form.Item {...tailLayout}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</>
);
}

5
client/src/pages/ProjectsListPage.js

@ -1,5 +0,0 @@
import React from "react";
export default function ProjectsListPage() {
return <p>Projects...</p>;
}

61
client/src/service/ClientService.ts

@ -1,61 +0,0 @@
import axios from "./api-client";
const basepath = process.env.API_BASE_PATH;
export interface Client {
id: string;
name: string;
}
export interface NewClient {
name: string;
}
export default {
getClients(): Promise<Client[]> {
return axios
.get(basepath + "/clients")
.then((res) => res.data)
.then((data) =>
data.map((client) => {
return { id: client._id, name: client.name };
})
);
},
addClient(client: NewClient): Promise<Client> {
return axios
.post(basepath + "/clients", client)
.then((res) => res.data)
.then((client) => {
return { ...client, id: client._id };
})
.then((data) => data as Client);
},
getClientById(id: String): Promise<Client> {
return axios
.get(basepath + "/clients/" + id)
.then((res) => res.data)
.then((client) => {
return { ...client, id: client._id };
})
.then((client) => client as Client);
},
updateStatus(id: String, status: String): Promise<String> {
return axios.post(basepath + "/clients/" + id + "/events", {
eventType: "status_changed",
value: status,
});
},
deleteMany(ids: String[]): Promise<any> {
console.log("To delete:", ids);
return axios({
method: "delete",
url: basepath + "/clients",
data: ids,
});
},
};

6
client/src/util/statusTagMap.json

@ -1,6 +0,0 @@
{
"potential": { "color": "default", "text": "Potential" },
"active": { "color": "success", "text": "Active" },
"on_hold": { "color": "purple", "text": "On Hold" },
"inactive": { "color": "error", "text": "Inactive" }
}

13
client/views/404.jsx

@ -1,13 +0,0 @@
import React from "react";
import Layout from "./layouts/Main";
export default function Index() {
return (
<Layout>
<div className="jumbotron">
<h1 className="display-4">404</h1>
<p className="lead">This page was not found.</p>
</div>
</Layout>
);
}

47
client/views/Index.jsx

@ -1,47 +0,0 @@
import React from "react";
import Layout from "./layouts/Main";
export default function Index({ user }) {
return (
<Layout user={user}>
<div className="jumbotron">
<h1 className="display-4">Welcome back, {user.name?.split(" ")[0]}!</h1>
<p className="lead">Thanks for testing out the Ωmega alpha.</p>
<p className="lead">
Please keep in mind that this is an unfinished product. If you have
any thoughts or encounter problems, I would encourage you to share it
on the <a href="https://lists.sr.ht/~garritfra/omega">mailing list</a>{" "}
of this project.
</p>
<hr className="my-4"></hr>
<div className="d-flex flex-column">
<a
className="btn btn-primary btn-sm mb-2"
href="/clients"
role="button"
>
Manage Clients
</a>
<a
className="btn btn-light btn-sm"
href="https://git.sr.ht/~garritfra/omega"
>
Source Code
</a>
<a
className="btn btn-light btn-sm mt-2"
href="https://lists.sr.ht/~garritfra/omega"
>
Thoughts, questions, ideas?
</a>
<a
className="btn btn-light btn-sm mt-2"
href="https://todo.sr.ht/~garritfra/omega"
>
File a bug
</a>
</div>
</div>
</Layout>
);
}

30
client/views/Landing.jsx

@ -1,30 +0,0 @@
import React from "react";
import Layout from "./layouts/Main";
export default function Index() {
return (
<Layout>
<div className="jumbotron">
<h1 className="display-4">Ωmega</h1>
<p className="lead">The last CRM you will ever need.</p>
<hr className="my-4"></hr>
<div className="d-flex flex-column">
<a
className="btn btn-primary btn-sm mb-2"
href="/auth/register"
role="button"
>
Try out the Alpha
</a>
<a
className="btn btn-light btn-sm mb-2"
href="/auth/login"
role="button"
>
Already have an account? Log in here
</a>
</div>
</div>
</Layout>
);
}

44
client/views/Login.jsx

@ -1,44 +0,0 @@
import React, { useEffect } from "react";
import Layout from "./layouts/Main";
const basePath = process.env.API_BASE_PATH;
const frontendBasePath = process.env.FRONTEND_BASE_PATH;
export default function Index() {
return (
<Layout>
<form
action={
basePath + "/auth/login" + "?redirect=" + frontendBasePath + "/"
}
method="POST"
>
<div className="form-group">
<label for="email">Email address</label>
<input
type="email"
name="email"
className="form-control"
id="email"
aria-describedby="emailHelp"
/>
<small id="emailHelp" className="form-text text-muted">
We'll never share your email with anyone else.
</small>
</div>
<div className="form-group">
<label for="password">Password</label>
<input
type="password"
name="password"
className="form-control"
id="password"
/>
</div>
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
</Layout>
);
}

53
client/views/Register.jsx

@ -1,53 +0,0 @@
import React from "react";
import Layout from "./layouts/Main";
const basePath = process.env.API_BASE_PATH;
const frontendBasePath = process.env.FRONTEND_BASE_PATH;
export default function Index() {
return (
<Layout>
<form
action={
basePath + "/auth/register" + "?redirect=" + frontendBasePath + "/"
}
method="POST"
>
<div className="form-group">
<label for="email">Email address</label>
<input
type="email"
name="email"
className="form-control"
id="email"
aria-describedby="emailHelp"
/>
<small id="emailHelp" className="form-text text-muted">
We'll never share your email with anyone else.
</small>
</div>
<div className="form-group">
<label for="password">Password</label>
<input
type="password"
name="password"
className="form-control"
id="password"
/>
</div>
<div className="form-group">
<label for="name">Full Name</label>
<input
type="name"
name="full_name"
className="form-control"
id="name"
/>
</div>
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
</Layout>
);
}

46
client/views/clients/Detail.jsx

@ -1,46 +0,0 @@
import React from "react";
import Layout from "../layouts/Main";
import moment from "moment";
import ClientDetailHeader from "../components/ClientDetailHeader";
export default function Detail(props = { client, user }) {
const timelineComponent = props.client.events
.reverse()
.slice(0, 2)
.map((event) => {
return (
<div className="col-sm-5 my-1">
<div className="card h-100">
<div className="card-body">
<h5 className="card-title text-capitalize">
{event.eventType.replace("_", " ")}
</h5>
<h6 className="card-subtitle mb-2 text-muted">
{moment(event.createdAt).fromNow()}
</h6>
<p className="card-text text-capitalize">
{event.value?.replace("_", " ")}
</p>
</div>
</div>
</div>
);
});
return (
<Layout user={props.user}>
<ClientDetailHeader {...props} />
<div className="jumbotron jumbotron-fluid row mt-4 py-1 mx-0">
{timelineComponent}
<div className="col-sm-2 d-flex align-items-center">
<a
className="btn btn-light my-2"
href={`/clients/${props.client._id}/timeline`}
>
View Full Timeline
</a>
</div>
</div>
</Layout>
);
}

39
client/views/clients/Index.jsx

@ -1,39 +0,0 @@
import React from "react";
import Layout from "../layouts/Main";
export default function Clients({ clients, user }) {
const clientViews = clients.map((client) => {
return (
<a
href={"/clients/" + client.id}
className="list-group-item list-group-item-action"
>
<div className="d-flex w-100 justify-content-between">
<h5 className="mb-1">{client.name}</h5>
<small className="text-capitalize">
{client.status.replace("_", " ")}
</small>
</div>
<p className="mb-1">{client.email}</p>
</a>
);
});
return (
<Layout user={user}>
<div className="row">
<div className="col-md-4">
<a
href="/clients/new"
className="btn btn-outline-dark btn-block mb-3"
>
New Client
</a>
</div>
<div className="col-md-8">
<div className="list-group">{clientViews}</div>
</div>
</div>
</Layout>
);
}

74
client/views/clients/New.jsx

@ -1,74 +0,0 @@
import React from "react";
import Layout from "../layouts/Main";
const basePath = process.env.API_BASE_PATH;
const frontendBasePath = process.env.FRONTEND_BASE_PATH;
export default function Clients({ user }) {
return (
<Layout user={user}>
<form
action={
basePath +
"/clients" +
"?token=" +
user.token +
"&redirect=" +
encodeURIComponent(frontendBasePath) +
"/clients"
}
method="POST"
>
<div className="row">
<div className="col-lg-6">
<div className="form-group">
<label for="name">Name</label>
<input
type="name"
name="name"
className="form-control"
id="name"
/>
</div>
<div className="form-group">
<label for="email">E-Mail</label>
<input
type="email"
name="email"
className="form-control"
id="email"
/>
</div>
<div className="form-group">
<label for="status">Current Status</label>
<select className="form-control" id="status" name="status">
<option value="potential">Potential</option>
<option value="active">Active</option>
<option value="on_hold">On Hold</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
<div className="col-lg-6">
<div className="form-group">
<label for="address">Address</label>
<textarea name="address" className="form-control" id="address" />
</div>
<div className="form-group">
<label for="telephone">Phone</label>
<input
type="tel"
name="telephone"
className="form-control"
id="telephone"
/>
</div>
</div>
</div>
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
</Layout>
);
}

29
client/views/clients/Timeline.jsx

@ -1,29 +0,0 @@
import React from "react";
import Layout from "../layouts/Main";
import moment from "moment";
import ClientDetailHeader from "../components/ClientDetailHeader";
export default function Timeline(props = { client, events, user }) {
const eventViews = props.events.reverse().map((event) => {
return (
<li className="list-group-item">
<h5 className="card-title text-capitalize">
{event.eventType.replace("_", " ")}
</h5>
<h6 className="card-subtitle mb-2 text-muted">
{moment(event.createdAt).fromNow()}
</h6>
<p className="card-text text-capitalize">
{event.value?.replace("_", " ")}
</p>
</li>
);
});
return (
<Layout user={props.user}>
<ClientDetailHeader {...props} />
<ul className="list-group my-5">{eventViews}</ul>
</Layout>
);
}

73
client/views/components/ClientDetailHeader.jsx

@ -1,73 +0,0 @@
import React from "react";
import Layout from "../layouts/Main";
import moment from "moment";
const basePath = process.env.API_BASE_PATH;
const frontendBasePath = process.env.FRONTEND_BASE_PATH;
export default function ClientDetailHeader({ client, user }) {
return (
<div className="row">
<div className="col col-md-8">
<h4 className="display-4">{client.name}</h4>
<h4 className="lead text-muted">{client.email}</h4>
</div>
<div className="col col-md-4">
<div class="card border-dark">
<div class="card-body d-flex flex-column">
<div className="d-flex justify-content-between">
<span>Address</span>
<span className="text-right">{client.address || "-"}</span>
</div>
<div className="d-flex justify-content-between">
<span>Tel.</span>
<span>{client.telephone || "-"}</span>
</div>
<form
method="post"
action={
basePath +
"/clients/" +
client._id +
"/events" +
"?token=" +
user.token +
"&redirect=" +
frontendBasePath +
"/clients/" +
client._id
}
class="inline"
>
<div className="d-flex justify-content-between">
<span>Status</span>
<select
className="card-text text-capitalize"
name="value"
defaultValue={client.status}
>
<option value="potential" className="card-text">
Potential
</option>
<option value="active" className="card-text">
Active
</option>
<option value="inactive" className="card-text">
Inactive
</option>
<option value="on_hold" className="card-text">
On Hold
</option>
</select>
</div>
<button type="submit" class="badge badge-primary align-self-end">
Update
</button>
<input type="hidden" name="eventType" value="status_changed" />
</form>
</div>
</div>
</div>
</div>
);
}

57
client/views/layouts/Header.jsx

@ -1,57 +0,0 @@
import React, { useContext } from "react";
import UserContext from "../../contexts/UserContext";
export default function Head() {
const user = useContext(UserContext);
return (
<nav className="navbar navbar-expand-md navbar-light container">
<a className="navbar-brand" href="/">
Ωmega
</a>
<button
className="navbar-toggler"
type="button"
data-toggle="collapse"
data-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarNav">
<ul className="navbar-nav">
<li className="nav-item active">
<a className="nav-link" href="/clients">
Clients <span className="sr-only">(current)</span>
</a>
</li>
</ul>
<ul className="navbar-nav ml-auto">
{user ? (
<li className="nav-item">
<a className="nav-link" href="/auth/logout">
Logged in as {user.name} - Log Out{" "}
<span className="sr-only">(current)</span>
</a>
</li>
) : (
<>
<li className="nav-item">
<a className="nav-link" href="/auth/login">
Login <span className="sr-only">(current)</span>
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="/auth/register">
Register <span className="sr-only">(current)</span>
</a>
</li>
</>
)}
</ul>
</div>
</nav>
);
}

25
client/views/layouts/Main.jsx

@ -1,25 +0,0 @@
import React from "react";
import Header from "./Header";
import UserContext from "../../contexts/UserContext";
export default function ({ user, children }) {
return (
<UserContext.Provider value={user}>
<head>
<title>Ωmega</title>
<link rel="stylesheet" href="/bootstrap.min.css"></link>
<link rel="stylesheet" href="/styles.css"></link>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
></meta>
<script src="/jquery.min.js"></script>
<script src="/bootstrap.min.js"></script>
</head>
<body>
<Header></Header>
<div className="container mt-3">{children}</div>
</body>
</UserContext.Provider>
);
}
Loading…
Cancel
Save