diff --git a/app/controllers/chip_values_controller.rb b/app/controllers/chip_values_controller.rb new file mode 100644 index 0000000..928359d --- /dev/null +++ b/app/controllers/chip_values_controller.rb @@ -0,0 +1,40 @@ +class ChipValuesController < ApplicationController + + def create + if ChipValue.create_full_chip_value(chip_value_params) + json_response all_chips_response + else + error_response :bad_request + end + rescue StandardError + error_response :bad_request + end + + def update + if ChipValue.update_full_chip_value(chip_value_params) + json_response all_chips_response + end + rescue StandardError + error_response :bad_request + end + + def destroy + if ChipValue.destroy_full_chip_value(params[:id]) + json_response all_chips_response + else + error_response :bad_request + end + rescue StandardError + error_response :bad_request + end + + private + + def chip_value_params + params.require(:chip_value).permit(:id, :base_chip_id, :secondary_chip_id, :value) + end + + def all_chips_response + Chip.all.order(:name).to_json(include: :base_chip_values) + end +end \ No newline at end of file diff --git a/app/controllers/chips_controller.rb b/app/controllers/chips_controller.rb new file mode 100644 index 0000000..2f70b67 --- /dev/null +++ b/app/controllers/chips_controller.rb @@ -0,0 +1,29 @@ +class ChipsController < ApplicationController + def index + json_response Chip.all.order(:name).to_json(include: :base_chip_values) + end + + def create + chip = Chip.new(chip_params) + if chip.save + json_response chip + else + error_response :bad_request + end + end + + def destroy + chip_id = params[:id] + if chip_id.present? && Chip.destroy(chip_id) + index + else + error_response :bad_request + end + end + + private + + def chip_params + params.require(:chip).permit(:id, :name, :symbol, :enabled) + end +end \ No newline at end of file diff --git a/app/models/chip.rb b/app/models/chip.rb new file mode 100644 index 0000000..9fb244f --- /dev/null +++ b/app/models/chip.rb @@ -0,0 +1,23 @@ +class Chip < ApplicationRecord + has_many :base_chip_values, class_name: "ChipValue", foreign_key: "base_chip_id", dependent: :delete_all + has_many :secondary_chip_values, class_name: "ChipValue", foreign_key: "secondary_chip_id", dependent: :delete_all + + validates :name, uniqueness: true + validates :name, :symbol, presence: true + + after_create :add_chip_values + + private + + # When new chip(currency) is added, add new record to the chip_values table for every existing chip(currency) + def add_chip_values + Chip.all.each do |chip| + next if chip.name == name + + transaction do + ChipValue.new(base_chip: self, secondary_chip: chip, value: 0).save + ChipValue.new(base_chip: chip, secondary_chip: self, value: 0).save + end + end + end +end \ No newline at end of file diff --git a/app/models/chip_value.rb b/app/models/chip_value.rb new file mode 100644 index 0000000..fc2040e --- /dev/null +++ b/app/models/chip_value.rb @@ -0,0 +1,53 @@ +class ChipValue < ApplicationRecord + belongs_to :base_chip, class_name: 'Chip' + belongs_to :secondary_chip, class_name: 'Chip' + + validates :base_chip_id, uniqueness: { scope: :secondary_chip_id } + + def self.create_full_chip_value(params) + base_chip_id = params[:base_chip_id] + secondary_chip_id = params[:secondary_chip_id] + value = params[:value].to_f + + chips = Chip.where(id: [base_chip_id, secondary_chip_id]) + base_chip = chips.first + secondary_chip = chips.second + + mirrored_value = value.zero? ? 0 : (1 / value) + + base_chip_value = ChipValue.new(base_chip: base_chip, secondary_chip: secondary_chip, value: value) + mirrored_chip_value = ChipValue.new(base_chip: secondary_chip, secondary_chip: base_chip, value: mirrored_value) + + transaction do + base_chip_value.save + mirrored_chip_value.save + end + end + + def self.update_full_chip_value(params) + id = params[:id] + new_value = params[:value].to_f + mirrored_value = new_value.zero? ? 0 : (1 / new_value) + + chip_value = ChipValue.find(id) + mirrored_chip_value = ChipValue.where(base_chip_id: chip_value.secondary_chip_id, secondary_chip_id: chip_value.base_chip_id).first + + chip_value.value = new_value + mirrored_chip_value.value = mirrored_value + + transaction do + chip_value.save + mirrored_chip_value.save + end + end + + def self.destroy_full_chip_value(id) + chip_value = ChipValue.find(id) + mirrored_chip_value = ChipValue.where(base_chip_id: chip_value.secondary_chip_id, secondary_chip_id: chip_value.base_chip_id).first + + transaction do + chip_value.destroy + mirrored_chip_value.destroy + end + end +end \ No newline at end of file diff --git a/client/src/App.css b/client/src/App.css index 5f58b0b..cc1ca1d 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -41,3 +41,7 @@ z-index: 996; } +.mr-10 { + margin-right: 100px; +} + diff --git a/client/src/App.js b/client/src/App.js index c8032fe..0dc8f4e 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,39 +1,43 @@ import React from 'react'; import './App.css'; -import { Tabs, Tab, Navbar } from 'react-materialize'; +import { Navbar } from 'react-materialize'; import MakeMoneyMove from './cash/MakeMoneyMove'; import Cash from './cash/Cash'; -import { BrowserRouter as Router, Route, Link } from "react-router-dom"; +import Chips from './chips/Chips'; +import { BrowserRouter as Router, Route } from "react-router-dom"; import RoutableNavItem from './common/RoutableNavItem'; import { - CRIB, + CRIB, + CHIPS, MAKE_MONEY_MOVE } from './RouteNames'; function App() { - return ( + +
+ GKS
} alignLinks="right"> + + Crib + - -
- GKS
} alignLinks="right"> - - Crib - - - - Homies - + + Chips + - - Make Money Move - + + Homies + - + + Make Money Move + + + - -
+
+
diff --git a/client/src/RouteNames.js b/client/src/RouteNames.js index 0c997a0..d84b419 100644 --- a/client/src/RouteNames.js +++ b/client/src/RouteNames.js @@ -1,2 +1,3 @@ export const CRIB = '/'; +export const CHIPS = '/chips'; export const MAKE_MONEY_MOVE = '/make-money-move'; diff --git a/client/src/cash/Cash.js b/client/src/cash/Cash.js index 9502c82..4d938f2 100644 --- a/client/src/cash/Cash.js +++ b/client/src/cash/Cash.js @@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react'; import { Button, Table } from 'react-materialize'; import './Cash.css'; import axios from 'axios'; -import { Link } from 'react-router-dom'; import { MAKE_MONEY_MOVE } from '../RouteNames'; import { withRouter } from 'react-router-dom'; diff --git a/client/src/chips/AddChip.js b/client/src/chips/AddChip.js new file mode 100644 index 0000000..b2c7218 --- /dev/null +++ b/client/src/chips/AddChip.js @@ -0,0 +1,86 @@ +import React, { useState } from 'react'; +import { TextInput, Button } from "react-materialize"; +import axios from "axios"; +import M from "materialize-css"; + +const AddChip = (props) => { + const [chipName, setChipName] = useState(''); + const [chipSymbol, setChipSymbol] = useState(''); + const [submitInProgress, setSubmitInProgress] = useState(false); + + const handleInputChange = (e) => { + const newValue = e.target.value; + switch (e.target.id){ + case 'chipName': + setChipName(newValue); + break; + case 'chipSymbol': + setChipSymbol(newValue); + break; + default: + break; + } + } + + const disableSubmit = () => { + return submitInProgress || chipName.length === 0 || chipSymbol.length === 0; + } + + const clearForm = () => { + setChipName(''); + setChipSymbol(''); + } + + const errorToast = () => M.toast({ html: "Yo! It ain't workin'" }); + + const handleSubmit = async () => { + setSubmitInProgress(true); + const chipRequest = { + chip: { + name: chipName, + symbol: chipSymbol, + } + } + + try{ + const submitResponse = await axios.post('/chips', chipRequest); + + if (submitResponse && submitResponse.status === 200 && submitResponse.data) { + M.toast({ html: "Chipped In" }); + clearForm(); + } else { + errorToast(); + } + }catch (e) { + errorToast(); + } + + setSubmitInProgress(false); + } + + return ( +
+
Add New Chip
+ + + + + + +
+ ); +} + +export default AddChip; \ No newline at end of file diff --git a/client/src/chips/Chips.js b/client/src/chips/Chips.js new file mode 100644 index 0000000..86196bb --- /dev/null +++ b/client/src/chips/Chips.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import AddChip from "./AddChip"; +import ListChips from "./ListChips"; + +const Chips = (props) => ( +
+ + + +
+); + +export default withRouter(Chips); \ No newline at end of file diff --git a/client/src/chips/ListChips.js b/client/src/chips/ListChips.js new file mode 100644 index 0000000..d912efa --- /dev/null +++ b/client/src/chips/ListChips.js @@ -0,0 +1,248 @@ +import React, { useState, useEffect } from 'react'; +import axios from "axios"; +import { Table, Collapsible, CollapsibleItem, Button, TextInput, Row, Col, Select } from "react-materialize"; +import M from "materialize-css"; +import YesNoModal from "../common/YesNoModal"; + +const ListChips = (props) => { + const [chipsList, setChipsList] = useState([]); + const [chipValuePairs, setChipValuePairs] = useState([]); + const [chipValueActiveIndex, setChipValueActiveIndex] = useState(undefined); + const [editingChipValue, setEditingChipValue] = useState(""); + const [newChipValueSecondaryChipId, setNewChipValueSecondaryChipId] = useState(""); + + const reloadChipsListEffect = () => { + (async() => { + try { + const chipsResponse = await axios.get(`/chips`); + + if (chipsResponse && chipsResponse.status === 200 && Array.isArray(chipsResponse.data)) { + setChipsList(chipsResponse.data); + } + + } catch (e) { + errorToast(); + } + })(); + } + + const updateChipValuePairsEffect = () => { + const result = []; + chipsList.forEach(chip => { + const chipValues = chip['base_chip_values']; + chipValues.forEach(chipValue => { + result.push({ + baseChipId: chip.id, + secondaryChipId: chipValue['secondary_chip_id'] + }); + }); + }); + setChipValuePairs(result); + } + + useEffect(reloadChipsListEffect, []); + useEffect(updateChipValuePairsEffect, [chipsList]); + + const deleteChip = async (chipId) => { + try { + const chipsResponse = await axios.delete(`/chips/${chipId}`); + + if (chipsResponse && chipsResponse.status === 200 && Array.isArray(chipsResponse.data)){ + setChipsList(chipsResponse.data); + M.toast({ html: 'Chip destroyed!' }); + } + + } catch (e) { + errorToast(); + } + } + + const addNewChipValue = async (baseChipId) => { + try{ + const newChipValueObject = { + 'chip_value': { + base_chip_id: baseChipId, + secondary_chip_id: newChipValueSecondaryChipId, + value: editingChipValue + } + } + const chipsResponse = await axios.post(`/chip_values`, newChipValueObject); + + if (chipsResponse && chipsResponse.status === 200){ + setChipsList(chipsResponse.data); + setNewChipValueSecondaryChipId(""); + setChipValueActiveIndex(undefined); + setEditingChipValue(""); + M.toast({ html: 'I smell money $$$' }); + } + } catch (e) { + errorToast(); + } + } + + const updateChipValue = async (chipValueId) => { + try{ + const updatedChipValue = { + 'chip_value': { + id: chipValueId, + value: editingChipValue + } + } + const chipsResponse = await axios.put(`/chip_values/${chipValueId}`, updatedChipValue); + + if (chipsResponse && chipsResponse.status === 200){ + setChipsList(chipsResponse.data); + setChipValueActiveIndex(undefined); + setEditingChipValue(""); + } + } catch (e) { + errorToast(); + } + } + + const deleteChipValue = async (chipValueId) => { + try { + const chipsResponse = await axios.delete(`/chip_values/${chipValueId}`); + + if (chipsResponse && chipsResponse.status === 200){ + setChipsList(chipsResponse.data); + M.toast({ html: 'Destroyed!' }); + } + + } catch (e) { + errorToast(); + } + } + + + + const errorToast = () => M.toast({ html: "Yo! It ain't workin'" }); + + const getChipData = (chipId) => chipsList.find(chip => chip.id === chipId); + + const checkIfPairExists = (baseChipId, secondaryChipId) => { + return chipValuePairs.find(chipValuePair => + chipValuePair.baseChipId === baseChipId && + chipValuePair.secondaryChipId === secondaryChipId); + } + + const secondaryChipOptions = (baseChipId) => { + const options = chipsList.map((chip, index) => { + if (chip.id !== baseChipId && !checkIfPairExists(baseChipId, chip.id)) { + return + }else { + return null; + } + }); + + return options.filter(option => option !== null); + }; + + const chipActions = (id) => ( + + + + + + + + + ]} + bottomSheet + fixedFooter={false} + header={title} + id="Modal-0" + open={false} + options={{ + dismissible: true, + endingTop: '10%', + inDuration: 250, + onCloseEnd: null, + onCloseStart: null, + onOpenEnd: null, + onOpenStart: null, + opacity: 0.5, + outDuration: 250, + preventScrolling: true, + startingTop: '4%' + }} + trigger={triggerNode} + > +

{body}

+ + ) +} + +export default YesNoModal; \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 3ea271d..1a60905 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,8 +1,10 @@ Rails.application.routes.draw do constraints format: :json do - resources :money_moves - resources :homies do - collection do + resources :money_moves + resources :chips, only: %i[index create destroy] + resources :chip_values, only: %i[create update destroy] + resources :homies do + collection do get 'cash' end end diff --git a/db/migrate/20200827113649_create_chips.rb b/db/migrate/20200827113649_create_chips.rb new file mode 100644 index 0000000..989a694 --- /dev/null +++ b/db/migrate/20200827113649_create_chips.rb @@ -0,0 +1,12 @@ +class CreateChips < ActiveRecord::Migration[5.2] + def change + create_table :chips do |t| + t.text :name, null: false + t.text :symbol, null: false + t.boolean :enabled, default: true + t.timestamps + end + + add_index :chips, :name, unique: true + end +end diff --git a/db/migrate/20200827142428_create_chip_values.rb b/db/migrate/20200827142428_create_chip_values.rb new file mode 100644 index 0000000..2d391bd --- /dev/null +++ b/db/migrate/20200827142428_create_chip_values.rb @@ -0,0 +1,12 @@ +class CreateChipValues < ActiveRecord::Migration[5.2] + def change + create_table :chip_values do |t| + t.references :base_chip, foreign_key: { to_table: :chips } + t.references :secondary_chip, foreign_key: { to_table: :chips } + t.decimal :value, precision: 12, scale: 3, null: false + t.timestamps + + t.index [:base_chip_id, :secondary_chip_id], unique: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0a594d1..babee0e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,31 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_06_20_200006) do +ActiveRecord::Schema.define(version: 2020_08_27_142428) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "chip_values", force: :cascade do |t| + t.bigint "base_chip_id" + t.bigint "secondary_chip_id" + t.decimal "value", precision: 12, scale: 3, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["base_chip_id", "secondary_chip_id"], name: "index_chip_values_on_base_chip_id_and_secondary_chip_id", unique: true + t.index ["base_chip_id"], name: "index_chip_values_on_base_chip_id" + t.index ["secondary_chip_id"], name: "index_chip_values_on_secondary_chip_id" + end + + create_table "chips", force: :cascade do |t| + t.text "name", null: false + t.text "symbol", null: false + t.boolean "enabled", default: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["name"], name: "index_chips_on_name", unique: true + end + create_table "homies", force: :cascade do |t| t.text "name", null: false t.integer "importance", default: 5, null: false @@ -31,4 +51,6 @@ ActiveRecord::Schema.define(version: 2019_06_20_200006) do t.datetime "updated_at", null: false end + add_foreign_key "chip_values", "chips", column: "base_chip_id" + add_foreign_key "chip_values", "chips", column: "secondary_chip_id" end