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
+ ?
+ :
{currentOption.label}
}
+
+ )
+ }
+}
+
+EditSelect.propTypes = {
+ currentOption: PropTypes.object,
+ options: PropTypes.array,
+ editMode: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired
+}
+
+export default EditSelect;
diff --git a/src/javascript/components/EditSwitcher.js b/src/javascript/components/EditSwitcher.js
new file mode 100644
index 0000000..733eda1
--- /dev/null
+++ b/src/javascript/components/EditSwitcher.js
@@ -0,0 +1,24 @@
+import React, { Component } from "react";
+import PropTypes from 'prop-types';
+import Select from 'react-select';
+
+class EditSwitcher extends Component {
+ render() {
+ const { handler, status } = this.props;
+
+ return (
+
+ {status
+ ?
+ : }
+
+ )
+ }
+}
+
+EditSwitcher.propTypes = {
+ handler: PropTypes.func.isRequired,
+ status: PropTypes.bool.isRequired
+}
+
+export default EditSwitcher;
diff --git a/src/javascript/components/EditTime.js b/src/javascript/components/EditTime.js
new file mode 100644
index 0000000..b21f201
--- /dev/null
+++ b/src/javascript/components/EditTime.js
@@ -0,0 +1,41 @@
+import React, { Component } from "react";
+import PropTypes from 'prop-types';
+import * as moment from 'moment';
+import TimePicker from 'rc-time-picker';
+import 'rc-time-picker/assets/index.css';
+
+class EditTime extends Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(value) {
+ const { onChange } = this.props
+
+ if(onChange) {
+ onChange(value ? value.format(moment.HTML5_FMT.TIME_SECONDS) : '0:00:00');
+ }
+ }
+
+ render() {
+ const { editMode, value } = this.props;
+
+ return (
+
+ {editMode
+ ?
+ :
{value}
}
+
+ )
+ }
+}
+
+EditTime.propTypes = {
+ val: PropTypes.string,
+ editMode: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired
+}
+
+export default EditTime;
diff --git a/src/javascript/components/Header.js b/src/javascript/components/Header.js
new file mode 100644
index 0000000..f4fc6a0
--- /dev/null
+++ b/src/javascript/components/Header.js
@@ -0,0 +1,20 @@
+import React, { Component } from "react"
+import { Navbar, Nav } from 'react-bootstrap'
+import { NavLink } from 'react-router-dom'
+
+const Header = () => (
+
+ Triathlets
+
+
+
+
+
+)
+
+export default Header
diff --git a/src/javascript/components/Race.js b/src/javascript/components/Race.js
new file mode 100644
index 0000000..7fe30e9
--- /dev/null
+++ b/src/javascript/components/Race.js
@@ -0,0 +1,76 @@
+import React, { Component } from "react"
+import { connect } from 'react-redux'
+import PropTypes from 'prop-types'
+import Result from "./Result"
+import AddResult from "./AddResult"
+import * as Route from '../lib/routes'
+import { fetchResults, fetchCities, fetchUsers, fetchTeams } from '../redux/actions'
+
+class Race extends Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ currentRace: null
+ }
+ }
+
+ fetchRace(id) {
+ fetch(Route.racePath(id))
+ .then(response => response.json())
+ .then(data => this.setState({ currentRace: data }))
+ }
+
+ componentDidMount() {
+ const { match: { params }, fetchResults, fetchCities, fetchUsers, fetchTeams } = this.props
+
+ fetchResults(params.id)
+ fetchCities()
+ fetchUsers()
+ fetchTeams()
+
+ this.fetchRace(params.id)
+ }
+
+ render() {
+ const { racesList, resultsList, usersList, teamsList, citiesList, match: { params } } = this.props
+ const { currentRace } = this.state
+
+ return (
+ <>
+ {currentRace && currentRace.title}
+
+
+ {resultsList.map(result => )}
+ >
+ );
+ }
+}
+
+const mapStateToProps = ({ racesList, resultsList, usersList, teamsList, citiesList }) => ({
+ racesList,
+ resultsList,
+ usersList,
+ teamsList,
+ citiesList
+})
+
+const mapDispatchToProps = {
+ fetchResults,
+ fetchCities,
+ fetchUsers,
+ fetchTeams
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Race)
diff --git a/src/javascript/components/Races.js b/src/javascript/components/Races.js
new file mode 100644
index 0000000..0bd7135
--- /dev/null
+++ b/src/javascript/components/Races.js
@@ -0,0 +1,34 @@
+import React, { Component } from "react"
+import { NavLink } from 'react-router-dom'
+import { connect } from 'react-redux'
+import { fetchRaces } from '../redux/actions'
+import * as Route from '../lib/routes'
+
+class Races extends Component {
+ componentDidMount() {
+ const { fetchRaces } = this.props
+
+ fetchRaces()
+ }
+
+ render() {
+ const { racesList } = this.props
+
+ return (
+ <>
+ Триатлоны
+
+ {racesList.map(race =>
{race.title}
)}
+
+ >
+ )
+ }
+}
+
+const mapStateToProps = ({ racesList }) => ({
+ racesList
+})
+
+const mapDispatchToProps = { fetchRaces }
+
+export default connect(mapStateToProps, mapDispatchToProps)(Races)
diff --git a/src/javascript/components/Result.js b/src/javascript/components/Result.js
new file mode 100644
index 0000000..bce87db
--- /dev/null
+++ b/src/javascript/components/Result.js
@@ -0,0 +1,128 @@
+import React, { Component } from "react"
+import { connect } from 'react-redux'
+import PropTypes from 'prop-types'
+import EditTime from "./EditTime.js"
+import EditSelect from "./EditSelect.js"
+import EditSwitcher from "./EditSwitcher.js"
+import { listToSelectOptions, findById } from '../lib/func.js'
+import { toggleEditMode, updateResult, deleteResult } from '../redux/actions.js'
+
+import "../styles/Results.scss"
+
+class Result extends Component {
+ constructor(props) {
+ super(props)
+
+ this.state = {
+ id: props.id,
+ userId: props.userData.id,
+ teamId: props.teamData.id,
+ cityId: props.cityData.id,
+ time: props.time
+ };
+ this.handleSaveClick = this.handleSaveClick.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)
+ }
+
+ handleSaveClick(event) {
+ event.preventDefault()
+
+ const { toggleEditMode, editableId, id } = this.props
+
+ if(editableId) {
+ const { usersList, teamsList, citiesList, updateResult } = this.props
+ const { userId, teamId, cityId, time, id } = this.state
+ const payload = {
+ id: id,
+ user: findById(usersList, userId),
+ team: findById(teamsList, teamId),
+ city: findById(citiesList, cityId),
+ time: time
+ }
+ updateResult(payload)
+ }
+ toggleEditMode(id)
+ }
+
+ handleDeleteClick(event, id) {
+ event.preventDefault()
+
+ this.props.deleteResult(id)
+ }
+
+ 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 { editableId, userData, usersList, teamData, teamsList, cityData, citiesList, id } = this.props
+ const { userId, teamId, cityId, time } = this.state
+
+ const usersOptions = listToSelectOptions(usersList)
+ const teamsOptions = listToSelectOptions(teamsList)
+ const citiesOptions = listToSelectOptions(citiesList)
+
+ const userOption = usersOptions.find(item => item.value == userId)
+ const teamOption = teamsOptions.find(item => item.value == teamId)
+ const cityOption = citiesOptions.find(item => item.value == cityId)
+
+ const currentUserOption = userOption || {}
+ const currentTeamOption = teamOption || {}
+ const currentCityOption = cityOption || {}
+
+ const editMode = editableId == id
+
+ return (
+
+
{id}
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+Result.propTypes = {
+ id: PropTypes.number,
+ userData: PropTypes.object.isRequired,
+ teamData: PropTypes.object.isRequired,
+ cityData: PropTypes.object.isRequired,
+ time: PropTypes.string
+}
+
+const mapStateToProps = ({ editableId, resultsList, usersList, teamsList, citiesList }) => ({
+ editableId,
+ resultsList,
+ usersList,
+ teamsList,
+ citiesList
+})
+
+const mapDispatchToProps = {
+ toggleEditMode,
+ updateResult,
+ deleteResult
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Result)
diff --git a/src/javascript/components/Teams.js b/src/javascript/components/Teams.js
new file mode 100644
index 0000000..57ab969
--- /dev/null
+++ b/src/javascript/components/Teams.js
@@ -0,0 +1,35 @@
+import React, { Component } from "react"
+import { connect } from 'react-redux'
+import { fetchTeams } from '../redux/actions'
+import * as Route from '../lib/routes'
+
+class Teams extends Component {
+ componentDidMount() {
+ const { fetchTeams } = this.props
+
+ fetchTeams()
+ }
+
+ render() {
+ const { teamsList } = this.props
+
+ return (
+ <>
+ Команды
+
+ {teamsList.map(user =>
{user.name}
)}
+
+ >
+ )
+ }
+}
+
+const mapStateToProps = ({ teamsList }) => ({
+ teamsList
+})
+
+const mapDispatchToProps = {
+ fetchTeams
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Teams)
diff --git a/src/javascript/components/User.js b/src/javascript/components/User.js
new file mode 100644
index 0000000..06ecbf9
--- /dev/null
+++ b/src/javascript/components/User.js
@@ -0,0 +1,12 @@
+import React, { Component } from "react"
+import { connect } from 'react-redux'
+import PropTypes from 'prop-types'
+import Result from "./Result.js"
+import AddResult from "./AddResult.js"
+import * as Route from '../lib/routes.js'
+
+class User extends Component {
+
+}
+
+export default User
diff --git a/src/javascript/components/Users.js b/src/javascript/components/Users.js
new file mode 100644
index 0000000..d2fac25
--- /dev/null
+++ b/src/javascript/components/Users.js
@@ -0,0 +1,36 @@
+import React, { Component } from "react"
+import { connect } from 'react-redux'
+import { addUser, fetchUsers } from '../redux/actions'
+import * as Route from '../lib/routes'
+
+class Users extends Component {
+ componentDidMount() {
+ const { fetchUsers } = this.props
+
+ fetchUsers()
+ }
+
+ render() {
+ const { usersList } = this.props
+
+ return (
+ <>
+ Спортсмены
+
+ {usersList.map(user =>
{user.name}
)}
+
+ >
+ )
+ }
+}
+
+const mapStateToProps = ({ usersList }) => ({
+ usersList
+})
+
+const mapDispatchToProps = {
+ addUser,
+ fetchUsers
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Users)
diff --git a/src/javascript/index.js b/src/javascript/index.js
new file mode 100644
index 0000000..362262b
--- /dev/null
+++ b/src/javascript/index.js
@@ -0,0 +1,31 @@
+import "core-js/stable";
+import "regenerator-runtime/runtime";
+import React from "react"
+import ReactDOM from "react-dom"
+import { Provider } from 'react-redux'
+import { BrowserRouter as Router, Route } from 'react-router-dom'
+import Race from "./components/Race"
+import Races from "./components/Races"
+import Users from "./components/Users"
+import Cities from "./components/Cities"
+import Teams from "./components/Teams"
+import Header from "./components/Header"
+import store from './redux/store'
+
+const reactAdminDashboardEntrypoint = document.getElementById("react-admin-dashboard-entrypoint")
+
+if(reactAdminDashboardEntrypoint) {
+ ReactDOM.render(
+
+
+
+
+
+
+
+ } />
+
+ ,
+ reactAdminDashboardEntrypoint
+ )
+}
diff --git a/src/javascript/lib/func.js b/src/javascript/lib/func.js
new file mode 100644
index 0000000..59b62c1
--- /dev/null
+++ b/src/javascript/lib/func.js
@@ -0,0 +1,15 @@
+export function listToSelectOptions(list) { return list.map(e => ({ value: e.id, label: e.name })) }
+export function findById(list, id) { return list.find(item => item.id == id) }
+
+export function resultJsonToFormData(obj) {
+ const formData = new FormData()
+
+ formData.append('time', obj.time)
+ formData.append('user_id', obj.user.id)
+ formData.append('city_id', obj.city.id)
+ formData.append('team_id', obj.team.id)
+ obj.group && formData.append('group_id', obj.group.id)
+ obj.raceId && formData.append('race_id', obj.raceId)
+
+ return formData
+}
diff --git a/src/javascript/lib/routes.js b/src/javascript/lib/routes.js
new file mode 100644
index 0000000..1fad9ed
--- /dev/null
+++ b/src/javascript/lib/routes.js
@@ -0,0 +1,9 @@
+export function resultsPath(race_id) { return '/api/results.json' + (race_id ? `?race_id=${race_id}` : '') }
+export function resultPath(id) { return `/api/results/${id}.json` }
+
+export function usersPath() { return '/api/users.json' }
+export function teamsPath() { return '/api/teams.json' }
+export function citiesPath() { return '/api/cities.json' }
+
+export function racesPath() { return '/api/races.json' }
+export function racePath(id) { return `/api/races/${id}.json` }
diff --git a/src/javascript/redux/actionTypes.js b/src/javascript/redux/actionTypes.js
new file mode 100644
index 0000000..ce26924
--- /dev/null
+++ b/src/javascript/redux/actionTypes.js
@@ -0,0 +1,22 @@
+export const FETCH_RESULTS = 'FETCH_RESULTS'
+export const FETCH_RESULTS_SUCCESS = 'FETCH_RESULTS_SUCCESS'
+export const FETCH_TEAMS = 'FETCH_TEAMS'
+export const FETCH_TEAMS_SUCCESS = 'FETCH_TEAMS_SUCCESS'
+export const FETCH_CITIES = 'FETCH_CITIES'
+export const FETCH_CITIES_SUCCESS = 'FETCH_CITIES_SUCCESS'
+export const FETCH_USERS = 'FETCH_USERS'
+export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS'
+export const FETCH_RACES = 'FETCH_RACES'
+export const FETCH_RACES_SUCCESS = 'FETCH_RACES_SUCCESS'
+
+export const ADD_RESULT = 'ADD_RESULT'
+export const ADD_RESULT_SUCCESS = 'ADD_RESULT_SUCCESS'
+export const ADD_USER = 'ADD_USER'
+
+export const UPDATE_RESULT = 'UPDATE_RESULT'
+export const UPDATE_RESULT_SUCCESS = 'UPDATE_RESULT_SUCCESS'
+
+export const DELETE_RESULT = 'DELETE_RESULT'
+export const DELETE_RESULT_SUCCESS = 'DELETE_RESULT_SUCCESS'
+
+export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'
diff --git a/src/javascript/redux/actions.js b/src/javascript/redux/actions.js
new file mode 100644
index 0000000..bf698ea
--- /dev/null
+++ b/src/javascript/redux/actions.js
@@ -0,0 +1,89 @@
+import { ADD_RESULT, ADD_RESULT_SUCCESS, UPDATE_RESULT, UPDATE_RESULT_SUCCESS, DELETE_RESULT, DELETE_RESULT_SUCCESS,
+TOGGLE_EDIT_MODE, FETCH_RESULTS, FETCH_USERS, FETCH_TEAMS, FETCH_CITIES, ADD_USER, FETCH_RACES, FETCH_RESULTS_SUCCESS,
+FETCH_CITIES_SUCCESS, FETCH_USERS_SUCCESS, FETCH_TEAMS_SUCCESS, FETCH_RACES_SUCCESS } from "./actionTypes"
+
+export const fetchRaces = () => ({
+ type: FETCH_RACES
+})
+
+export const fetchRacesSuccess = payload => ({
+ type: FETCH_RACES_SUCCESS,
+ payload: payload
+})
+
+export const fetchResults = (id) => ({
+ type: FETCH_RESULTS,
+ payload: { id }
+})
+
+export const fetchResultsSuccess = payload => ({
+ type: FETCH_RESULTS_SUCCESS,
+ payload: payload
+})
+
+export const fetchUsers = () => ({
+ type: FETCH_USERS
+})
+
+export const fetchUsersSuccess = payload => ({
+ type: FETCH_USERS_SUCCESS,
+ payload: payload
+})
+
+export const fetchTeams = () => ({
+ type: FETCH_TEAMS
+})
+
+export const fetchTeamsSuccess = payload => ({
+ type: FETCH_TEAMS_SUCCESS,
+ payload: payload
+})
+
+export const fetchCities = () => ({
+ type: FETCH_CITIES
+})
+
+export const fetchCitiesSuccess = payload => ({
+ type: FETCH_CITIES_SUCCESS,
+ payload: payload
+})
+
+export const addResult = payload => ({
+ type: ADD_RESULT,
+ payload: payload
+})
+
+export const addResultSuccess = payload => ({
+ type: ADD_RESULT_SUCCESS,
+ payload: payload
+})
+
+export const addUser = payload => ({
+ type: ADD_USER,
+ payload: payload
+})
+
+export const updateResult = payload => ({
+ type: UPDATE_RESULT,
+ payload: payload
+})
+
+export const updateResultSuccess = payload => ({
+ type: UPDATE_RESULT_SUCCESS,
+ payload: payload
+})
+
+export const deleteResult = id => ({
+ type: DELETE_RESULT,
+ payload: { id }
+})
+
+export const deleteResultSuccess = id => ({
+ type: DELETE_RESULT_SUCCESS,
+ payload: { id }
+})
+
+export const toggleEditMode = id => ({
+ type: TOGGLE_EDIT_MODE,
+ payload: { id }
+})
diff --git a/src/javascript/redux/reducers/cities.js b/src/javascript/redux/reducers/cities.js
new file mode 100644
index 0000000..000185b
--- /dev/null
+++ b/src/javascript/redux/reducers/cities.js
@@ -0,0 +1,11 @@
+import { FETCH_CITIES_SUCCESS } from '../actionTypes'
+
+export default function(state = [], action) {
+ switch (action.type) {
+ case FETCH_CITIES_SUCCESS: {
+ return [...action.payload]
+ }
+ default:
+ return state;
+ }
+}
diff --git a/src/javascript/redux/reducers/editable.js b/src/javascript/redux/reducers/editable.js
new file mode 100644
index 0000000..f52d8ba
--- /dev/null
+++ b/src/javascript/redux/reducers/editable.js
@@ -0,0 +1,11 @@
+import { TOGGLE_EDIT_MODE } from '../actionTypes'
+
+export default function(state = null, action) {
+ switch (action.type) {
+ case TOGGLE_EDIT_MODE: {
+ return state == action.payload.id ? null : action.payload.id
+ }
+ default:
+ return state;
+ }
+}
diff --git a/src/javascript/redux/reducers/index.js b/src/javascript/redux/reducers/index.js
new file mode 100644
index 0000000..dc57991
--- /dev/null
+++ b/src/javascript/redux/reducers/index.js
@@ -0,0 +1,9 @@
+import { combineReducers } from 'redux'
+import results from './results'
+import users from './users'
+import teams from './teams'
+import cities from './cities'
+import races from './races'
+import editable from './editable'
+
+export default combineReducers({ resultsList: results, usersList: users, teamsList: teams, citiesList: cities, racesList: races, editableId: editable })
diff --git a/src/javascript/redux/reducers/races.js b/src/javascript/redux/reducers/races.js
new file mode 100644
index 0000000..69475f0
--- /dev/null
+++ b/src/javascript/redux/reducers/races.js
@@ -0,0 +1,11 @@
+import { FETCH_RACES_SUCCESS } from '../actionTypes'
+
+export default function(state = [], action) {
+ switch (action.type) {
+ case FETCH_RACES_SUCCESS: {
+ return [...action.payload]
+ }
+ default:
+ return state;
+ }
+}
diff --git a/src/javascript/redux/reducers/results.js b/src/javascript/redux/reducers/results.js
new file mode 100644
index 0000000..f4aa1f1
--- /dev/null
+++ b/src/javascript/redux/reducers/results.js
@@ -0,0 +1,27 @@
+import { ADD_RESULT_SUCCESS, UPDATE_RESULT_SUCCESS, DELETE_RESULT_SUCCESS, FETCH_RESULTS_SUCCESS } from "../actionTypes";
+
+export default function(state = [], action) {
+ switch (action.type) {
+ case FETCH_RESULTS_SUCCESS: {
+ return [...action.payload]
+ }
+ case ADD_RESULT_SUCCESS: {
+ return [...state, action.payload]
+ }
+ case UPDATE_RESULT_SUCCESS: {
+ const index = state.findIndex(i => i == action.payload.id)
+
+ if(index) {
+ state[index] = action.payload
+ return [...state]
+ } else {
+ return state;
+ }
+ }
+ case DELETE_RESULT_SUCCESS: {
+ return [...state.filter(item => item.id != action.payload.id)]
+ }
+ default:
+ return state;
+ }
+}
\ No newline at end of file
diff --git a/src/javascript/redux/reducers/teams.js b/src/javascript/redux/reducers/teams.js
new file mode 100644
index 0000000..702b49b
--- /dev/null
+++ b/src/javascript/redux/reducers/teams.js
@@ -0,0 +1,11 @@
+import { FETCH_TEAMS_SUCCESS } from '../actionTypes'
+
+export default function(state = [], action) {
+ switch (action.type) {
+ case FETCH_TEAMS_SUCCESS: {
+ return [...action.payload]
+ }
+ default:
+ return state;
+ }
+}
diff --git a/src/javascript/redux/reducers/users.js b/src/javascript/redux/reducers/users.js
new file mode 100644
index 0000000..47ee0a3
--- /dev/null
+++ b/src/javascript/redux/reducers/users.js
@@ -0,0 +1,14 @@
+import { FETCH_USERS_SUCCESS, ADD_USER } from '../actionTypes'
+
+export default function(state = [], action) {
+ switch (action.type) {
+ case FETCH_USERS_SUCCESS: {
+ return [...action.payload]
+ }
+ case ADD_USER: {
+ return [...state, action.payload]
+ }
+ default:
+ return state;
+ }
+}
diff --git a/src/javascript/redux/sagas/index.js b/src/javascript/redux/sagas/index.js
new file mode 100644
index 0000000..8cc1bd7
--- /dev/null
+++ b/src/javascript/redux/sagas/index.js
@@ -0,0 +1,102 @@
+import { put, call, takeEvery } from 'redux-saga/effects'
+import { FETCH_RESULTS, FETCH_USERS, FETCH_TEAMS, FETCH_CITIES, FETCH_RACES, ADD_RESULT, UPDATE_RESULT, DELETE_RESULT } from '../actionTypes'
+import * as Route from '../../lib/routes'
+import { resultJsonToFormData } from '../../lib/func'
+import { fetchResultsSuccess, fetchUsersSuccess, fetchRacesSuccess, fetchCitiesSuccess, fetchTeamsSuccess,
+addResultSuccess, updateResultSuccess, deleteResultSuccess } from '../actions'
+
+export function* watchFetchResults() {
+ yield takeEvery(FETCH_RESULTS, fetchResultsAsync)
+}
+
+export function* watchFetchUsers() {
+ yield takeEvery(FETCH_USERS, fetchUsersAsync)
+}
+
+export function* watchFetchTeams() {
+ yield takeEvery(FETCH_TEAMS, fetchTeamsAsync)
+}
+
+export function* watchFetchCities() {
+ yield takeEvery(FETCH_CITIES, fetchCitiesAsync)
+}
+
+export function* watchFetchRaces() {
+ yield takeEvery(FETCH_RACES, fetchRacesAsync)
+}
+
+export function* watchAddResult() {
+ yield takeEvery(ADD_RESULT, addResultSync)
+}
+
+export function* watchUpdateResult() {
+ yield takeEvery(UPDATE_RESULT, updateResultSync)
+}
+
+export function* watchDeleteResult() {
+ yield takeEvery(DELETE_RESULT, deleteResultSync)
+}
+
+export function* fetchResultsAsync(action) {
+ const data = yield call(() => {
+ return fetch(Route.resultsPath(action.payload.id))
+ .then(res => res.json())
+ })
+ yield put(fetchResultsSuccess(data))
+}
+
+export function* fetchUsersAsync() {
+ const data = yield call(() => {
+ return fetch(Route.usersPath())
+ .then(res => res.json())
+ })
+ yield put(fetchUsersSuccess(data))
+}
+
+export function* fetchTeamsAsync() {
+ const data = yield call(() => {
+ return fetch(Route.teamsPath())
+ .then(res => res.json())
+ })
+ yield put(fetchTeamsSuccess(data))
+}
+
+export function* fetchCitiesAsync() {
+ const data = yield call(() => {
+ return fetch(Route.citiesPath())
+ .then(res => res.json())
+ })
+ yield put(fetchCitiesSuccess(data))
+}
+
+export function* fetchRacesAsync() {
+ const data = yield call(() => {
+ return fetch(Route.racesPath())
+ .then(res => res.json())
+ })
+ yield put(fetchRacesSuccess(data))
+}
+
+export function* addResultSync(action) {
+ const data = yield call(() => {
+ return fetch(Route.resultsPath(), { method: 'POST', body: resultJsonToFormData(action.payload) })
+ .then(res => res.json())
+ })
+ yield put(addResultSuccess({ ...action.payload, id: data.id }))
+}
+
+export function* updateResultSync(action) {
+ const data = yield call(() => {
+ return fetch(Route.resultPath(action.payload.id), { method: 'PUT', body: resultJsonToFormData(action.payload) })
+ .then(res => res.json())
+ })
+ yield put(updateResultSuccess({ ...action.payload }))
+}
+
+export function* deleteResultSync(action) {
+ const data = yield call(() => {
+ return fetch(Route.resultPath(action.payload.id), { method: 'DELETE' })
+ .then(res => res.json())
+ })
+ yield put(deleteResultSuccess(action.payload.id))
+}
diff --git a/src/javascript/redux/store.js b/src/javascript/redux/store.js
new file mode 100644
index 0000000..da709a5
--- /dev/null
+++ b/src/javascript/redux/store.js
@@ -0,0 +1,19 @@
+import { createStore, applyMiddleware } from "redux"
+import rootReducer from "./reducers"
+import createSagaMiddleware from 'redux-saga'
+import { watchFetchResults, watchFetchRaces, watchFetchUsers, watchFetchCities, watchFetchTeams,
+watchAddResult, watchUpdateResult, watchDeleteResult } from './sagas'
+
+const sagaMiddleware = createSagaMiddleware()
+
+export default createStore(rootReducer, applyMiddleware(sagaMiddleware))
+
+sagaMiddleware.run(watchFetchResults)
+sagaMiddleware.run(watchFetchRaces)
+sagaMiddleware.run(watchFetchUsers)
+sagaMiddleware.run(watchFetchCities)
+sagaMiddleware.run(watchFetchTeams)
+
+sagaMiddleware.run(watchAddResult)
+sagaMiddleware.run(watchUpdateResult)
+sagaMiddleware.run(watchDeleteResult)
diff --git a/src/javascript/styles/Results.scss b/src/javascript/styles/Results.scss
new file mode 100644
index 0000000..a6061cf
--- /dev/null
+++ b/src/javascript/styles/Results.scss
@@ -0,0 +1,54 @@
+.result {
+ display: flex;
+ justify-content: space-between;
+ padding: 10px;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ margin-bottom: 3px;
+
+ .result-attribute {
+ flex-basis: 20%;
+ display: flex;
+
+ .rc-time-picker-input {
+ height: 100%;
+ }
+
+ .result-input {
+ width: 70px;
+ height: 38px;
+ border-radius: 4px;
+ }
+
+ .result-button-save {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ }
+
+ div:first-child {
+ width: 100%;
+ }
+ }
+
+ &--has-error {
+ .result-attribute {
+ &--has-error div {
+ border-color: red;
+ }
+ }
+ }
+
+ &-id {
+ flex-basis: 3%;
+ }
+
+ &-actions {
+ flex-basis: 12%;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .rc-time-picker-clear {
+ top: 7px;
+ }
+}
diff --git a/src/models/city.cr b/src/models/city.cr
index 3a02206..4ce0660 100644
--- a/src/models/city.cr
+++ b/src/models/city.cr
@@ -6,4 +6,11 @@ class City < Jennifer::Model::Base
created_at: {type: Time, null: true},
updated_at: {type: Time, null: true}
)
+
+ def as_json
+ {
+ id: id,
+ name: name,
+ }
+ end
end
diff --git a/src/models/group.cr b/src/models/group.cr
index b6f06f8..385cb2c 100644
--- a/src/models/group.cr
+++ b/src/models/group.cr
@@ -6,4 +6,11 @@ class Group < Jennifer::Model::Base
created_at: {type: Time, null: true},
updated_at: {type: Time, null: true}
)
+
+ def as_json
+ {
+ id: id,
+ name: name,
+ }
+ end
end
diff --git a/src/models/race.cr b/src/models/race.cr
index 6c9149e..bbd3fe5 100644
--- a/src/models/race.cr
+++ b/src/models/race.cr
@@ -9,6 +9,12 @@ class Race < Jennifer::Model::Base
updated_at: {type: Time, null: true}
)
+ JSON.mapping(
+ id: Int32?,
+ title: String,
+ date: String
+ )
+
has_many :results, Result
has_many :race_disciplines, RaceDiscipline, {order(position: :asc)}
diff --git a/src/models/result.cr b/src/models/result.cr
index a72b1b4..582c6b4 100644
--- a/src/models/result.cr
+++ b/src/models/result.cr
@@ -19,9 +19,22 @@ class Result < Jennifer::Model::Base
belongs_to :team, Team
belongs_to :group, Group
- has_many :result_race_disciplines, ResultRaceDiscipline, { order(position: :asc) }
+ has_many :result_race_disciplines, ResultRaceDiscipline, {order(position: :asc)}
+
+ scope :by_race_id { |race_id| where { _race_id == race_id } unless race_id.nil? }
def time_format
time.nil? ? "DNF" : time
end
-end
\ No newline at end of file
+
+ def as_json
+ {
+ id: id,
+ user: user!.as_json,
+ city: city!.as_json,
+ team: team!.as_json,
+ group: group ? group!.as_json : nil,
+ time: time,
+ }
+ end
+end
diff --git a/src/models/team.cr b/src/models/team.cr
index df3d08e..9470a42 100644
--- a/src/models/team.cr
+++ b/src/models/team.cr
@@ -11,4 +11,11 @@ class Team < Jennifer::Model::Base
all.find_records_by_sql "SELECT teams.*, COUNT(users.*) as count FROM teams, LATERAL(SELECT DISTINCT results.user_id FROM results WHERE results.team_id = teams.id) users
WHERE teams.name ILIKE $1 AND teams.name != 'Лично' GROUP BY teams.id", ["%#{name}%"]
end
+
+ def as_json
+ {
+ id: id,
+ name: name,
+ }
+ end
end
diff --git a/src/models/user.cr b/src/models/user.cr
index 00ce304..707a4b0 100644
--- a/src/models/user.cr
+++ b/src/models/user.cr
@@ -18,4 +18,13 @@ class User < Jennifer::Model::Base
def self.by_team_id(team_id)
all.find_by_sql "SELECT users.* FROM users JOIN results ON results.user_id = users.id WHERE results.team_id = $1 GROUP BY users.id", [team_id]
end
+
+ def as_json
+ {
+ id: id,
+ name: name,
+ qualification: qualification,
+ year: year,
+ }
+ end
end
diff --git a/src/views/admin/dashboard/index.slang b/src/views/admin/dashboard/index.slang
new file mode 100644
index 0000000..825522f
--- /dev/null
+++ b/src/views/admin/dashboard/index.slang
@@ -0,0 +1 @@
+#react-admin-dashboard-entrypoint
diff --git a/src/views/admin/layouts/_nav.slang b/src/views/admin/layouts/_nav.slang
new file mode 100644
index 0000000..51b372d
--- /dev/null
+++ b/src/views/admin/layouts/_nav.slang
@@ -0,0 +1 @@
+a class="nav-item active" href="/admin" = "Панель управления"
diff --git a/src/views/layouts/admin.slang b/src/views/layouts/admin.slang
new file mode 100644
index 0000000..5ba6b4b
--- /dev/null
+++ b/src/views/layouts/admin.slang
@@ -0,0 +1,16 @@
+doctype html
+html
+ head
+ meta charset="utf-8"
+ meta http-equiv="X-UA-Compatible" content="IE=edge"
+ meta name="viewport" content="width=device-width, initial-scale=1"
+ title Dashboard
+ link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
+
+ body
+ div.container.wide
+ div.row
+ div.col-sm-12.main
+ == content
+
+ script src="/javascripts/bundle.js"
diff --git a/src/views/layouts/application.slang b/src/views/layouts/application.slang
index ef6efda..ac85fd7 100644
--- a/src/views/layouts/application.slang
+++ b/src/views/layouts/application.slang
@@ -27,7 +27,6 @@ html
script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"
script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"
script src="https://use.fontawesome.com/b4153aa4cc.js"
- script src="/javascripts/main.js"
/! Yandex.Metrika counter
javascript:
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..ea3688a
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,24 @@
+const path = require("path");
+
+module.exports = {
+ entry: "./src/javascript/index.js",
+ output: {
+ path: path.join(__dirname, "public/javascripts"),
+ filename: "bundle.js"
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ use: {
+ loader: "babel-loader"
+ },
+ },
+ {
+ test: /\.s?css$/,
+ use: ["style-loader", "css-loader", "sass-loader"]
+ }
+ ]
+ }
+};