Garrit Franke
3 years ago
51 changed files with 28938 additions and 15935 deletions
@ -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;"] |
||||
|
@ -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. |
@ -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 |
@ -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); |
@ -0,0 +1,13 @@
|
||||
{ |
||||
"presets": [ |
||||
[ |
||||
"@babel/preset-react", |
||||
{ |
||||
"targets": { |
||||
"node": "current" |
||||
} |
||||
} |
||||
], |
||||
"@babel/preset-env" |
||||
] |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -1,3 +0,0 @@
|
||||
const { createContext } = require("react"); |
||||
|
||||
module.exports = createContext({}); |
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 15 KiB |
@ -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> |
File diff suppressed because it is too large
Load Diff
@ -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; |
||||
} |
@ -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; |
@ -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; |
@ -1,7 +0,0 @@
|
||||
const router = require("express").Router(); |
||||
|
||||
router.get("/", (req, res) => { |
||||
res.render("Index", { user: req.user }); |
||||
}); |
||||
|
||||
module.exports = router; |
@ -1,7 +0,0 @@
|
||||
const router = require("express").Router(); |
||||
|
||||
router.get("/", (req, res) => { |
||||
res.render("Landing"); |
||||
}); |
||||
|
||||
module.exports = router; |
@ -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, |
||||
}); |
||||
}, |
||||
}; |
@ -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 }; |
||||
}, |
||||
}; |
@ -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; |
@ -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> |
||||
); |
||||
} |
||||
|
@ -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> |
||||
); |
||||
} |
@ -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} />; |
||||
}; |
@ -0,0 +1,5 @@
|
||||
import React from "react"; |
||||
|
||||
const Landing = () => <h1>Landing</h1> |
||||
|
||||
export default Landing; |
@ -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> |
||||
); |
||||
} |
@ -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>; |
||||
} |
@ -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> |
||||
</> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
</> |
||||
); |
||||
} |
@ -1,5 +0,0 @@
|
||||
import React from "react"; |
||||
|
||||
export default function ProjectsListPage() { |
||||
return <p>Projects...</p>; |
||||
} |
@ -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, |
||||
}); |
||||
}, |
||||
}; |
@ -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" } |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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> |
||||
); |
||||
} |
@ -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…
Reference in new issue