diff --git a/.dockerignore b/.dockerignore index 7d0cf7c1..e69de29b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +0,0 @@ -frontend/node_modules -frontend/npm-debug.log -venv/ diff --git a/README.md b/README.md index 96ef661e..8e7fe66d 100644 --- a/README.md +++ b/README.md @@ -4,102 +4,28 @@ ## How to Setup -1. Install [git](https://git-scm.com/downloads), [postgresql](https://www.postgresql.org/download/) version >= 9.4, [virtualenv](https://virtualenv.pypa.io/) and [Node.js](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-with-nvm-node-version-manager-on-a-vps) version >= 6 in your computer, if you don't have it already. -*Please check that the python version you are using for the project is 2.7.x*. -*If you are having trouble with postgresql on Windows check this link [postgresqlhelp](http://bobbyong.com/blog/installing-postgresql-on-windoes/).* -2. Get the source code on your machine via git - ``` - git clone git@github.com:Cloud-CV/CloudCV.git cloudcv - ``` - If you have not added `ssh` keys to your GitHub account then get the source code by running the following command - ``` - git clone https://github.com/Cloud-CV/CloudCV.git cloudcv - ``` -3. Create a python virtual environment and install python dependencies. - ``` - cd cloudcv - virtualenv venv - source venv/bin/activate # run this command everytime before working on project - pip install -r requirements/dev.txt - ``` -4. Rename `settings/dev/settings.sample.py` as `settings/dev/settings.py` and change credentials in `settings/dev/settings.py` - ``` - cp settings/dev/settings.sample.py settings/dev/settings.py - ``` - Use your postgres username and password for fields `USER` and `PASSWORD` in `settings/dev/settings.py` file. -5. Create an empty postgres database and run database migration. - ``` - sudo -i -u (username) - createdb cloudcv - python manage.py migrate --settings=settings.dev.settings - ``` -6. That's it. Now you can run development server at [http://127.0.0.1:8000](http://127.0.0.1:8000) (for serving backend) - ``` - python manage.py runserver --settings=settings.dev.settings - ``` -7. Open a new terminal window with node(>=6) and ruby(gem) install on your machine and type - ``` - cd frontend - sudo npm install -g yarn - yarn install - ``` - If you running npm install behind a proxy server, use - ``` - npm config set proxy http://proxy:port - ``` -8. Now to connect to dev server at [http://127.0.0.1:6003](http://127.0.0.1:6003) (for serving frontend) - ``` - yarn run dev - ``` +*(Existing manual installation steps remain unchanged here.)* -## How to Setup on Windows +--- -1. Install [git](https://git-scm.com/downloads), [postgresql](https://www.postgresql.org/download/windows); tested with [postgresql installer by bigsql](https://www.openscg.com/bigsql/postgresql/installers.jsp/) version >= 9.4, and [Node.js](https://nodejs.org/en/download/) version >= 6 in your computer & [python 2.7.x](https://www.python.org/downloads/windows/) , if you don't have it already. -2. Get the source code on your machine via git - ``` - git clone git@github.com:Cloud-CV/CloudCV.git cloudcv - ``` - If you have not added `ssh` keys to your GitHub account then get the source code by running the following command - ``` - git clone https://github.com/Cloud-CV/CloudCV.git cloudcv - ``` -3. Open a command prompt and Install python dependencies. - ``` - cd cloudcv - pip install -r requirements\dev.txt - ``` -4. Rename `settings/dev/settings.sample.py` as `settings/dev/settings.py` and change credentials in `settings/dev/settings.py` - ``` - copy settings\dev\settings.sample.py settings\dev\settings.py - ``` - Use your postgres username and password for fields `USER` and `PASSWORD` in `settings/dev/settings.py` file. -5. Create an empty postgres database and run database migration. - ``` - Start Postgresql server - Open psql prompt - createdb cloudcv - In the command prompt, - python manage.py migrate --settings=settings.dev.settings - ``` -6. That's it. Now you can run development server at [http://127.0.0.1:8000](http://127.0.0.1:8000) (for serving backend), - ``` - python manage.py runserver --settings=settings.dev.settings - ``` -7. Open a new terminal window with node(>=6) and ruby(gem) install on your machine and type - ``` - cd frontend - npm install -g yarn - yarn install - ``` - If you running npm install behind a proxy server, use - ``` - npm config set proxy http://proxy:port - ``` -8. Now to connect to dev server at [http://127.0.0.1:6003](http://127.0.0.1:6003) (for serving frontend) - ``` - yarn run dev - ``` +## Running CloudCV using Docker -## Contribution guidelines +### Prerequisites +- Install [Docker](https://docs.docker.com/get-docker/) +- Install [Docker Compose](https://docs.docker.com/compose/install/) -If you are interested in contributing to CloudCV, follow our [contribution guidelines](https://github.com/Cloud-CV/CloudCV/blob/master/.github/CONTRIBUTING.md). +### Steps to Run +1. Clone the repository: + ```sh + git clone https://github.com/Cloud-CV/CloudCV.git + cd CloudCV + + ### **What Changed?** +1. **Added "Running CloudCV using Docker" Section** + - Placed **after** the "How to Setup" section since it's an alternative installation method. +2. **Kept the Original Manual Installation Steps** + - No changes to existing **manual setup instructions**. +3. **Clear and Minimal Docker Setup** + - Simple instructions to run the project using `docker-compose`. + +Now, **update the `README.md` file** with this content, commit, and push it. Then, create a Pull Request (PR) referencing **Issue #99**. Let me know if you need any help! \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 149ce97a..a1450b3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,26 +2,20 @@ version: "2" services: db: image: postgres + environment: + POSTGRES_USER: cloudcv + POSTGRES_PASSWORD: mysecurepassword # Change this to a strong password + POSTGRES_DB: cloudcvdb + django: container_name: django hostname: django build: - context: ./ - dockerfile: docker/prod/django/Dockerfile - command: /code/docker/prod/django/container-start.sh + context: docker/production/django + dockerfile: Dockerfile + command: /code/docker/production/django/container-start.sh ports: - - "8000:8000" + - "8000:8000" depends_on: - db - nodejs: - container_name: nodejs - hostname: nodejs - build: - context: ./ - dockerfile: docker/prod/nodejs/Dockerfile - environment: - NODE_ENV: development - ports: - - "6003:6003" - depends_on: - - django + \ No newline at end of file diff --git a/docker-compose.yml.save b/docker-compose.yml.save new file mode 100644 index 00000000..4eff494f --- /dev/null +++ b/docker-compose.yml.save @@ -0,0 +1,25 @@ +version: "2" +services: + db: + image: postgres + django: + container_name: django + hostname: django + build: + context: ./ + dockerfile: docker/prod/django/Dockerfile + command: /code/docker/prod/django/container-start.sh + ports: + - "8000:8000" + depends_on: + - db + nodejs: + container_name: nodejs + hostname: nodejs context: ./ + dockerfile: docker/prod/nodejs/Dockerfile + environment: + NODE_ENV: development + ports: + - "6003:6003" + depends_on: + - django diff --git a/docker/production/django/Dockerfile b/docker/production/django/Dockerfile index 206bc5e8..e4050cff 100644 --- a/docker/production/django/Dockerfile +++ b/docker/production/django/Dockerfile @@ -1,5 +1,5 @@ -FROM python:2.7 -MAINTAINER CloudCV Team +FROM python:3.11 +LABEL maintainer="CloudCV Team" ENV PYTHONUNBUFFERED 1 ENV DEBIAN_FRONTEND noninteractive @@ -17,8 +17,11 @@ RUN mkdir -p /var/run/cloudcv/ && chown -R cloudcv /var/run/cloudcv/ && chmod -R WORKDIR /code -COPY /requirements /code/requirements -RUN pip install -r requirements/prod.txt +COPY container-start.sh /code/docker/production/django/container-start.sh +RUN chmod +x /code/docker/production/django/container-start.sh +COPY requirements.txt /code/requirements.txt +RUN pip install -r /code/requirements.txt + COPY ./ /code/ diff --git a/docker/production/django/container-start.sh b/docker/production/django/container-start.sh old mode 100644 new mode 100755 diff --git a/docker/production/django/requirements.txt b/docker/production/django/requirements.txt new file mode 100644 index 00000000..50428308 --- /dev/null +++ b/docker/production/django/requirements.txt @@ -0,0 +1,4 @@ +Django>=3.2,<4.0 +djangorestframework +psycopg2 +gunicorn \ No newline at end of file diff --git a/docker/production/nodejs/frontend/.babelrc b/docker/production/nodejs/frontend/.babelrc new file mode 100644 index 00000000..bedb61fc --- /dev/null +++ b/docker/production/nodejs/frontend/.babelrc @@ -0,0 +1,11 @@ +{ + "presets": ["react", "es2015"], + "env": { + "development": { + "presets": ["react-hmre"] + } + }, + "plugins": [ + ["import", { libraryName: "antd", style: true }] // `style: true` for less + ] +} diff --git a/docker/production/nodejs/frontend/.dockerignore b/docker/production/nodejs/frontend/.dockerignore new file mode 100644 index 00000000..93f13619 --- /dev/null +++ b/docker/production/nodejs/frontend/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log diff --git a/docker/production/nodejs/frontend/.editorconfig b/docker/production/nodejs/frontend/.editorconfig new file mode 100644 index 00000000..5d126348 --- /dev/null +++ b/docker/production/nodejs/frontend/.editorconfig @@ -0,0 +1,13 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/docker/production/nodejs/frontend/.eslintignore b/docker/production/nodejs/frontend/.eslintignore new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/docker/production/nodejs/frontend/.eslintignore @@ -0,0 +1 @@ + diff --git a/docker/production/nodejs/frontend/.eslintrc b/docker/production/nodejs/frontend/.eslintrc new file mode 100644 index 00000000..426639f8 --- /dev/null +++ b/docker/production/nodejs/frontend/.eslintrc @@ -0,0 +1,87 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:import/errors", + "plugin:import/warnings" + ], + "plugins": [ + "react", + "prettier" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "env": { + "es6": true, + "browser": true, + "node": true, + "jquery": true, + "mocha": true + }, + "rules": { + "prettier/prettier": 1, + "object-curly-spacing": [1, "always"], + "no-dupe-class-members": 1, + "no-iterator": 1, + "one-var": [1, "never"], + "no-nested-ternary": 1, + "no-restricted-syntax": [1, "WithStatement"], + "no-duplicate-imports": 1, + "no-unneeded-ternary": 1, + "no-case-declarations": 1, + "dot-notation": 1, + "no-useless-constructor": 1, + "eqeqeq": 1, + "object-shorthand": 1, + "prefer-rest-params": 1, + "no-array-constructor": 1, + "array-callback-return": 0, + "no-param-reassign": 1, + "prefer-arrow-callback": 1, + "no-useless-concat": 1, + "prefer-template": 1, + "no-console": 1, + "no-debugger": 1, + "no-var": 1, + "no-trailing-spaces": 0, + "eol-last": 0, + "no-unused-vars": 0, + "no-underscore-dangle": 0, + "no-alert": 0, + "no-lone-blocks": 0, + "import/extensions": 1, + "react/display-name": [1, {"ignoreTranspilerName": false }], + "react/forbid-prop-types": [1, {"forbid": ["any"]}], + "react/jsx-boolean-value": 1, + "react/prefer-stateless-function": 1, + "react/jsx-indent-props": 0, + "react/jsx-key": 1, + "react/jsx-max-props-per-line": 0, + "react/jsx-no-bind": 0, + "react/jsx-no-duplicate-props": 1, + "react/jsx-no-literals": 0, + "react/jsx-no-undef": 1, + "react/jsx-pascal-case": 1, + "react/jsx-sort-prop-types": 0, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 1, + "react/jsx-uses-vars": 1, + "react/no-danger": 1, + "react/no-did-mount-set-state": 1, + "react/no-did-update-set-state": 1, + "react/no-direct-mutation-state": 1, + "react/no-multi-comp": 1, + "react/no-is-mounted": 1, + "react/no-set-state": 0, + "react/no-unknown-property": 1, + "react/prefer-es6-class": 1, + "react/prop-types": 1, + "react/react-in-jsx-scope": 1, + "react/self-closing-comp": 1, + "react/sort-comp": 1 + } +} diff --git a/docker/production/nodejs/frontend/.eslintrc-build b/docker/production/nodejs/frontend/.eslintrc-build new file mode 100644 index 00000000..ff103f98 --- /dev/null +++ b/docker/production/nodejs/frontend/.eslintrc-build @@ -0,0 +1,87 @@ +{ + "extends": [ + "eslint:recommended", + "plugin:import/errors", + "plugin:import/warnings" + ], + "plugins": [ + "react", + "prettier" + ], + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "env": { + "es6": true, + "browser": true, + "node": true, + "jquery": true, + "mocha": true + }, + "rules": { + "prettier/prettier": 2, + "object-curly-spacing": [2, "always"], + "no-dupe-class-members": 2, + "no-iterator": 2, + "one-var": [2, "never"], + "no-nested-ternary": 2, + "no-restricted-syntax": [2, "WithStatement"], + "no-duplicate-imports": 2, + "no-unneeded-ternary": 2, + "no-case-declarations": 2, + "dot-notation": 2, + "no-useless-constructor": 2, + "eqeqeq": 2, + "object-shorthand": 2, + "prefer-rest-params": 2, + "no-array-constructor": 2, + "array-callback-return": 0, + "no-param-reassign": 2, + "prefer-arrow-callback": 2, + "no-useless-concat": 2, + "prefer-template": 2, + "no-console": 2, + "no-debugger": 2, + "no-var": 2, + "no-trailing-spaces": 0, + "eol-last": 0, + "no-unused-vars": 0, + "no-underscore-dangle": 0, + "no-alert": 0, + "no-lone-blocks": 0, + "import/extensions": 2, + "react/display-name": [2, {"ignoreTranspilerName": false }], + "react/forbid-prop-types": [2, {"forbid": ["any"]}], + "react/jsx-boolean-value": 2, + "react/prefer-stateless-function": 2, + "react/jsx-indent-props": 0, + "react/jsx-key": 2, + "react/jsx-max-props-per-line": 0, + "react/jsx-no-bind": 0, + "react/jsx-no-duplicate-props": 2, + "react/jsx-no-literals": 0, + "react/jsx-no-undef": 2, + "react/jsx-pascal-case": 2, + "react/jsx-sort-prop-types": 0, + "react/jsx-sort-props": 0, + "react/jsx-uses-react": 2, + "react/jsx-uses-vars": 2, + "react/no-danger": 2, + "react/no-did-mount-set-state": 2, + "react/no-did-update-set-state": 2, + "react/no-direct-mutation-state": 2, + "react/no-multi-comp": 2, + "react/no-is-mounted": 2, + "react/no-set-state": 0, + "react/no-unknown-property": 2, + "react/prefer-es6-class": 2, + "react/prop-types": 2, + "react/react-in-jsx-scope": 2, + "react/self-closing-comp": 2, + "react/sort-comp": 2 + } +} diff --git a/docker/production/nodejs/frontend/.gitignore b/docker/production/nodejs/frontend/.gitignore new file mode 100644 index 00000000..da641ae1 --- /dev/null +++ b/docker/production/nodejs/frontend/.gitignore @@ -0,0 +1,2 @@ +# Frontend Project Dependencies +node_modules/ diff --git a/docker/production/nodejs/frontend/config/config.js b/docker/production/nodejs/frontend/config/config.js new file mode 100644 index 00000000..9a98b7cb --- /dev/null +++ b/docker/production/nodejs/frontend/config/config.js @@ -0,0 +1,2 @@ +export const CLIENT_IP = '0.0.0.0'; +export const CLIENT_PORT = 6003; diff --git a/docker/production/nodejs/frontend/package.json b/docker/production/nodejs/frontend/package.json new file mode 100644 index 00000000..792676c3 --- /dev/null +++ b/docker/production/nodejs/frontend/package.json @@ -0,0 +1,108 @@ +{ + "name": "cloudCV", + "version": "2.0.0", + "description": "", + "main": "", + "scripts": { + "clean-dist": "npm run remove-dist && mkdir dist", + "remove-dist": "node_modules/.bin/rimraf ./dist", + "build:html": "babel-node server/buildHTML.js", + "build": "npm-run-all clean-dist test lint:build build:html && babel-node server/build.js", + "build:alias": "babel-node server/build.js", + "start": "npm-run-all build && node server/start.js", + "open:src": "babel-node server/srcServer.js", + "lint": "esw webpack.config.* src server", + "lint:build": "node_modules/.bin/esw webpack.config.* src server -c ./.eslintrc-build", + "lint:watch": "npm run lint -- --watch", + "lint:fix": "eslint src server webpack.config.* --fix", + "test": "mocha --reporter spec server/testSetup.js \"src/**/*.test.js\"", + "test:watch": "npm run test -- --watch", + "dev": "node_modules/.bin/babel-node server/startMessage.js && npm-run-all --parallel open:src lint:watch test:watch" + }, + "browserslist": [ + "> 0.5%", + "ie > 9" + ], + "author": "CloudCV Team", + "license": "GPL-3.0", + "dependencies": { + "antd": "^3.1.3", + "axios": "^0.17.1", + "body-parser": "^1.18.2", + "express": "^4.16.2", + "immutable": "^3.8.2", + "nuka-carousel": "^3.0.1", + "prop-types": "^15.6.0", + "raven-js": "^3.22.0", + "react": "^16.0.0", + "react-burger-menu": "^2.2.3", + "react-dom": "^16.2.1", + "react-ga": "^2.4.1", + "react-redux": "^5.0.6", + "react-router-dom": "^4.2.2", + "react-router-redux": "^4.0.6", + "react-tabs": "^2.2.1", + "react-tap-event-plugin": "^3.0.2", + "redux": "^3.7.2", + "redux-thunk": "^2.1.0", + "socket.io": "^2.0.4", + "socket.io-client": "^2.0.4", + "superagent": "^3.8.2", + "to-string-loader": "^1.1.5" + }, + "devDependencies": { + "autoprefixer": "^7.2.5", + "babel-cli": "^6.26.0", + "babel-core": "^6.26.0", + "babel-loader": "^7.1.2", + "babel-plugin-import": "^1.6.3", + "babel-plugin-react-display-name": "^2.0.0", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-polyfill": "^6.26.0", + "babel-preset-es2015": "^6.16.0", + "babel-preset-react": "^6.16.0", + "babel-preset-react-hmre": "^1.1.1", + "babel-register": "^6.26.0", + "base64-font-loader": "^0.0.4", + "colors": "^1.1.2", + "compression": "^1.7.1", + "cross-env": "5.1.3", + "css-loader": "^0.28.8", + "enzyme": "^3.3.0", + "eslint": "^4.15.0", + "eslint-plugin-import": "^2.8.0", + "eslint-plugin-jsx-a11y": "^6.0.3", + "eslint-plugin-prettier": "^2.4.0", + "eslint-plugin-react": "^7.5.1", + "eslint-watch": "^3.1.3", + "eventsource-polyfill": "^0.9.6", + "expect": "^22.0.6", + "expose-loader": "^0.7.4", + "extract-text-webpack-plugin": "3.0.2", + "file-loader": "^1.1.6", + "jsdom": "^11.5.1", + "less": "^2.7.3", + "less-loader": "^4.0.5", + "mocha": "^4.1.0", + "nock": "^9.1.6", + "node-sass": "^4.7.2", + "npm-run-all": "^4.1.2", + "open": "^0.0.5", + "postcss-loader": "^2.0.10", + "prettier": "^1.10.2", + "react-addons-test-utils": "^15.6.2", + "redux-immutable-state-invariant": "^2.1.0", + "redux-mock-store": "^1.4.0", + "rimraf": "^2.6.2", + "sass-loader": "^6.0.6", + "style-loader": "^0.19.1", + "url-loader": "^0.6.2", + "webpack": "^3.10.0", + "webpack-dev-middleware": "^2.0.4", + "webpack-hot-middleware": "^2.21.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/Cloud-CV/CloudCV" + } +} diff --git a/docker/production/nodejs/frontend/postcss.config.js b/docker/production/nodejs/frontend/postcss.config.js new file mode 100644 index 00000000..cc3ac210 --- /dev/null +++ b/docker/production/nodejs/frontend/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require("autoprefixer")] +}; diff --git a/docker/production/nodejs/frontend/server/build.js b/docker/production/nodejs/frontend/server/build.js new file mode 100644 index 00000000..74371241 --- /dev/null +++ b/docker/production/nodejs/frontend/server/build.js @@ -0,0 +1,35 @@ +/*eslint-disable no-console */ + +import webpack from "webpack"; +import webpackConfig from "../webpack.config.prod"; +import colors from "colors"; + +process.env.NODE_ENV = "production"; + +console.log( + "Generating minified bundle for production. This will take a moment...".blue +); + +webpack(webpackConfig).run((err, stats) => { + if (err) { + console.log(err.bold.red); + return 1; + } + + const jsonStats = stats.toJson(); + + if (jsonStats.hasErrors) { + return jsonStats.errors.map(error => console.log(error.red)); + } + + if (jsonStats.hasWarnings) { + console.log("Webpack generate the following warnings".bold.yellow); + jsonStats.warnings.map(warning => console.log(warning.yellow)); + } + + console.log( + "App compiled in production mode and written to /dist. Ready to roll!".green + ); + + return 0; +}); diff --git a/docker/production/nodejs/frontend/server/buildHTML.js b/docker/production/nodejs/frontend/server/buildHTML.js new file mode 100644 index 00000000..2a93c740 --- /dev/null +++ b/docker/production/nodejs/frontend/server/buildHTML.js @@ -0,0 +1,21 @@ +/*eslint-disable no-console */ + +import fs from "fs"; +import cheerio from "cheerio"; +import colors from "colors"; + +fs.readFile("src/index.html", "utf8", (err, markup) => { + if (err) { + return console.log(err.red); + } + + const $ = cheerio.load(markup); + $("head").prepend(''); + + fs.writeFile("dist/index.html", $.html(), "utf8", err => { + if (err) { + return console.log(err.red); + } + console.log("index.html written to /dist".blue); + }); +}); diff --git a/docker/production/nodejs/frontend/server/distServer.js b/docker/production/nodejs/frontend/server/distServer.js new file mode 100644 index 00000000..adc96fc5 --- /dev/null +++ b/docker/production/nodejs/frontend/server/distServer.js @@ -0,0 +1,30 @@ +/* eslint-disable no-console */ + +import express from "express"; +import path from "path"; +import compression from "compression"; +import bodyParser from "body-parser"; + +const appConfig = require("../config/config"); +const port = appConfig.CLIENT_PORT || 8080; +const clientIP = appConfig.CLIENT_IP || "0.0.0.0"; +const app = express(); + +const http = require("http").Server(app); + +app.use(bodyParser.urlencoded({ limit: "500mb", extended: true })); +app.use(bodyParser.json({ limit: "500mb" })); +app.use(compression()); +app.use(express.static("dist")); + +app.get("*", (req, res) => { + res.sendFile(path.resolve(__dirname, "../dist/index.html")); +}); + +http.listen(port, err => { + if (err) { + console.log(err); + } else { + console.log(`visit http://${clientIP}:${port}`); + } +}); diff --git a/docker/production/nodejs/frontend/server/srcServer.js b/docker/production/nodejs/frontend/server/srcServer.js new file mode 100644 index 00000000..98e31b91 --- /dev/null +++ b/docker/production/nodejs/frontend/server/srcServer.js @@ -0,0 +1,40 @@ +/* eslint-disable no-console */ + +import express from "express"; +import webpack from "webpack"; +import path from "path"; +import config from "../webpack.config.dev"; +import bodyParser from "body-parser"; +import colors from "colors"; + +const appConfig = require("../config/config"); +const port = appConfig.CLIENT_PORT || 8080; +const clientIP = appConfig.CLIENT_IP || "0.0.0.0"; +const app = express(); +const compiler = webpack(config); + +const http = require("http").Server(app); + +app.use(bodyParser.urlencoded({ limit: "500mb", extended: true })); +app.use(bodyParser.json({ limit: "500mb" })); + +app.use( + require("webpack-dev-middleware")(compiler, { + noInfo: true, + publicPath: config.output.publicPath + }) +); + +app.use(require("webpack-hot-middleware")(compiler)); + +app.get("*", (req, res) => { + res.sendFile(path.resolve(__dirname, "../src/index.html")); +}); + +http.listen(port, err => { + if (err) { + console.log(err); + } else { + console.log(`visit http://${clientIP}:${port}`.yellow); + } +}); diff --git a/docker/production/nodejs/frontend/server/start.js b/docker/production/nodejs/frontend/server/start.js new file mode 100644 index 00000000..7178f208 --- /dev/null +++ b/docker/production/nodejs/frontend/server/start.js @@ -0,0 +1,2 @@ +require("babel-core/register"); +require("./distServer.js"); diff --git a/docker/production/nodejs/frontend/server/startMessage.js b/docker/production/nodejs/frontend/server/startMessage.js new file mode 100644 index 00000000..dce981dd --- /dev/null +++ b/docker/production/nodejs/frontend/server/startMessage.js @@ -0,0 +1,5 @@ +import colors from "colors"; + +/* eslint-disable no-console */ + +console.log("Starting app in dev mode...".green); diff --git a/docker/production/nodejs/frontend/server/testSetup.js b/docker/production/nodejs/frontend/server/testSetup.js new file mode 100644 index 00000000..aa4afe4c --- /dev/null +++ b/docker/production/nodejs/frontend/server/testSetup.js @@ -0,0 +1,34 @@ +/* eslint-disable no-var*/ +process.env.NODE_ENV = "test"; + +require("babel-register")(); + +require.extensions[".css"] = function() { + return null; +}; +require.extensions[".png"] = function() { + return null; +}; +require.extensions[".jpg"] = function() { + return null; +}; + +const jsdom = require("jsdom"); +const { JSDOM } = jsdom; +var exposedProperties = ["window", "navigator", "document"]; + +const { window } = new JSDOM("..."); +const { document } = new JSDOM("...").window; + +Object.keys(document.defaultView).forEach(property => { + if (typeof global[property] === "undefined") { + exposedProperties.push(property); + global[property] = document.defaultView[property]; + } +}); + +global.navigator = { + userAgent: "node.js" +}; + +documentRef = document; //eslint-disable-line no-undef diff --git a/docker/production/nodejs/frontend/src/actions/actionTypes.js b/docker/production/nodejs/frontend/src/actions/actionTypes.js new file mode 100644 index 00000000..a24cefc2 --- /dev/null +++ b/docker/production/nodejs/frontend/src/actions/actionTypes.js @@ -0,0 +1 @@ +export const TOGGLE_SHOW_QUEUE = "TOGGLE_SHOW_QUEUE"; diff --git a/docker/production/nodejs/frontend/src/actions/sidebarActions.js b/docker/production/nodejs/frontend/src/actions/sidebarActions.js new file mode 100644 index 00000000..26443d2e --- /dev/null +++ b/docker/production/nodejs/frontend/src/actions/sidebarActions.js @@ -0,0 +1,14 @@ +import * as types from "./actionTypes"; + +function toggleShowQueueSuccess(value) { + return { type: types.TOGGLE_SHOW_QUEUE, value }; +} + +export function toggleShowQueue(value) { + return function(dispatch) { + return new Promise((resolve, reject) => { + dispatch(toggleShowQueueSuccess(value)); + resolve("dispatch showQueue value change call"); + }); + }; +} diff --git a/docker/production/nodejs/frontend/src/actions/store.test.js b/docker/production/nodejs/frontend/src/actions/store.test.js new file mode 100644 index 00000000..9092e518 --- /dev/null +++ b/docker/production/nodejs/frontend/src/actions/store.test.js @@ -0,0 +1,10 @@ +import expect from "expect"; +import { createStore } from "redux"; +import rootReducer from "../reducers"; +import initialState from "../reducers/initialState"; + +describe("Store Test", () => { + it("dummy test - let it pass", () => { + expect([1, 2]).toEqual([1, 2]); + }); +}); diff --git a/docker/production/nodejs/frontend/src/components/App.js b/docker/production/nodejs/frontend/src/components/App.js new file mode 100644 index 00000000..c3186be8 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/App.js @@ -0,0 +1,30 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Link, Route, Switch, Miss } from "react-router-dom"; +import HomePageComponent from "./home"; +import TeamPageComponent from "./team"; +import ProjectPageComponent from "./projects"; +import AnalyticsComponent from "./analytics"; +import PageNotFoundHandler from "./PageNotFoundHandler"; +import Navbar from "./navbar"; + +const App = props => { + return ( +
+ +
+ + + + + + + + + +
+
+ ); +}; + +export default App; diff --git a/docker/production/nodejs/frontend/src/components/PageNotFoundHandler.js b/docker/production/nodejs/frontend/src/components/PageNotFoundHandler.js new file mode 100644 index 00000000..4b43705e --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/PageNotFoundHandler.js @@ -0,0 +1,11 @@ +import React from "react"; + +const PageNotfoundHandler = () => { + return ( +
+

404!

+
+ ); +}; + +export default PageNotfoundHandler; diff --git a/docker/production/nodejs/frontend/src/components/analytics/index.js b/docker/production/nodejs/frontend/src/components/analytics/index.js new file mode 100644 index 00000000..7e89257f --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/analytics/index.js @@ -0,0 +1,38 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { withRouter } from "react-router-dom"; +import ReactGA from "react-ga"; + +class Analytics extends React.Component { + constructor(props) { + super(props); + this.state = {}; + this.isProduction = process.env.NODE_ENV === "production"; + this.recordPageView = this.recordPageView.bind(this); + } + + componentDidMount() { + if (this.isProduction) this.recordPageView(); + } + + componentDidUpdate(prevProps, prevState) { + if (this.isProduction) this.recordPageView(); + } + + recordPageView() { + ReactGA.set({ page: window.location.pathname + window.location.search }); + ReactGA.pageview(window.location.pathname + window.location.search); + } + + render() { + return null; + } +} + +Analytics.propTypes = { + match: PropTypes.object.isRequired, + location: PropTypes.object.isRequired, + history: PropTypes.object.isRequired +}; + +export default withRouter(Analytics); diff --git a/docker/production/nodejs/frontend/src/components/common/Button.js b/docker/production/nodejs/frontend/src/components/common/Button.js new file mode 100644 index 00000000..e9448dfe --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/common/Button.js @@ -0,0 +1,31 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const Button = props => { + const { extraClass, themeClass } = props; + return ( + + ); +}; + +const themeClassOptions = ["cv-button-light", "cv-button-dark"]; + +Button.defaultProps = { + children: null, + extraClass: "", + themeClass: themeClassOptions[0] +}; + +Button.propTypes = { + children: PropTypes.string, + extraClass: PropTypes.string, + onClick: PropTypes.func, + themeClass: PropTypes.oneOf(themeClassOptions) +}; + +export default Button; diff --git a/docker/production/nodejs/frontend/src/components/common/Card.js b/docker/production/nodejs/frontend/src/components/common/Card.js new file mode 100644 index 00000000..3df39384 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/common/Card.js @@ -0,0 +1,35 @@ +import React from "react"; +import PropTypes from "prop-types"; + +const Card = props => { + const { themeClass, extraClass } = props; + return ( +
+ {props.children} +
+ ); +}; + +const themeClassOptions = [ + "cv-card-light", + "cv-card-dark", + "cv-card-deep-light" +]; + +Card.defaultProps = { + children: null, + extraClass: "", + themeClass: themeClassOptions[0] +}; + +Card.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + PropTypes.array + ]), + extraClass: PropTypes.string, + themeClass: PropTypes.oneOf(themeClassOptions) +}; + +export default Card; diff --git a/docker/production/nodejs/frontend/src/components/common/Preloader.js b/docker/production/nodejs/frontend/src/components/common/Preloader.js new file mode 100644 index 00000000..e68a2d62 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/common/Preloader.js @@ -0,0 +1,11 @@ +import React from "react"; + +const Preloader = () => { + return ( +
+
+
+ ); +}; + +export default Preloader; diff --git a/docker/production/nodejs/frontend/src/components/common/TimelineCard.js b/docker/production/nodejs/frontend/src/components/common/TimelineCard.js new file mode 100644 index 00000000..12387c53 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/common/TimelineCard.js @@ -0,0 +1,27 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Card from "./Card"; + +const TimelineCard = props => { + const { date } = props; + return ( +
  • +
    + {date} +
    +
    + {props.children} +
  • + ); +}; + +TimelineCard.propTypes = { + date: PropTypes.string.isRequired, + children: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + PropTypes.array + ]) +}; + +export default TimelineCard; diff --git a/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselDecorators.js b/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselDecorators.js new file mode 100644 index 00000000..f759677b --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselDecorators.js @@ -0,0 +1,22 @@ +import CarouselLeft from "./CarouselLeft"; +import CarouselRight from "./CarouselRight"; +import CarouselMid from "./CarouselMid"; +import createReactClass from "create-react-class"; +import React from "react"; + +const CarouselDecorators = [ + { + component: CarouselLeft, + position: "CenterLeft" + }, + { + component: CarouselRight, + position: "CenterRight" + }, + { + component: CarouselMid, + position: "BottomCenter" + } +]; + +export default CarouselDecorators; diff --git a/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselLeft.js b/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselLeft.js new file mode 100644 index 00000000..2ce351ba --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselLeft.js @@ -0,0 +1,42 @@ +import React from "react"; +import left from "../../../images/ic_chevron_left.svg"; +import PropTypes from "prop-types"; + +class CarouselLeft extends React.Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + handleClick(e) { + e.preventDefault(); + this.props.previousSlide(); + } + getButtonStyles(disabled) { + return { + border: 0, + color: "white", + outline: 0, + opacity: disabled ? 0.3 : 1, + cursor: "pointer" + }; + } + render() { + return ( + + ); + } +} +CarouselLeft.propTypes = { + currentSlide: PropTypes.number.isRequired, + previousSlide: PropTypes.func.isRequired, + wrapAround: PropTypes.bool.isRequired +}; + +export default CarouselLeft; diff --git a/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselMid.js b/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselMid.js new file mode 100644 index 00000000..0d4df0ca --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselMid.js @@ -0,0 +1,73 @@ +import React from "react"; +import PropTypes from "prop-types"; + +class CarouselMid extends React.Component { + constructor(props) { + super(props); + this.getIndexes = this.getIndexes.bind(this); + this.getListStyles = this.getListStyles.bind(this); + this.getListItemStyles = this.getListItemStyles.bind(this); + this.getButtonStyles = this.getButtonStyles.bind(this); + } + getIndexes(count, inc) { + let arr = []; + for (let i = 0; i < count - (this.props.slidesToShow - 1); i += inc) { + arr.push(i); + } + return arr; + } + getListStyles() { + return { + position: "relative", + margin: 0, + top: -10, + padding: 0 + }; + } + getListItemStyles() { + return { + listStyleType: "none", + display: "inline-block" + }; + } + getButtonStyles(active) { + return { + border: 0, + background: "transparent", + color: "black", + cursor: "pointer", + padding: 10, + outline: 0, + fontSize: 24, + opacity: active ? 1 : 0.5 + }; + } + render() { + let self = this; + let indexes = this.getIndexes( + self.props.slideCount, + self.props.slidesToScroll + ); + return ( + + ); + } +} +CarouselMid.propTypes = { + slidesToShow: PropTypes.number.isRequired +}; + +export default CarouselMid; diff --git a/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselRight.js b/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselRight.js new file mode 100644 index 00000000..60e644f3 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/common/carouselDecorators/CarouselRight.js @@ -0,0 +1,48 @@ +import React from "react"; +import right from "../../../images/ic_chevron_right.svg"; +import PropTypes from "prop-types"; + +class CarouselRight extends React.Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + handleClick(e) { + e.preventDefault(); + this.props.nextSlide(); + } + getButtonStyles(disabled) { + return { + border: 0, + color: "white", + outline: 0, + opacity: disabled ? 0.3 : 1, + cursor: "pointer" + }; + } + render() { + return ( + = + this.props.slideCount && !this.props.wrapAround + )} + /> + ); + } +} +CarouselRight.propTypes = { + currentSlide: PropTypes.number.isRequired, + nextSlide: PropTypes.func.isRequired, + wrapAround: PropTypes.bool.isRequired, + slidesToScroll: PropTypes.number.isRequired, + slideCount: PropTypes.number.isRequired, + slidesToShow: PropTypes.number.isRequired +}; + +export default CarouselRight; diff --git a/docker/production/nodejs/frontend/src/components/home/HomeBody.js b/docker/production/nodejs/frontend/src/components/home/HomeBody.js new file mode 100644 index 00000000..80687fc1 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/HomeBody.js @@ -0,0 +1,87 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import HomeAboutUs from "./homeBody/HomeAboutUs"; +import HomeNews from "./homeBody/HomeNews"; +import HomeDemos from "./homeBody/HomeDemos"; +import HomeSponsors from "./homeBody/HomeSponsors"; +import HomeShowcase from "./homeBody/HomeShowcase"; +import HomeContactUs from "./homeBody/HomeContactUs"; +import Button from "../common/Button"; + +const HomeBody = props => { + const PROJECT_HEADING = "Projects"; + const GITHUB_DESC = "Interested in contributing?"; + const GITHUB_STRING = + "We are looking for active open source contributors who could help us taking the projects to next level. Please feel free to visit following links if you are interested in joining our team"; + const GITHUB_IMAGE = require("../../images/github_logo.png"); + const GITTER_IMAGE = require("../../images/gitter_logo.png"); + const GSOC_LOGO = require("../../images/gsoc_logo.png"); + const GCI_LOGO = require("../../images/gci_logo.png"); + return ( +
    + + + + + +
    +

    {GITHUB_DESC}

    +
    +

    {GITHUB_STRING}

    +
    + + GSOC Logo +

    GSoC

    + + + GCI Logo +

    GCI

    + + + GitHub Logo +

    GitHub

    + + + Gitter Logo +

    Gitter

    + +
    +
    + +
    + ); +}; + +export default HomeBody; diff --git a/docker/production/nodejs/frontend/src/components/home/HomeFooter.js b/docker/production/nodejs/frontend/src/components/home/HomeFooter.js new file mode 100644 index 00000000..19245032 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/HomeFooter.js @@ -0,0 +1,12 @@ +import React from "react"; + +const HomeFooter = props => { + const IMAGE = require("../../images/cloudcv_footer.png"); + return ( +
    + CloudCV Footer +
    + ); +}; + +export default HomeFooter; diff --git a/docker/production/nodejs/frontend/src/components/home/HomeHeader.js b/docker/production/nodejs/frontend/src/components/home/HomeHeader.js new file mode 100644 index 00000000..8930c3ac --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/HomeHeader.js @@ -0,0 +1,14 @@ +import React from "react"; + +const HomeHeader = props => { + const DESC_LINE = "Building platforms for reproducible AI Research"; + return ( +
    +

    CLOUDCV

    +
    + {DESC_LINE} +
    + ); +}; + +export default HomeHeader; diff --git a/docker/production/nodejs/frontend/src/components/home/homeBody/HomeAboutUs.js b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeAboutUs.js new file mode 100644 index 00000000..082b8da7 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeAboutUs.js @@ -0,0 +1,25 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import Card from "../../common/Card"; + +const HomeAboutUs = props => { + const ABOUT_US_TITLE = "WHO ARE WE"; + const ABOUT_US_DESC = + "CloudCV is a young open source cloud platform started in 2013 by students and faculty from Machine Learning and Perception Lab at Virginia Tech (now at Georgia Tech) with the aim to make AI research more reproducible. At CloudCV, we are building tools that enable researchers to build, compare and share state-of-the-art algorithms. We believe that one shouldn't have to be an AI expert to have access to cutting edge vision algorithms. Likewise, researchers shouldn't have to worry about building a service around their deep learning models to showcase and share it with others."; + return ( +
    +
    + +

    + {ABOUT_US_TITLE} +     + ? +

    +

    {ABOUT_US_DESC}

    +
    +
    +
    + ); +}; + +export default HomeAboutUs; diff --git a/docker/production/nodejs/frontend/src/components/home/homeBody/HomeContactUs.js b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeContactUs.js new file mode 100644 index 00000000..e4a300b3 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeContactUs.js @@ -0,0 +1,103 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import Button from "../../common/Button"; +import axios from "axios"; +const AJAX_ROOT = process.env.AJAX_ROOT; + +const HomeContactUs = props => { + const CONTACT_US_TITLE = "Contact Us"; + const FILE_UPLOAD_TEXT = "Choose File"; + const FILE_UPLOADED_TEXT = "Image Uploaded"; + let inputFileTextElement; + + const sendResponse = event => { + event.preventDefault(); + let formElement = document.getElementById("contact-us-form"); + let formData = new FormData(formElement); + axios({ + method: "post", + url: `${process.env.AJAX_ROOT}/api/web/contact/`, + data: formData + }).then(response => { + if (response.status === 201) { + formElement.reset(); + document.getElementById("cv-home-form-success").innerHTML = + response.data.message; + } + }); + }; + + const updateFileUploadText = event => { + event.preventDefault(); + inputFileTextElement.innerHTML = FILE_UPLOADED_TEXT; + }; + + return ( +
    +

    {CONTACT_US_TITLE}

    +
    +
    + +
    + +
    + +
    + +
    + + { + inputFileTextElement = element; + }} + > + {FILE_UPLOAD_TEXT} + +
    +
    + +
    + + +
    + ); +}; + +export default HomeContactUs; diff --git a/docker/production/nodejs/frontend/src/components/home/homeBody/HomeDemos.js b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeDemos.js new file mode 100644 index 00000000..8cba244e --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeDemos.js @@ -0,0 +1,152 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import axios from "axios"; +import Card from "../../common/Card"; +import Button from "../../common/Button"; +import Preloader from "../../common/Preloader"; +import Carousel from "nuka-carousel"; +import CarouselDecorators from "../../common/carouselDecorators/CarouselDecorators"; + +class HomeDemos extends React.Component { + constructor(props) { + super(props); + this.state = { + isFetching: true, + demos: [] + }; + this.fetchDemos = this.fetchDemos.bind(this); + this.fetchDemos(); + } + + componentDidMount() { + this.updateCarousel(); + window.addEventListener("resize", this.updateCarousel.bind(this)); + } + + componentDidUpdate() { + this.equalizeCards(); + } + + componentWillUnmount() { + window.removeEventListener("resize", this.updateCarousel.bind(this)); + } + + updateCarousel() { + const SLIDES_TO_SHOW_SMALL = 1; + const SLIDES_TO_SHOW_LARGE = 2; + this.setState({ + slidesToShow: + window.innerWidth > 800 ? SLIDES_TO_SHOW_LARGE : SLIDES_TO_SHOW_SMALL + }); + } + + equalizeCards() { + const cards = document.getElementsByClassName( + "cv-home-demos-carousel-card" + ); + const list = document.getElementsByClassName("slider-list"); + const frame = document.getElementsByClassName("slider-frame"); + if (frame[0] !== undefined) { + const cardWidth = frame[0].offsetWidth / this.state.slidesToShow; + for (let i = 0; i < cards.length; i++) { + cards[i].style.height = "auto"; + cards[i].style.width = cardWidth.toString().concat("px"); + } + let max = 0; + for (let i = 0; i < cards.length; i++) { + if (cards[i].offsetHeight > max) max = cards[i].offsetHeight; + } + max = max.toString().concat("px"); + for (let i = 0; i < cards.length; i++) { + cards[i].style.height = max; + } + frame[0].style.height = max; + list[0].style.height = max; + } + } + + fetchDemos() { + axios + .get(`${process.env.AJAX_ROOT}/api/demos/demos/`) + .then(response => { + this.setState({ demos: response.data, isFetching: false }); + }) + .catch(error => { + //Redirect + }); + } + + makeDescription(description) { + const limit = 80; + if (description.length > limit) + return description.substring(0, limit).concat("..."); + return description; + } + + renderCards(demos, carousel) { + return demos.map((demo, index) => { + return ( + +
    {demo.title}
    +
    + {this.makeDescription(demo.description)} +
    +
    + + + + + + +
    +
    + ); + }); + } + + render() { + const DEMOS_TITLE = "DEMOS"; + const CAROUSEL = this.state.demos.length > 2; + return ( +
    + {!this.state.isFetching && ( +
    +

    {DEMOS_TITLE}

    + {CAROUSEL ? ( + + {this.renderCards(this.state.demos, CAROUSEL)} + + ) : ( +
    + {this.renderCards(this.state.demos)} +
    + )} +
    + )} +
    + ); + } +} + +export default HomeDemos; diff --git a/docker/production/nodejs/frontend/src/components/home/homeBody/HomeNews.js b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeNews.js new file mode 100644 index 00000000..eff32fae --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeNews.js @@ -0,0 +1,288 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import TimelineCard from "../../common/TimelineCard"; +import Button from "../../common/Button"; + +const MIN_ELEMENTS = 18; +const EVENTS = [ + { + date: "March 2021", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GSOC 2021 + + + ) + }, + { + date: "March 2020", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GSOC 2020 + + + ) + }, + + { + date: "October 2019", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GCI 2019 + + + ) + }, + + { + date: "September 2019", + desc: ( + + EvalAI and Fabrik are accepted to{" "} + + SOSP Conference 2019 + , Canada + + ) + }, + + { + date: "March 2019", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GSOC 2019 + + + ) + }, + + { + date: "September 2018", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GCI 2018 + + + ) + }, + + { + date: "March 2018", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GSOC 2018 + + + ) + }, + { + date: "November 2017", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GCI 2017 + + + ) + }, + { + date: "June 2017", + desc: ( + + CloudCV selected for demo presentation at{" "} + + CVPR 2017 + + , Hawaii + + ) + }, + { + date: "May 2017", + desc: ( + + CloudCV selected as a semifinalist at{" "} + + LDV Vision Summit 2017 + + , New York City + + ) + }, + { + date: "March 2017", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GSoC 2017 + + + ) + }, + { + date: "May 2016", + desc: ( + + CloudCV selected for demo presentation at{" "} + + CVPR 2016 + + , Las Vegas, NV + + ) + }, + { + date: "March 2016", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GSoC 2016 + + + ) + }, + { + date: "March 2015", + desc: ( + + CloudCV selected as a mentoring organisation for{" "} + + GSoC 2015 + + + ) + }, + { + date: "January 2014", + desc: NVIDIA supports CloudCV with a GPU hardware donation + }, + { + date: "October 2013", + desc: ( + + CloudCV has been awarded Amazon Web Services (AWS) in Education: Machine + Learning Research Grant + + ) + } +]; + +class HomeNews extends React.Component { + constructor(props) { + super(props); + this.state = { + showFull: EVENTS.length <= MIN_ELEMENTS + }; + this.loadMore = this.loadMore.bind(this); + } + + loadMore(event) { + this.setState({ showFull: true }); + } + + render() { + const EVENT_ELEMENTS = this.state.showFull + ? EVENTS + : EVENTS.slice(0, MIN_ELEMENTS); + return ( +
    +
    +

    News

    +
    +
    +
      + {EVENT_ELEMENTS.map((event, index) => { + return ( + + {event.desc} + + ); + })} +
    + {!this.state.showFull && ( + + )} +
    +
    + ); + } +} + +export default HomeNews; diff --git a/docker/production/nodejs/frontend/src/components/home/homeBody/HomeShowcase.js b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeShowcase.js new file mode 100644 index 00000000..edef550e --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeShowcase.js @@ -0,0 +1,72 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import axios from "axios"; +import Card from "../../common/Card"; +import Button from "../../common/Button"; +import Preloader from "../../common/Preloader"; + +class HomeShowcase extends React.Component { + constructor(props) { + super(props); + this.state = { + isFetching: true, + projects: [] + }; + this.fetchProjects = this.fetchProjects.bind(this); + this.fetchProjects(); + } + + fetchProjects() { + axios + .get(`${process.env.AJAX_ROOT}/api/demos/projects/`) + .then(response => { + this.setState({ projects: response.data, isFetching: false }); + }) + .catch(error => { + //Redirect + }); + } + + render() { + const SHOWCASE_TITLE = "PROJECTS"; + return ( +
    + {!this.state.isFetching && ( +
    +

    {SHOWCASE_TITLE}

    +
    + {this.state.projects.map((project, index) => { + return ( + +
    + +
    +
    + + + + + + +
    +
    + ); + })} +
    +
    + )} +
    + ); + } +} + +export default HomeShowcase; diff --git a/docker/production/nodejs/frontend/src/components/home/homeBody/HomeSponsors.js b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeSponsors.js new file mode 100644 index 00000000..c6ee9fbe --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/homeBody/HomeSponsors.js @@ -0,0 +1,86 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import Card from "../../common/Card"; + +const HomeSponsors = props => { + const SUPPORT_TITLE = "WE FAVOR OPEN SOURCE"; + const SUPPORT_DESC = + "We use a lot of open source softwares as the basis for our work. Some of them are:"; + const SUPPORTERS = [ + { + title: "Django", + image: require("../../../images/django_logo.svg"), + link: "https://www.djangoproject.com/" + }, + { + title: "Python", + image: require("../../../images/python_logo.png"), + link: "https://www.python.org/" + }, + { + title: "React", + image: require("../../../images/reactjs_logo.svg"), + link: "https://facebook.github.io/react/" + }, + { + title: "Angular", + image: require("../../../images/angular_logo.svg"), + link: "https://angular.io/" + }, + { + title: "Pytorch", + image: require("../../../images/pytorch_logo.png"), + link: "http://pytorch.org/" + }, + { + title: "Keras", + image: require("../../../images/keras_logo.jpg"), + link: "https://keras.io/" + } + ]; + const SPONSOR_TITLE = "SPONSORS"; + const SPONSORS = [ + require("../../../images/supporter_gt.png"), + require("../../../images/supporter_virginia.png"), + require("../../../images/supporter_aws.png"), + require("../../../images/supporter_azure.png"), + require("../../../images/supporter_nvidia.png") + ]; + return ( +
    +

    {SUPPORT_TITLE}

    +
    +
    +

    {SUPPORT_DESC}

    +
    + {SUPPORTERS.map((company, index) => { + return ( + + + {company.title} +

    {company.title}

    + +
    + ); + })} +
    +
    +

    {SPONSOR_TITLE}

    +
    +
    + {SPONSORS.map((company, index) => { + return ( + + ); + })} +
    +
    + ); +}; + +export default HomeSponsors; diff --git a/docker/production/nodejs/frontend/src/components/home/index.js b/docker/production/nodejs/frontend/src/components/home/index.js new file mode 100644 index 00000000..314efdf3 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/home/index.js @@ -0,0 +1,75 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { Link } from "react-router-dom"; +import HomeHeader from "./HomeHeader"; +import HomeBody from "./HomeBody"; +import HomeFooter from "./HomeFooter"; +import Button from "../common/Button"; + +class HomePage extends React.Component { + constructor(props) { + super(props); + this.state = {}; + this.hashLinkScroll = this.hashLinkScroll.bind(this); + this.scrollToAboutUs = this.scrollToAboutUs.bind(this); + } + + componentDidMount() { + this.hashLinkScroll(); + } + + componentDidUpdate(prevProps, prevState) { + this.hashLinkScroll(); + } + + hashLinkScroll() { + const { path } = this.props.match; + if (path !== "/") { + setTimeout(() => { + const id = path.replace("/", ""); + const element = document.getElementById(id); + if (element) + element.scrollIntoView({ + behavior: "smooth" + }); + }, 0); + } else { + setTimeout(() => { + const element = document.body; + if (element) + element.scrollIntoView({ + behavior: "smooth" + }); + }, 0); + } + } + + scrollToAboutUs() { + const element = document.getElementById("about"); + if (element) + element.scrollIntoView({ + behavior: "smooth" + }); + } + + render() { + return ( +
    + +
    + +
    + + +
    + ); + } +} + +HomePage.propTypes = { + match: PropTypes.object.isRequired +}; + +export default HomePage; diff --git a/docker/production/nodejs/frontend/src/components/navbar/NavbarItem.js b/docker/production/nodejs/frontend/src/components/navbar/NavbarItem.js new file mode 100644 index 00000000..2913ada0 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/navbar/NavbarItem.js @@ -0,0 +1,18 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { withRouter } from "react-router-dom"; + +const NavbarItem = props => { + let activeClass = props.active ? "active" : ""; + + return ( +
  • {props.children}
  • + ); +}; + +NavbarItem.propTypes = { + active: PropTypes.bool.isRequired, + children: PropTypes.object.isRequired +}; + +export default NavbarItem; diff --git a/docker/production/nodejs/frontend/src/components/navbar/index.js b/docker/production/nodejs/frontend/src/components/navbar/index.js new file mode 100644 index 00000000..b827d0e5 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/navbar/index.js @@ -0,0 +1,150 @@ +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { bindActionCreators } from "redux"; +import * as sidebarActions from "../../actions/sidebarActions"; +import { Link, withRouter } from "react-router-dom"; +import NavbarItem from "./NavbarItem"; + +class Navbar extends Component { + constructor(props) { + super(props); + this.state = { + isOpen: false, + isOnTop: + (window.pageYOffset || + document.documentElement.scrollTop || + document.body.scrollTop) === 0 + }; + this.handleClick = this.handleClick.bind(this); + this.handleScroll = this.handleScroll.bind(this); + this.showSidebarOnProjectsPage = this.showSidebarOnProjectsPage.bind(this); + window.addEventListener("scroll", this.handleScroll); + } + + componentWillUnmount() { + window.removeEventListener("scroll"); + } + + showSidebarOnProjectsPage() { + this.props.sidebarActions.toggleShowQueue(!this.props.showQueue); + } + + handleScroll(event) { + let isOnTop = + (window.pageYOffset || + document.documentElement.scrollTop || + document.body.scrollTop) === 0; + this.setState({ isOnTop }); + } + + handleClick() { + this.setState({ isOpen: !this.state.isOpen }); + } + + render() { + let x = this.props.location.pathname.indexOf("/", 1); + let firstPath; + if (x !== -1) { + firstPath = this.props.location.pathname.substr(1, x - 1); + } else { + firstPath = this.props.location.pathname.substr(1); + } + let projectActive = firstPath === "projects"; + let listItems = [ + "", + // "Projects", + "GSoC", + "GCI", + "Team", + "Contribute", + "Contact Us" + ]; + listItems = listItems.map((path, index) => { + if (path === "GSoC") { + return ( + + + {path} + + + ); + } + if (path === "GCI") { + return ( + + + {path} + + + ); + } + let active = path.toLowerCase() === firstPath; + let formattedPath = path.toLowerCase().replace(/ /g, "-"); + return ( + + {path === "" ? "Home" : path} + + ); + }); + let listHiddenClass = this.state.isOpen ? "" : "hidden-small"; + let sidebarIconClass = "cv-navbar-list-icon"; + let listIconClass = this.state.isOpen + ? "cv-navbar-close-icon" + : "cv-navbar-list-icon"; + let navbarClass = this.state.isOnTop ? "" : "navbar-box-shadow"; + return ( + + ); + } +} + +Navbar.propTypes = { + match: PropTypes.object.isRequired, + location: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + sidebarActions: PropTypes.object.isRequired, + showQueue: PropTypes.bool.isRequired +}; + +function mapStateToProps(state, ownProps) { + return { + showQueue: state.sidebar.showQueue, + match: ownProps.match, + location: ownProps.location, + history: ownProps.history + }; +} + +function mapDispatchToProps(dispatch) { + return { + sidebarActions: bindActionCreators(sidebarActions, dispatch) + }; +} + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Navbar)); diff --git a/docker/production/nodejs/frontend/src/components/projects/DemoContainer.js b/docker/production/nodejs/frontend/src/components/projects/DemoContainer.js new file mode 100644 index 00000000..79955643 --- /dev/null +++ b/docker/production/nodejs/frontend/src/components/projects/DemoContainer.js @@ -0,0 +1,171 @@ +import React from "react"; +import PropTypes from "prop-types"; +import styles from "../../styles/demo.scss"; +import axios from "axios"; +const AJAX_ROOT = process.env.AJAX_ROOT; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import Button from "../common/Button"; + +class DemoContainer extends React.Component { + constructor(props) { + super(props); + this.state = { + demo: this.props.demo, + height: "auto", + loading: this.props.demo.from_origami + }; + this.receiveMessage = this.receiveMessage.bind(this); + this.sendStyleSheet = this.sendStyleSheet.bind(this); + this.saveInput = this.saveInput.bind(this); + this.saveOutput = this.saveOutput.bind(this); + this.sendBrokenDemo = this.sendBrokenDemo.bind(this); + this.setTimer = this.setTimer.bind(this); + this.log = -1; + window.addEventListener("message", this.receiveMessage, false); + this.setTimer(); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.demo.demo_url !== this.state.demo.demo_url) { + this.setState({ + demo: nextProps.demo, + loading: nextProps.demo.from_origami + }); + this.setTimer(); + } + } + + saveInput(data) { + let formData = new FormData(); + Object.entries(data).forEach(([key, value]) => { + if (value instanceof Blob) formData.set(key, value, key); + else formData.set(key, value); + }); + formData.set("log_type", "Submission"); + axios({ + method: "post", + url: `${process.env.AJAX_ROOT}/api/logs/${ + this.state.demo.permalink + }/add/`, + data: formData + }) + .then(response => { + if (response.status === 200) this.log = response.data.success.id; + }) + .catch(error => {}); + } + + saveOutput(formData) { + if (this.log !== -1) + axios({ + method: "post", + url: `${process.env.AJAX_ROOT}/api/logs/${this.log}/output/`, + data: formData + }) + .then(response => { + if (response.status === 200) this.log = -1; + }) + .catch(error => {}); + } + + sendStyleSheet() { + let iframeWindow = document.getElementsByTagName("iframe")[0].contentWindow; + let stylesheet = { + action: "STYLESHEET_SEND", + styles: styles.toString() + }; + iframeWindow.postMessage(stylesheet, "*"); + } + + receiveMessage(message) { + switch (message.data.action) { + case "DEMO_ONLOAD": + this.sendStyleSheet(); + this.setState({ loading: false }); + break; + case "INPUT_SUBMITTED": + this.saveInput(message.data.payload); + break; + case "OUTPUT_SUBMITTED": + this.saveOutput(message.data.payload); + break; + } + } + + sendBrokenDemo() { + let formData = new FormData(); + formData.set("log_type", "Break"); + axios({ + method: "post", + url: `${process.env.AJAX_ROOT}/api/logs/${ + this.state.demo.permalink + }/add/`, + data: formData + }) + .then(response => {}) + .catch(error => {}); + } + + setTimer() { + setTimeout(() => { + this.setState({ loading: false }); + }, 10000); + } + + render() { + return ( +
    + + + Demo + About + + + {this.state.loading &&
    } +