diff --git a/client/components/Header.js b/client/components/Header.js
index e9f03bc..d2017b2 100644
--- a/client/components/Header.js
+++ b/client/components/Header.js
@@ -1,16 +1,20 @@
import React from "react";
import PropTypes from "prop-types";
+import Router from "next/router";
import Link from "next/link";
-import {Layout, Menu, Icon, Drawer} from "antd";
+import {Layout, Menu} from "antd";
+
+import HeaderMenu from "./HeaderMenu";
+
+import "antd/dist/antd.css";
import "../static/Header.css";
const {Header} = Layout;
export default class MainHeader extends React.Component {
state = {
- drawerIsVisible: false,
currentItem: "0"
- };
+ }
componentDidMount() {
let currentItem = "0";
@@ -31,59 +35,37 @@ export default class MainHeader extends React.Component {
this.setState({currentItem});
}
- openDrawer = () => {
- this.setState({
- drawerIsVisible: true,
- currentItem: "0"
- });
- };
-
- closeDrawer = () => {
- this.setState({
- drawerIsVisible: false
- });
- };
+ navigateHome = () => {
+ if (window.location.pathname === "/") {
+ window.location.reload();
+ } else {
+ Router.push("/");
+ }
+ }
render() {
- const {drawerIsVisible, currentItem} = this.state;
+ const {currentItem} = this.state;
const {authState} = this.props;
return (
);
diff --git a/client/components/HeaderMenu.js b/client/components/HeaderMenu.js
new file mode 100644
index 0000000..547c33a
--- /dev/null
+++ b/client/components/HeaderMenu.js
@@ -0,0 +1,48 @@
+import React from "react";
+import PropTypes from "prop-types";
+import Link from "next/link";
+import {Menu} from "antd";
+
+const HeaderMenu = ({currentItem = "0", handleClick, authState = "unchecked", className = "header__menu", mode = "horizontal"}) => {
+ return (
+
+
{
+ this.searchInput = input;
+ }}
+ className="search_input"
+ >
Search
{
try {
@@ -24,16 +24,18 @@ class SearchTag extends React.Component {
console.log(error);
return [];
}
- };
+ }
componentDidMount() {
this.updateTags();
}
componentDidUpdate(prevProps) { // TODO: Only update the tags if a new query has been searched for (i.e. the user has removed all contents from the search input and then typed a new query).
- const tagSearchField = document.querySelector(".ant-select-search__field");
- if (tagSearchField) {
- tagSearchField.addEventListener("input", this.updateTags); // TODO: Make sure that we always have at least 5 tags in the autocomplete list.
+ if (this.tagSelect instanceof HTMLElement) {
+ const tagInputField = this.tagSelect.querySelector(".ant-select-search__field");
+ if (tagInputField) {
+ tagInputField.addEventListener("input", this.updateTags); // TODO: Make sure that we always have at least 5 tags in the autocomplete list.
+ }
}
const {tags} = this.state;
@@ -64,7 +66,7 @@ class SearchTag extends React.Component {
}
return clonedArr;
- };
+ }
handleChange = tag => {
this.setState(prevState => ({
@@ -72,7 +74,7 @@ class SearchTag extends React.Component {
}), () => {
this.props.updateTags(this.state.activeTags);
});
- };
+ }
render() {
const {ranSearch = false} = this.props;
@@ -85,20 +87,26 @@ class SearchTag extends React.Component {
return (
// TODO: Figure out how to stop the options popup from jumping around, and make it stay under the tag field.
- {
+ this.tagSelect = element;
+ }}
>
- {tags
- .map(tag => {tag.name} )
- .filter(option => !activeTags.includes(option.key))
- }
-
+
+ {tags
+ .map(tag => {tag.name} )
+ .filter(option => !activeTags.includes(option.key))
+ }
+
+
);
}
}
diff --git a/client/config.json b/client/config.json
index 8dc80ca..013b064 100644
--- a/client/config.json
+++ b/client/config.json
@@ -7,5 +7,5 @@
"storageBucket": "react-firebase-85039.appspot.com",
"messagingSenderId": "55358337129"
},
- "apiUrl": "http://knowl-knowl-ciw3basidwqs-321112760.eu-central-1.elb.amazonaws.com/"
+ "apiUrl": "http://knowl-knowl-ciw3basidwqs-321112760.eu-central-1.elb.amazonaws.com"
}
diff --git a/client/package.json b/client/package.json
index c3765e2..226bfba 100644
--- a/client/package.json
+++ b/client/package.json
@@ -6,12 +6,12 @@
"node": ">=8"
},
"scripts": {
- "dev": "next",
+ "dev": "next dev",
"start": "next start",
- "heroku:start": "next start --port $PORT",
"build": "next build",
- "container:build": "docker build -t knowledge-client .",
- "container:run": "docker run -d --name knowledge-client -p 3000:3000 knowledge-client",
+ "heroku:start": "next start --port $PORT",
+ "docker:build": "docker build -t knowledge-client .",
+ "docker:run": "docker run -d --name knowledge-client -p 3000:3000 knowledge-client",
"test": "xo"
},
"dependencies": {
diff --git a/client/pages/login.js b/client/pages/login.js
index 2b22b8a..e5b2343 100644
--- a/client/pages/login.js
+++ b/client/pages/login.js
@@ -46,7 +46,7 @@ class Login extends React.Component {
await get("/auth/logout"); // User already logged in with Firebase
return {message: "Login failed (internal server error)"};
}
- };
+ }
render() {
return (
diff --git a/client/pages/logout.js b/client/pages/logout.js
index 7478b84..840d110 100644
--- a/client/pages/logout.js
+++ b/client/pages/logout.js
@@ -11,7 +11,7 @@ export default class Logout extends React.Component {
state = {
status: "Logging out...",
loading: true
- };
+ }
async componentDidMount() {
try {
diff --git a/client/pages/post-question.js b/client/pages/post-question.js
index c7d18af..edd56b3 100644
--- a/client/pages/post-question.js
+++ b/client/pages/post-question.js
@@ -11,7 +11,7 @@ import {post} from "../http";
class QuestionPage extends React.Component {
state = {
status: "drafting"
- };
+ }
postQuestion = async questionData => {
this.setState(() => ({
@@ -50,7 +50,7 @@ class QuestionPage extends React.Component {
status: "drafting"
}));
}
- };
+ }
render() {
const {status} = this.state;
diff --git a/client/pages/search.js b/client/pages/search.js
index df150c3..6a29345 100644
--- a/client/pages/search.js
+++ b/client/pages/search.js
@@ -16,7 +16,7 @@ class Search extends React.Component {
stemmedWords: [],
ranSearch: false,
showPost: false
- };
+ }
querySearch = async query => {
const joinedTags = this.state.activeTags.join(",");
@@ -38,7 +38,7 @@ class Search extends React.Component {
}
return this.setState({ranSearch: true});
- };
+ }
updateActiveTags = tags => {
this.setState({
@@ -50,7 +50,7 @@ class Search extends React.Component {
this.setState(() => ({
showPost: authState === "logged in"
}));
- };
+ }
render() { // TODO: Only show the option to post a new question once a user searches something, and hide it when the query text field changes.
const {questions, stemmedWords, ranSearch, showPost} = this.state;
diff --git a/client/pages/tag.js b/client/pages/tag.js
index e992fe5..8841171 100644
--- a/client/pages/tag.js
+++ b/client/pages/tag.js
@@ -11,7 +11,7 @@ import "../static/Tag.css";
class Tag extends React.Component {
state = {
questions: []
- };
+ }
static getInitialProps({query}) {
return {
diff --git a/client/pages/thread.js b/client/pages/thread.js
index 12c2d32..88a8417 100644
--- a/client/pages/thread.js
+++ b/client/pages/thread.js
@@ -15,7 +15,7 @@ class Thread extends React.Component {
thread: {},
replyIsActive: false,
auth: false
- };
+ }
static getInitialProps({query}) {
return {id: query.id};
@@ -29,7 +29,7 @@ class Thread extends React.Component {
this.setState(() => ({
auth: authState === "logged in"
}));
- };
+ }
async fetchThread() {
const {id} = this.props;
@@ -52,7 +52,7 @@ class Thread extends React.Component {
this.setState({
replyIsActive: true
});
- };
+ }
onSubmitReply = async replyData => {
if (replyData) { // TODO: Keep the reply active and display some sort of error message in case the given reply is empty / invalid (i.e. too short / has empty fields).
@@ -79,7 +79,7 @@ class Thread extends React.Component {
this.setState({
replyIsActive: false
});
- };
+ }
render() {
const {id} = this.props;
diff --git a/client/static/Footer.css b/client/static/Footer.css
index 5116bee..501576e 100644
--- a/client/static/Footer.css
+++ b/client/static/Footer.css
@@ -1,8 +1,10 @@
.footer {
text-align: center;
- height: 2rem;
background: #333 !important;
color: #ddd !important;
- display: grid;
+ display: flex;
+ line-height: 0.1;
align-content: center;
+ justify-content: center;
+ height: 10%;
}
diff --git a/client/static/Header.css b/client/static/Header.css
index affa5a9..99eb427 100644
--- a/client/static/Header.css
+++ b/client/static/Header.css
@@ -1,25 +1,48 @@
.header {
background: #fff !important;
- padding: 0;
+ padding: 0 50px;
}
.header__nav {
display: flex;
}
-.header__menu {
+.header__menu, .header__join {
vertical-align: middle;
margin: 0 !important;
padding: 0 !important;
- display: none;
line-height: 64px !important;
+ border: 0 !important;
+}
+
+.ant-menu > .ant-menu-item {
+ border: 0 !important;
+ top: 0px;
+ margin-top: -2px;
+}
+
+.header__menu > *, .header__join > * {
+ line-height: 64px !important;
+ border: 0px !important;
+}
+
+.header__menu {
+ display: none;
}
-.header a {
- color: inherit;
+.header > nav > .ant-menu > .ant-menu-item > a, .header a {
+ color: inherit !important;
border: 0;
}
+.header > nav > .ant-menu > .ant-menu-item > a:before {
+ bottom: 0px !important;
+}
+
+.header > nav > .ant-menu > .ant-menu-item.ant-menu-item-selected {
+ border-bottom: 2px solid #1890ff !important;
+}
+
.header a:active,
.header a:hover,
.header a:focus {
@@ -32,46 +55,11 @@
font-size: 1.2rem;
}
-.header__drawer--toggle {
- cursor: pointer;
- display: block;
- font-size: 1.1rem;
-}
-
-.mobile__menu {
- margin: 0 !important;
- padding: 0 !important;
- border: 0 !important;
- text-align: center;
-}
-
-.mobile__menu a {
- color: inherit;
- border: 0;
-}
-
-.logo {
- background: url(/static/logo.png);
- background-repeat: no-repeat;
- background-size: cover;
- width: 6rem;
- height: 6rem;
- margin: auto;
- margin-top: 1.5rem;
- margin-bottom: 0.5rem;
- display: block;
-}
-
-@media(min-width: 40rem) {
+@media(min-width: 30rem) {
.header__menu {
display: block;
}
-
- .header__drawer--toggle {
- display: none;
- }
-
- .logo {
+ .header__join {
display: none;
}
}
diff --git a/client/static/logo.png b/client/static/logo.png
deleted file mode 100644
index 9f67834..0000000
Binary files a/client/static/logo.png and /dev/null differ
diff --git a/readme.md b/readme.md
index d644e97..a9d5de1 100644
--- a/readme.md
+++ b/readme.md
@@ -43,7 +43,7 @@ Both our frontend and backend are written in Node.js. The tech stack consists of
- [MongoDB](https://www.mongodb.com) - for data storage.
- [Firebase Auth](https://firebase.google.com/docs/auth) - for password-based user authentication
-### DevOps and deployment
+#### DevOps and deployment
- [Docker](https://www.docker.com) - for deploying our backend and frontend as containers.
- [Docker Compose](https://docs.docker.com/compose) - for locally running the backend app and Mongo.
diff --git a/server/.dockerignore b/server/.dockerignore
index c82c981..fc9d61f 100644
--- a/server/.dockerignore
+++ b/server/.dockerignore
@@ -2,3 +2,4 @@ cloud
node_modules
.git
readme.md
+sample.env
diff --git a/server/index.js b/server/index.js
index d8d52a5..95479fb 100644
--- a/server/index.js
+++ b/server/index.js
@@ -17,7 +17,7 @@ const app = express();
const firebase = admin.initializeApp({
credential: admin.credential.cert(credentials),
- databaseURL: "https://react-firebase-85039.firebaseio.com"
+ databaseURL: "https://react-firebase-85039.firebaseio.com" // TODO: Move this setting to `config.js`.
}, "server");
// Setting a few options to remove warnings on feature deprecations.
@@ -33,7 +33,7 @@ mongoose.connect(databaseUrl || "mongodb://mongo:27017/test_db")
console.log(`Error connecting to Mongo: ${error}`);
});
-const corsMiddleware = cors({origin: clientUrl, optionsSuccessStatus: 200, credentials: true});
+const corsMiddleware = cors({origin: clientUrl, optionsSuccessStatus: 200, credentials: true}); // TODO: Make the cors `origin` url configurable through an environment variable.
app.use(corsMiddleware);
app.options("*", corsMiddleware);
diff --git a/server/package.json b/server/package.json
index e8aac84..1665ae8 100644
--- a/server/package.json
+++ b/server/package.json
@@ -9,8 +9,8 @@
"scripts": {
"start": "node index.js",
"compose:start": "docker-compose up",
- "container:build": "docker build -t knowledge .",
- "container:run": "docker run -d --name knowledge -p 5000:5000 knowledge",
+ "docker:build": "docker build -t knowledge-server .",
+ "docker:run": "docker run -d --name knowledge-server -p 5000:5000 knowledge-server",
"postinstall": "cd cloud && npm install",
"deploy": "cd cloud && npm run deploy",
"test": "xo"