diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..2b7bafa --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@babel/preset-env", "@babel/preset-react"] +} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..b15bbef --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,37 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": [ + "airbnb", + "eslint:recommended", + "plugin:react/recommended" + ], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2018, + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "indent": [2, 2, { "SwitchCase": 1 }], + "react/jsx-filename-extension": [ + 1, + { + "extensions": [ + ".js", + ".jsx" + ] + } + ] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 99f599c..ce7a251 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ app triathlets .DS_Store package-lock.json +node_modules +**/bundle.js \ No newline at end of file diff --git a/README.md b/README.md index 0ad448e..74b1656 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,12 @@ or amber w ``` +npm watcher + +``` +npm start +``` + run in production: ``` diff --git a/config/application.cr b/config/application.cr index 2fd0767..5f20ed6 100644 --- a/config/application.cr +++ b/config/application.cr @@ -5,6 +5,7 @@ require "amber" require "../src/models/**" require "../src/helpers/**" require "../src/controllers/application_controller" +require "../src/controllers/admin/dashboard_controller" require "../src/controllers/**" # About Application.cr File diff --git a/config/jennifer_test.cr b/config/jennifer_test.cr new file mode 100644 index 0000000..7cbcfd3 --- /dev/null +++ b/config/jennifer_test.cr @@ -0,0 +1,6 @@ +# Jennifer::Config.read("config/database.yml", :test) + +# Jennifer::Config.configure do |conf| +# conf.logger = Logger.new(STDOUT) +# conf.logger.level = Logger::INFO +# end diff --git a/config/routes.cr b/config/routes.cr index b03da94..7138172 100644 --- a/config/routes.cr +++ b/config/routes.cr @@ -19,6 +19,10 @@ Amber::Server.configure do |app| plug Amber::Pipe::Static.new("./public") end + pipeline :api do + plug Amber::Pipe::Logger.new + end + routes :web do get "/", HomeController, :index get "/about", HomeController, :about @@ -29,6 +33,18 @@ Amber::Server.configure do |app| get "/teams/:id", TeamsController, :show end + routes :api, "/api" do + resources "/races", Api::RacesController, only: [:index, :show, :create, :update, :destroy] + resources "/results", Api::ResultsController, only: [:index, :show, :create, :update, :destroy] + resources "/users", Api::UsersController, only: [:index] + resources "/teams", Api::TeamsController, only: [:index] + resources "/cities", Api::CitiesController, only: [:index] + end + + routes :web, "/admin" do + get "/*", Admin::DashboardController, :index + end + routes :static do # Each route is defined as follow # verb resource : String, controller : Symbol, action : Symbol diff --git a/package.json b/package.json new file mode 100644 index 0000000..086c500 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "triathlets", + "version": "1.0.0", + "description": "Triatleths application", + "main": "index.js", + "directories": { + "lib": "lib" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "webpack --mode development --watch", + "watch": "webpack --mode development --watch", + "build": "webpack --mode production", + "lint": "eslint src/**/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/forsaken1/triathlets.git" + }, + "author": "Alexey Krylov", + "license": "ISC", + "bugs": { + "url": "https://github.com/forsaken1/triathlets/issues" + }, + "homepage": "https://github.com/forsaken1/triathlets#readme", + "devDependencies": { + "@babel/core": "^7.4.3", + "@babel/preset-env": "^7.4.3", + "@babel/preset-react": "^7.0.0", + "babel-loader": "^8.0.5", + "css-loader": "^2.1.1", + "eslint": "^5.16.0", + "eslint-config-airbnb": "^17.1.0", + "eslint-plugin-import": "^2.18.0", + "eslint-plugin-jsx-a11y": "^6.2.1", + "eslint-plugin-react": "^7.14.2", + "node-sass": "^4.12.0", + "sass-loader": "^7.1.0", + "style-loader": "^0.23.1", + "webpack": "^4.29.6", + "webpack-cli": "^3.3.0" + }, + "dependencies": { + "bootstrap": "^4.3.1", + "rc-time-picker": "^3.7.1", + "react": "^16.8.6", + "react-bootstrap": "^1.0.0-beta.9", + "react-dom": "^16.8.6", + "react-redux": "^7.1.0", + "react-router-dom": "^5.0.1", + "react-select": "^3.0.4", + "redux": "^4.0.1", + "redux-saga": "^1.0.5" + } +} diff --git a/public/javascripts/main.js b/public/javascripts/main.js deleted file mode 100644 index e69de29..0000000 diff --git a/spec/requests/dashboard_spec.cr b/spec/requests/dashboard_spec.cr new file mode 100644 index 0000000..1e2c31a --- /dev/null +++ b/spec/requests/dashboard_spec.cr @@ -0,0 +1,8 @@ +require "../spec_helper" + +describe "Admin::Dashboard" do + it "renders /admin" do + get "/admin" + response.status_code.should eq 200 + end +end diff --git a/spec/requests/resources_spec.cr b/spec/requests/resources_spec.cr new file mode 100644 index 0000000..e4c721b --- /dev/null +++ b/spec/requests/resources_spec.cr @@ -0,0 +1,47 @@ +require "../spec_helper" + +describe "Admin::Resources" do + describe "GET /admin/resources" do + it "responds successfully" do + get "/admin/resources" + response.status_code.should eq 200 + end + + it "returns users list" do + 5.times { |i| User.create name: "name #{i}" } + get "/admin/resources" + p response + end + end + + # describe "GET /admin/resources/:id" do + # it "responds successfully" do + # resource = User.create name: "test user" + # get "/admin/resources/#{resource.id}" + # response.status_code.should eq 200 + # end + # end + + # describe "POST /admin/resources" do + # it "responds successfully" do + # post "/admin/resources" + # response.status_code.should eq 200 + # end + # end + + # describe "PUT /admin/resources/:id" do + # it "responds successfully" do + # resource = User.create name: "test user" + # put "/admin/resources/#{resource.id}" + # response.status_code.should eq 200 + # end + # end + + # describe "DELETE /admin/resources/:id" do + # it "responds successfully" do + # resource = User.create name: "test user" + # delete "/admin/resources/#{resource.id}" + # response.status_code.should eq 200 + # end + # end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 13c485e..4fbefcc 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,6 +1,16 @@ -require "spec" -require "amber" +require "amber_spec" +require "jennifer/adapter/postgres" +require "jennifer" require "../src/controllers/**" require "../src/mailers/**" require "../src/models/**" -require "../config/*" +require "../src/helpers/**" +require "../config/jennifer_test" + +Spec.before_each do + Jennifer::Adapter.adapter.begin_transaction +end + +Spec.after_each do + Jennifer::Adapter.adapter.rollback_transaction +end diff --git a/src/controllers/admin/dashboard_controller.cr b/src/controllers/admin/dashboard_controller.cr new file mode 100644 index 0000000..0f8154a --- /dev/null +++ b/src/controllers/admin/dashboard_controller.cr @@ -0,0 +1,9 @@ +module Admin + class DashboardController < ApplicationController + LAYOUT = "admin.slang" + + def index + render "admin/dashboard/index.slang" + end + end +end diff --git a/src/controllers/api/application_controller.cr b/src/controllers/api/application_controller.cr new file mode 100644 index 0000000..876ffc9 --- /dev/null +++ b/src/controllers/api/application_controller.cr @@ -0,0 +1,4 @@ +module Api + class ApplicationController < ::ApplicationController + end +end diff --git a/src/controllers/api/cities_controller.cr b/src/controllers/api/cities_controller.cr new file mode 100644 index 0000000..4ef2e56 --- /dev/null +++ b/src/controllers/api/cities_controller.cr @@ -0,0 +1,11 @@ +module Api + class CitiesController < ApplicationController + def index + cities = City.all + + respond_with do + json cities.to_a.map(&.as_json).to_json + end + end + end +end diff --git a/src/controllers/api/races_controller.cr b/src/controllers/api/races_controller.cr new file mode 100644 index 0000000..253dfee --- /dev/null +++ b/src/controllers/api/races_controller.cr @@ -0,0 +1,26 @@ +module Api + class RacesController < ApplicationController + def index + races = Race.all.to_a + respond_with do + json races.to_json + end + end + + def show + race = Race.find! params["id"] + respond_with do + json race.to_json + end + end + + def create + end + + def update + end + + def destroy + end + end +end diff --git a/src/controllers/api/results_controller.cr b/src/controllers/api/results_controller.cr new file mode 100644 index 0000000..c7ec183 --- /dev/null +++ b/src/controllers/api/results_controller.cr @@ -0,0 +1,70 @@ +module Api + class ResultsController < ApplicationController + def index + results = Result.all.order(Result._time.asc).includes(:city, :user, :team, :group).by_race_id race_id + + respond_with do + json results.to_a.map(&.as_json).to_json + end + end + + def show + result = Result.find! params["id"] + + respond_with do + json result.as_json.to_json + end + end + + def create + result = Result.build create_result_params + + respond_with do + if result.save + json result.as_json.to_json + else + json({"errors" => result.errors.to_a}.to_json) + end + end + end + + def update + result = Result.find! params["id"] + + respond_with do + if result.update(update_result_params) + json result.as_json.to_json + else + json({"errors" => result.errors.to_a}.to_json) + end + end + end + + def destroy + result = Result.find! params["id"] + result.destroy + + halt!(200) + end + + private def race_id + params["race_id"]? ? params["race_id"] : nil + end + + private def update_result_params + { + user_id: params[:user_id].to_i, + city_id: params[:city_id].to_i, + team_id: params[:team_id].to_i, + group_id: params[:group_id]? ? params[:group_id] : nil, + time: params[:time], + } + end + + private def create_result_params + update_result_params.merge({ + race_id: params[:race_id].to_i, + }) + end + end +end diff --git a/src/controllers/api/teams_controller.cr b/src/controllers/api/teams_controller.cr new file mode 100644 index 0000000..a47e5da --- /dev/null +++ b/src/controllers/api/teams_controller.cr @@ -0,0 +1,11 @@ +module Api + class TeamsController < ApplicationController + def index + teams = Team.all + + respond_with do + json teams.to_a.map(&.as_json).to_json + end + end + end +end diff --git a/src/controllers/api/users_controller.cr b/src/controllers/api/users_controller.cr new file mode 100644 index 0000000..f6a2af9 --- /dev/null +++ b/src/controllers/api/users_controller.cr @@ -0,0 +1,11 @@ +module Api + class UsersController < ApplicationController + def index + users = User.all + + respond_with do + json users.to_a.map(&.as_json).to_json + end + end + end +end diff --git a/src/helpers/modules/route_helpers.cr b/src/helpers/modules/route_helpers.cr index 3add172..9aa91fd 100644 --- a/src/helpers/modules/route_helpers.cr +++ b/src/helpers/modules/route_helpers.cr @@ -20,6 +20,12 @@ module RouteHelpers "/teams" end + # admin panel routes + + def admin_race_path(race) + "/admin/races/#{race.id}" + end + private def join_attributes(attributes) unless attributes.empty? "?" + attributes.map do |key, value| diff --git a/src/javascript/components/AddResult.js b/src/javascript/components/AddResult.js new file mode 100644 index 0000000..c89d45e --- /dev/null +++ b/src/javascript/components/AddResult.js @@ -0,0 +1,105 @@ +import React, { Component } from "react" +import { connect } from 'react-redux' +import EditTime from "./EditTime.js" +import EditSelect from "./EditSelect.js" +import { listToSelectOptions, findById } from '../lib/func.js' +import { addResult, toggleEditMode } from '../redux/actions.js' + +const defaultState = { + userId: null, + teamId: null, + cityId: null, + time: '0:00:00', + hasError: false +} + +class AddResult extends Component { + constructor(props) { + super(props) + + this.state = defaultState; + this.handleClickAdd = this.handleClickAdd.bind(this) + this.handleChangeUser = this.handleChangeUser.bind(this) + this.handleChangeTeam = this.handleChangeTeam.bind(this) + this.handleChangeCity = this.handleChangeCity.bind(this) + this.handleChangeTime = this.handleChangeTime.bind(this) + } + + handleClickAdd(event) { + event.preventDefault() + + const { addResult, resultsList, usersList, teamsList, citiesList, raceId } = this.props + const { userId, teamId, cityId, time } = this.state + + if(userId == null || teamId == null || cityId == null) { + this.setState({ hasError: true }) + return + } + + const payload = { + user: findById(usersList, userId), + team: findById(teamsList, teamId), + city: findById(citiesList, cityId), + raceId: raceId, + time: time + } + + addResult(payload); + this.setState(defaultState); + } + + handleChangeUser(value) { + this.setState({ userId: value }) + } + + handleChangeTeam(value) { + this.setState({ teamId: value }) + } + + handleChangeCity(value) { + this.setState({ cityId: value }) + } + + handleChangeTime(value) { + this.setState({ time: value }) + } + + render() { + const { usersList, teamsList, citiesList } = this.props + const { userId, teamId, cityId, time, hasError } = this.state + + const usersOptions = listToSelectOptions(usersList) + const teamsOptions = listToSelectOptions(teamsList) + const citiesOptions = listToSelectOptions(citiesList) + + const currentUserOption = userId ? usersOptions.find(item => item.value == userId) : {} + const currentTeamOption = teamId ? teamsOptions.find(item => item.value == teamId) : {} + const currentCityOption = cityId ? citiesOptions.find(item => item.value == cityId) : {} + + const blockClassName = hasError ? 'result result--has-error' : 'result' + + return ( +
+
+ + + + +
+
+
+
+ ) + } +} + +const mapStateToProps = ({ resultsList, usersList, teamsList, citiesList }) => ({ + resultsList, + usersList, + teamsList, + citiesList +}) + +const mapDispatchToProps = { addResult } + +export default connect(mapStateToProps, mapDispatchToProps)(AddResult) \ No newline at end of file diff --git a/src/javascript/components/Cities.js b/src/javascript/components/Cities.js new file mode 100644 index 0000000..39ae2c7 --- /dev/null +++ b/src/javascript/components/Cities.js @@ -0,0 +1,35 @@ +import React, { Component } from "react" +import { connect } from 'react-redux' +import { fetchCities } from '../redux/actions' +import * as Route from '../lib/routes' + +class Cities extends Component { + componentDidMount() { + const { fetchCities } = this.props + + fetchCities() + } + + render() { + const { citiesList } = this.props + + return ( + <> +

Города

+
+ {citiesList.map(user =>
{user.name}
)} +
+ + ) + } +} + +const mapStateToProps = ({ citiesList }) => ({ + citiesList +}) + +const mapDispatchToProps = { + fetchCities +} + +export default connect(mapStateToProps, mapDispatchToProps)(Cities) diff --git a/src/javascript/components/EditSelect.js b/src/javascript/components/EditSelect.js new file mode 100644 index 0000000..6689250 --- /dev/null +++ b/src/javascript/components/EditSelect.js @@ -0,0 +1,37 @@ +import React, { Component } from "react"; +import PropTypes from 'prop-types'; +import Select from 'react-select'; + +class EditSelect extends Component { + constructor(props) { + super(props); + + this.handleChange = this.handleChange.bind(this); + } + + handleChange(option) { + this.props.onChange(option.value); + } + + render() { + const { options, editMode, hasError, currentOption } = this.props; + const blockClassName = hasError ? 'result-attribute result-attribute--has-error' : 'result-attribute' + + return ( +
+ {editMode + ?