diff --git a/README.md b/README.md index ef1cecd..16a3e22 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,25 @@ -# Docker Phoenix +# Simple. Uses Docker Phoenix ## Prerequisites + Install docker and docker-compose. Duplicate `.example.env` file and rename it to `.env` ## Motivation + I wanted to have a starter kit for Phoenix projects that I would be able to run anywhere in docker. Alpine linux is used for all the docker images. If you use vscode - all the necessary extensions for developing phoenix application will be installed automatically inside the container ## Set up for vscode users + 1. Install https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack 2. Open the folder in vscode and it should suggest you to reopen it inside the container 3. After setting up the database you can add connection in postresql explorer (use hostname `db`, user and password from `.env` file) 4. In `devcontainer.json` set `elixir.projectPath` and `elixirLS.projectDir` to the path to your project so elixir-ls extension starts working. ## Get started + 1. Start shell session inside the container. For vscode users - just open the terminal (Ctrl + J) - it will already be inside the container. For others - run e.g. `docker exec -it phoenix-docker_app_1 sh` 2. Create new phoenix project `mix phx.new your_project_name` and press enter to install dependencies when it prompts you 3. To set up postgres open `config/dev.exs`, change hostname to `"db"`, database to `"database"` and run `mix ecto.create` in your project folder diff --git a/assets/css/app.scss b/assets/css/app.scss index 72fa2e5..4897957 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -29,7 +29,7 @@ } .phx-click-loading { - opacity: 0.5; + opacity: 0.6; transition: opacity 1s ease-out; } diff --git a/assets/css/components/input.scss b/assets/css/components/input.scss index d8936eb..49ae83b 100644 --- a/assets/css/components/input.scss +++ b/assets/css/components/input.scss @@ -4,7 +4,7 @@ input { border: 1px solid var(--c-gray-6); border-radius: var(--bdrs-default); height: 30px; - padding-left: 7px; + padding-left: 5px; outline: none; &:focus { diff --git a/assets/css/components/modal.scss b/assets/css/components/modal.scss index 43ac728..7af779a 100644 --- a/assets/css/components/modal.scss +++ b/assets/css/components/modal.scss @@ -1,5 +1,5 @@ .modal { - opacity: 1!important; + opacity: 1 !important; position: fixed; z-index: 2; left: 0; diff --git a/assets/css/components/transactions.scss b/assets/css/components/transactions.scss index 94eb7bf..03e9157 100644 --- a/assets/css/components/transactions.scss +++ b/assets/css/components/transactions.scss @@ -5,7 +5,7 @@ align-items: center; flex-grow: 1; } - + &__item { max-width: var(--max-page-width); width: 100%; @@ -26,13 +26,13 @@ margin-bottom: 80px; } } - + &__itemSection { display: flex; justify-content: space-between; width: 100%; } - + &__itemLink { position: absolute; width: 100%; @@ -44,7 +44,7 @@ &__searchForm { border-bottom: 1px solid var(--c-gray-4); position: sticky; - top: 46px; + top: 45px; left: 0; background-color: var(--c-gray-1); z-index: 1; diff --git a/assets/css/reset.scss b/assets/css/reset.scss index afdfb25..345a2c2 100644 --- a/assets/css/reset.scss +++ b/assets/css/reset.scss @@ -1,3 +1,10 @@ +html, +body, +div, +span, +applet, +object, +iframe, h1, h2, h3, @@ -5,66 +12,156 @@ h4, h5, h6, p, -fieldset, -body, -button, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, figure, -ol { +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { margin: 0; + padding: 0; + border: 0; + font: inherit; + font-size: inherit; + vertical-align: baseline; } -fieldset, -button, -legend, -ul, -ol { - padding: 0; +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section, +img { + display: block; } -fieldset { - border: none; +li { + list-style: none; +} + +blockquote, +q { + quotes: none; +} + +blockquote::before, +blockquote::after, +q::before, +q::after { + content: ""; + content: none; +} + +table { + border-spacing: 0; + border-collapse: collapse; } button { - border: none; width: auto; overflow: visible; + border: none; background-color: transparent; - line-height: inherit; - text-align: inherit; color: inherit; font-family: inherit; font-size: inherit; font-weight: inherit; -} - -ul, -ol { - list-style: none; + line-height: inherit; + text-align: inherit; } input, textarea { + padding: 0; + border: none; + background-color: inherit; + color: inherit; font-family: inherit; - line-height: inherit; font-size: inherit; + line-height: inherit; } -input[type='number'] { +input[type="number"] { appearance: textfield; } -input[type='number']::-webkit-outer-spin-button, -input[type='number']::-webkit-inner-spin-button { +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { appearance: none; margin: 0; } -input[type='search']::-webkit-search-decoration, -input[type='search']::-webkit-search-cancel-button, -input[type='search']::-webkit-search-results-button, -input[type='search']::-webkit-search-results-decoration { +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-results-button, +input[type="search"]::-webkit-search-results-decoration { display: none; } @@ -75,13 +172,9 @@ select { } a { - text-decoration: none; color: inherit; font-family: inherit; font-size: inherit; font-weight: inherit; -} - -img { - display: block; + text-decoration: none; } diff --git a/assets/package-lock.json b/assets/package-lock.json index 4517c82..b7887e7 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -5068,9 +5068,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-type": { diff --git a/config/dev.exs b/config/dev.exs index 12b631e..376e3af 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -2,8 +2,8 @@ use Mix.Config # Configure your database config :simp, Simp.Repo, - username: "postgres", - password: "postgres", + username: "postgres_user", + password: "postgres_password", database: "database", hostname: "db", show_sensitive_data_on_connection_error: true, diff --git a/lib/simp_web/live/transaction_live/form_component.ex b/lib/simp_web/live/transaction_live/form_component.ex index b14abeb..d9b0a15 100644 --- a/lib/simp_web/live/transaction_live/form_component.ex +++ b/lib/simp_web/live/transaction_live/form_component.ex @@ -11,6 +11,7 @@ defmodule SimpWeb.TransactionLive.FormComponent do socket |> assign(assigns) |> assign(:changeset, changeset) + |> assign(:previous_is_expense, Ecto.Changeset.get_field(changeset, :is_expense)) |> set_data()} end @@ -18,16 +19,26 @@ defmodule SimpWeb.TransactionLive.FormComponent do %{ assigns: %{ current_user: current_user, - changeset: changeset + changeset: changeset, + previous_is_expense: previous_is_expense } } = socket ) do + is_expense = Ecto.Changeset.get_field(changeset, :is_expense) + categories = Transactions.list_categories( current_user, - Ecto.Changeset.get_field(changeset, :is_expense) + is_expense ) + changeset = + if previous_is_expense == is_expense do + changeset + else + Ecto.Changeset.change(changeset, category: List.first(categories) || "") + end + names = Transactions.list_names( current_user, @@ -37,12 +48,16 @@ defmodule SimpWeb.TransactionLive.FormComponent do assign(socket, categories: categories, names: names, - currencies: Transactions.list_currencies(current_user) + currencies: Transactions.list_currencies(current_user), + changeset: changeset, + previous_is_expense: is_expense ) end @impl true def handle_event("validate", %{"transaction" => transaction_params}, socket) do + transaction_params = parse_price_and_amount(transaction_params) + changeset = socket.assigns.transaction |> Transactions.change_transaction(transaction_params) @@ -68,6 +83,8 @@ defmodule SimpWeb.TransactionLive.FormComponent do } } = socket ) do + transaction_params = parse_price_and_amount(transaction_params) + if current_user.id == transaction.user_id do case Transactions.update_transaction(transaction, transaction_params) do {:ok, _transaction} -> @@ -95,6 +112,8 @@ defmodule SimpWeb.TransactionLive.FormComponent do } } = socket ) do + transaction_params = parse_price_and_amount(transaction_params) + case Transactions.create_transaction(transaction_params, current_user) do {:ok, _transaction} -> {:noreply, @@ -114,4 +133,103 @@ defmodule SimpWeb.TransactionLive.FormComponent do "_new" end end + + def parse_price_and_amount(%{"price" => price, "amount" => amount} = transaction_params) do + price_parsed = parse_price_or_amount(price) + transaction_params = Map.put(transaction_params, "price", price_parsed) + + amount_parsed = parse_price_or_amount(amount) + Map.put(transaction_params, "amount", amount_parsed) + end + + def parse_price_or_amount(string) do + parsingError = %{ + hasError: true, + isDotUsed: false, + sign: nil, + num: "", + res: 0 + } + + %{sign: sign, num: num, res: res, isDotUsed: isDotUsed, hasError: hasError} = + Enum.reduce( + String.graphemes(string), + %{sign: "+", num: "", res: 0, isDotUsed: false, hasError: false}, + fn symbol, %{sign: sign, num: num, res: res, isDotUsed: isDotUsed, hasError: hasError} -> + cond do + hasError or + (Enum.member?(["+", "-"], symbol) and num === "") or + ((symbol === "." and isDotUsed) or + (symbol === "." and num === "")) -> + parsingError + + Enum.member?(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], symbol) -> + %{ + hasError: false, + sign: sign, + num: num <> symbol, + res: res, + isDotUsed: isDotUsed + } + + symbol === "." -> + %{ + hasError: false, + sign: sign, + num: num <> symbol, + res: res, + isDotUsed: true + } + + Enum.member?(["+", "-"], symbol) -> + %{ + hasError: false, + sign: symbol, + num: "", + isDotUsed: false, + res: + res + + if sign === "+" do + {parsed, _} = Float.parse(num) + parsed + else + {parsed, _} = Float.parse(num) + -parsed + end + } + + true -> + parsingError + end + end + ) + + cond do + hasError -> + "error" + + true -> + if num === "" do + "" + else + res = + res + + if sign === "+" do + {parsed, _} = Float.parse(num) + parsed + else + {parsed, _} = Float.parse(num) + -parsed + end + + [integer_part, decimal_part] = String.split(Float.to_string(res), ".") + + if decimal_part == "0" do + String.to_integer(integer_part) + else + res + end + end + end + end end diff --git a/lib/simp_web/live/transaction_live/form_component.html.leex b/lib/simp_web/live/transaction_live/form_component.html.leex index cf13faa..d8ddb53 100644 --- a/lib/simp_web/live/transaction_live/form_component.html.leex +++ b/lib/simp_web/live/transaction_live/form_component.html.leex @@ -53,13 +53,13 @@