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 ( + + {authState !== "unchecked" ? // eslint-disable-line no-negated-condition + + + {authState === "logged in" ? "Logout" : "Login"} + + : + null} + {authState === "logged out" ? + + + Register + + : + null} + + ); +}; + +HeaderMenu.propTypes = { + mode: PropTypes.string, + className: PropTypes.string, + authState: PropTypes.string, + handleClick: PropTypes.func, + currentItem: PropTypes.string +}; +HeaderMenu.defaultProps = { + mode: "horizontal", + className: "header__menu", + authState: "unchecked", + handleClick: () => {}, + currentItem: "0" +}; + +export default HeaderMenu; diff --git a/client/components/LoginForm.js b/client/components/LoginForm.js index ea95734..ab26f2a 100644 --- a/client/components/LoginForm.js +++ b/client/components/LoginForm.js @@ -11,7 +11,7 @@ import "../static/LoginForm.css"; class LoginForm extends React.Component { state = { error: null - }; + } handleSubmit = e => { e.preventDefault(); @@ -33,7 +33,7 @@ class LoginForm extends React.Component { error: validationError && validationError.message ? validationError : null }); }); - }; + } render() { const {error} = this.state; diff --git a/client/components/QuestionForm.js b/client/components/QuestionForm.js index bb8fcaa..47dbebb 100644 --- a/client/components/QuestionForm.js +++ b/client/components/QuestionForm.js @@ -14,7 +14,7 @@ const {TextArea} = Input; class QuestionForm extends React.Component { state = { tags: [] - }; + } handleSubmit = e => { e.preventDefault(); @@ -28,7 +28,7 @@ class QuestionForm extends React.Component { }); } }); - }; + } cancel() { Router.push("/"); @@ -36,7 +36,7 @@ class QuestionForm extends React.Component { updateQuestionTags = tags => { this.setState({tags}); - }; + } render() { const {getFieldDecorator} = this.props.form; diff --git a/client/components/RegisterForm.js b/client/components/RegisterForm.js index 4804d4c..91e7ffd 100644 --- a/client/components/RegisterForm.js +++ b/client/components/RegisterForm.js @@ -11,7 +11,7 @@ import "../static/RegisterForm.css"; class RegisterForm extends React.Component { state = { confirmDirty: false // TODO: Figure out what this is used for and if we even need it. - }; + } handleSubmit = e => { e.preventDefault(); @@ -20,7 +20,7 @@ class RegisterForm extends React.Component { this.props.signup(values); } }); - }; + } compareToFirstPassword = (rule, value, callback) => { const {form} = this.props; @@ -29,7 +29,7 @@ class RegisterForm extends React.Component { } else { callback(); } - }; + } validateToNextPassword = (rule, value, callback) => { const {form} = this.props; @@ -38,7 +38,7 @@ class RegisterForm extends React.Component { } callback(); - }; + } render() { const {getFieldDecorator} = this.props.form; diff --git a/client/components/SearchForm.js b/client/components/SearchForm.js index 429cf23..3e61c4e 100644 --- a/client/components/SearchForm.js +++ b/client/components/SearchForm.js @@ -12,8 +12,19 @@ class SearchForm extends React.Component { isNewQuery: true // TODO: Make sure this always recognizes new queries correctly, even if we remove all query text except for one character. } - handleChange = e => { - if (e.target.value.length === 1) { + componentDidUpdate() { + const {isNewQuery} = this.state; + + if (this.searchInput instanceof HTMLElement) { + const inputField = this.searchInput.querySelector(".ant-input"); + if (inputField === document.activeElement && !isNewQuery) { + inputField.blur(); + } + } + } + + handleChange = event => { + if (event.target.value.length === 1) { this.setState({ isNewQuery: true }); @@ -21,12 +32,12 @@ class SearchForm extends React.Component { } onSearch = async value => { + if (this.state.isNewQuery) { + this.setState({isNewQuery: false}); + } + try { await this.props.search(value); - - if (this.state.isNewQuery) { - this.setState({isNewQuery: false}); - } } catch (error) { console.error("error", error); } @@ -36,7 +47,12 @@ class SearchForm extends React.Component { const {ranSearch = false, stemmedWords = [], updateTags} = this.props; 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. - + +
); } } 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"