diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index d09dd635..c08078e0 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -57,4 +57,16 @@ /* Small, single purpose classes that should take precedence over other styles */ @layer utilities { + .scrollbar::-webkit-scrollbar { + width: 4px; + } + + .scrollbar::-webkit-scrollbar-thumb { + background: #d6d6d6; + border-radius: 10px; + } + + .scrollbar::-webkit-scrollbar-thumb:hover { + background: #a6a6a6; + } } diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb index 396694eb..484fd7c8 100644 --- a/app/controllers/transactions_controller.rb +++ b/app/controllers/transactions_controller.rb @@ -35,7 +35,7 @@ class TransactionsController < ApplicationController respond_to do |format| if @transaction.save - format.html { redirect_to transaction_url(@transaction), notice: "Transaction was successfully created." } + format.html { redirect_to transaction_url(@transaction), notice: t(".success") } else format.html { render :new, status: :unprocessable_entity } end @@ -45,7 +45,13 @@ class TransactionsController < ApplicationController def update respond_to do |format| if @transaction.update(transaction_params) - format.html { redirect_to transaction_url(@transaction), notice: "Transaction was successfully updated." } + format.html { redirect_to transaction_url(@transaction), notice: t(".success") } + format.turbo_stream do + render turbo_stream: [ + turbo_stream.append("notification-tray", partial: "shared/notification", locals: { type: "success", content: t(".success") }), + turbo_stream.replace("transaction_#{@transaction.id}", partial: "transactions/transaction", locals: { transaction: @transaction }) + ] + end else format.html { render :edit, status: :unprocessable_entity } end @@ -56,7 +62,7 @@ class TransactionsController < ApplicationController @transaction.destroy! respond_to do |format| - format.html { redirect_to transactions_url, notice: "Transaction was successfully destroyed." } + format.html { redirect_to transactions_url, notice: t(".success") } end end @@ -68,6 +74,6 @@ class TransactionsController < ApplicationController # Only allow a list of trusted parameters through. def transaction_params - params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded) + params.require(:transaction).permit(:name, :date, :amount, :currency, :notes, :excluded, :category_id) end end diff --git a/app/javascript/controllers/dropdown_controller.js b/app/javascript/controllers/dropdown_controller.js index 610455a0..3a5bf58b 100644 --- a/app/javascript/controllers/dropdown_controller.js +++ b/app/javascript/controllers/dropdown_controller.js @@ -4,20 +4,32 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["menu"] - toggleMenu(event) { - event.stopPropagation(); // Prevent event from closing the menu immediately - this.menuTarget.classList.toggle("hidden"); + toggleMenu = (e) => { + e.stopPropagation(); // Prevent event from closing the menu immediately + this.menuTarget.classList.contains("hidden") ? this.showMenu() : this.hideMenu(); + } + + showMenu = () => { + document.addEventListener("click", this.onDocumentClick); + this.menuTarget.classList.remove("hidden"); } hideMenu = () => { + document.removeEventListener("click", this.onDocumentClick); this.menuTarget.classList.add("hidden"); } - connect() { - document.addEventListener("click", this.hideMenu); + disconnect = () => { + this.hideMenu(); } - disconnect() { - document.removeEventListener("click", this.hideMenu); + onDocumentClick = (e) => { + if (this.menuTarget.contains(e.target)) { + // user has clicked inside of the dropdown + e.stopPropagation(); + return; + } + + this.hideMenu(); } } diff --git a/app/javascript/controllers/modal_controller.js b/app/javascript/controllers/modal_controller.js index 8d0773d4..87cdd3cd 100644 --- a/app/javascript/controllers/modal_controller.js +++ b/app/javascript/controllers/modal_controller.js @@ -8,7 +8,7 @@ export default class extends Controller { } // Hide the dialog when the user clicks outside of it - click_outside(e) { + clickOutside(e) { if (e.target === this.element) { this.element.close(); } diff --git a/app/javascript/tailwindColors.js b/app/javascript/tailwindColors.js index 6031ee02..709d5da0 100644 --- a/app/javascript/tailwindColors.js +++ b/app/javascript/tailwindColors.js @@ -2,6 +2,22 @@ * To keep consistent styling across the app, this file can be imported in * Stimulus controllers to reference our color palette. Mostly used for D3 charts. */ + +export const categoryColors = [ + "#e99537", + "#4da568", + "#6471eb", + "#db5a54", + "#df4e92", + "#c44fe9", + "#eb5429", + "#61c9ea", + "#805dee", + "#6ad28a" +] + +export const categoryDefaultColor = "#737373" + export default { transparent: "transparent", current: "currentColor", @@ -138,4 +154,43 @@ export default { 600: "#7839EE", 700: "#6927DA", }, + fuchsia: { + 25: "#FEFAFF", + 50: "#FDF4FF", + 100: "#FBE8FF", + 200: "#F6D0FE", + 300: "#EEAAFD", + 400: "#E478FA", + 500: "#D444F1", + 600: "#BA24D5", + 700: "#9F1AB1", + 800: "#821890", + 900: "#6F1877", + }, + pink: { + 25: "#FFFAFC", + 50: "#FEF0F7", + 100: "#FFD1E2", + 200: "#FFB1CE", + 300: "#FD8FBA", + 400: "#F86BA7", + 500: "#F23E94", + 600: "#D5327F", + 700: "#BA256B", + 800: "#9E1958", + 900: "#840B45", + }, + orange: { + 25: "#FFF9F5", + 50: "#FFF4ED", + 100: "#FFE6D5", + 200: "#FFD6AE", + 300: "#FF9C66", + 400: "#FF692E", + 500: "#FF4405", + 600: "#E62E05", + 700: "#BC1B06", + 800: "#97180C", + 900: "#771A0D", + }, }; diff --git a/app/models/transaction/category.rb b/app/models/transaction/category.rb index 5c7b370d..042e4edd 100644 --- a/app/models/transaction/category.rb +++ b/app/models/transaction/category.rb @@ -5,14 +5,14 @@ class Transaction::Category < ApplicationRecord before_update :clear_internal_category, if: :name_changed? DEFAULT_CATEGORIES = [ - { internal_category: "income", color: "#fd7f6f" }, - { internal_category: "food_and_drink", color: "#7eb0d5" }, - { internal_category: "entertainment", color: "#b2e061" }, - { internal_category: "personal_care", color: "#bd7ebe" }, - { internal_category: "general_services", color: "#ffb55a" }, - { internal_category: "auto_and_transport", color: "#ffee65" }, - { internal_category: "rent_and_utilities", color: "#beb9db" }, - { internal_category: "home_improvement", color: "#fdcce5" } + { internal_category: "income", color: "#e99537" }, + { internal_category: "food_and_drink", color: "#4da568" }, + { internal_category: "entertainment", color: "#6471eb" }, + { internal_category: "personal_care", color: "#db5a54" }, + { internal_category: "general_services", color: "#df4e92" }, + { internal_category: "auto_and_transport", color: "#c44fe9" }, + { internal_category: "rent_and_utilities", color: "#eb5429" }, + { internal_category: "home_improvement", color: "#61c9ea" } ] def self.ransackable_attributes(auth_object = nil) diff --git a/app/views/shared/_category_badge.html.erb b/app/views/shared/_category_badge.html.erb new file mode 100644 index 00000000..b29f5426 --- /dev/null +++ b/app/views/shared/_category_badge.html.erb @@ -0,0 +1,4 @@ +<%# locals: (name: "Uncategorized", color: "#737373") %> +<% background_color = "color-mix(in srgb, #{color} 5%, white)" %> +<% border_color = "color-mix(in srgb, #{color} 10%, white)" %> +<%= name %> diff --git a/app/views/shared/_sidebar_modal.html.erb b/app/views/shared/_sidebar_modal.html.erb index 52f557d2..796326d6 100644 --- a/app/views/shared/_sidebar_modal.html.erb +++ b/app/views/shared/_sidebar_modal.html.erb @@ -1,5 +1,5 @@ <%= turbo_frame_tag "modal" do %> - +
diff --git a/app/views/transactions/_category_dropdown.html.erb b/app/views/transactions/_category_dropdown.html.erb new file mode 100644 index 00000000..46fe8b44 --- /dev/null +++ b/app/views/transactions/_category_dropdown.html.erb @@ -0,0 +1,42 @@ +<%# locals: (transaction:) %> +
+
+ <%= render partial: "shared/category_badge", locals: transaction.category.nil? ? {} : { name: transaction.category.name, color: transaction.category.color } %> +
+ +
diff --git a/app/views/transactions/_transaction.html.erb b/app/views/transactions/_transaction.html.erb index bc801e38..0a1eace0 100644 --- a/app/views/transactions/_transaction.html.erb +++ b/app/views/transactions/_transaction.html.erb @@ -1,7 +1,12 @@ -<%= link_to transaction_path(transaction), data: { turbo_frame: "modal" }, class: "text-gray-900 flex items-center gap-6 py-4 text-sm font-medium hover:bg-gray-50 px-4", id: dom_id(transaction) do %> -
-
<%= transaction.name[0].upcase %>
-

<%= transaction.name %>

+<%= turbo_frame_tag dom_id(transaction), class:"text-gray-900 flex items-center gap-6 py-4 text-sm font-medium px-4" do %> + <%= link_to transaction_path(transaction), data: { turbo_frame: "modal" }, class: "group", id: dom_id(transaction) do %> +
+
<%= transaction.name[0].upcase %>
+

<%= transaction.name %>

+
+ <% end %> +
+ <%= render partial: "transactions/category_dropdown", locals: { transaction: } %>

<%= transaction.account.name %>

diff --git a/app/views/transactions/index.html.erb b/app/views/transactions/index.html.erb index aedcb523..cb6e8afb 100644 --- a/app/views/transactions/index.html.erb +++ b/app/views/transactions/index.html.erb @@ -50,7 +50,7 @@
<%= form.select :date, options_for_select([['All', 'all'], ['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"]], selected: params.dig(:q, :date)), {}, { class: "block h-full w-full border border-gray-200 rounded-lg text-sm py-2 pr-8 pl-2", "data-transactions-search-target": "date" } %> - + <%= form.hidden_field :date_gteq, value: '', "data-transactions-search-target": "dateGteq" %> <%= form.hidden_field :date_lteq, value: '', "data-transactions-search-target": "dateLteq" %> @@ -68,12 +68,15 @@

transaction

+
+

category

+

account

amount

- +
<% @transactions.group_by { |transaction| transaction.date }.each do |date, grouped_transactions| %> <%= render partial: "transactions/transaction_group", locals: { date: date, transactions: grouped_transactions } %> diff --git a/config/locales/views/transaction/en.yml b/config/locales/views/transaction/en.yml new file mode 100644 index 00000000..685a56c9 --- /dev/null +++ b/config/locales/views/transaction/en.yml @@ -0,0 +1,9 @@ +--- +en: + transactions: + create: + success: New transaction created successfully + destroy: + success: Transaction deleted successfully + update: + success: Transaction updated successfully diff --git a/config/tailwind.config.js b/config/tailwind.config.js index 60660f0b..f8b82f40 100644 --- a/config/tailwind.config.js +++ b/config/tailwind.config.js @@ -1,4 +1,5 @@ const defaultTheme = require("tailwindcss/defaultTheme"); +const colors = require("../app/javascript/tailwindColors"); module.exports = { content: [ @@ -8,184 +9,7 @@ module.exports = { "./app/views/**/*.{erb,haml,html,slim}", ], theme: { - colors: { - transparent: "transparent", - current: "currentColor", - white: "#ffffff", - black: "#0B0B0B", - success: "#10A861", - warning: "#F79009", - error: "#F13636", - gray: { - 25: "#FAFAFA", - 50: "#F5F5F5", - 100: "#F0F0F0", - 200: "#E5E5E5", - 300: "#D6D6D6", - 400: "#A3A3A3 ", - 500: "#737373", - 600: "#525252", - 700: "#3D3D3D", - 800: "#212121", - 900: "#141414", - }, - "alpha-white": { - 25: "rgba(255, 255, 255, 0.03)", - 50: "rgba(255, 255, 255, 0.05)", - 100: "rgba(255, 255, 255, 0.08)", - 200: "rgba(255, 255, 255, 0.1)", - 300: "rgba(255, 255, 255, 0.15)", - 400: "rgba(255, 255, 255, 0.2)", - 500: "rgba(255, 255, 255, 0.3)", - 600: "rgba(255, 255, 255, 0.4)", - 700: "rgba(255, 255, 255, 0.5)", - 800: "rgba(255, 255, 255, 0.6)", - 900: "rgba(255, 255, 255, 0.7)", - }, - "alpha-black": { - 25: "rgba(20, 20, 20, 0.03)", - 50: "rgba(20, 20, 20, 0.05)", - 100: "rgba(20, 20, 20, 0.08)", - 200: "rgba(20, 20, 20, 0.1)", - 300: "rgba(20, 20, 20, 0.15)", - 400: "rgba(20, 20, 20, 0.2)", - 500: "rgba(20, 20, 20, 0.3)", - 600: "rgba(20, 20, 20, 0.4)", - 700: "rgba(20, 20, 20, 0.5)", - 800: "rgba(20, 20, 20, 0.6)", - 900: "rgba(20, 20, 20, 0.7)", - }, - red: { - 25: "#FFFBFB", - 50: "#FFF1F0", - 100: "#FFDEDB", - 200: "#FEB9B3", - 300: "#F88C86", - 400: "#ED4E4E", - 500: "#F13636", - 600: "#EC2222", - 700: "#C91313", - 800: "#A40E0E", - 900: "#7E0707", - }, - green: { - 25: "#F6FEF9", - 50: "#ECFDF3", - 100: "#D1FADF", - 200: "#A6F4C5", - 300: "#6CE9A6", - 400: "#32D583", - 500: "#12B76A", - 600: "#10A861", - 700: "#078C52", - 800: "#05603A", - 900: "#054F31", - }, - yellow: { - 25: "#FFFCF5", - 50: "#FFFAEB", - 100: "#FEF0C7", - 200: "#FEDF89", - 300: "#FEC84B", - 400: "#FDB022", - 500: "#F79009", - 600: "#DC6803", - 700: "#B54708", - 800: "#93370D", - 900: "#7A2E0E", - }, - cyan: { - 25: "#F5FEFF", - 50: "#ECFDFF", - 100: "#CFF9FE", - 200: "#A5F0FC", - 300: "#67E3F9", - 400: "#22CCEE", - 500: "#06AED4", - 600: "#088AB2", - 700: "#0E7090", - 800: "#155B75", - 900: "#155B75", - }, - blue: { - 25: "#F5FAFF", - 50: "#EFF8FF", - 100: "#D1E9FF", - 200: "#B2DDFF", - 300: "#84CAFF", - 400: "#53B1FD", - 500: "#2E90FA", - 600: "#1570EF", - 700: "#175CD3", - 800: "#1849A9", - 900: "#194185", - }, - indigo: { - 25: "#F5F8FF", - 50: "#EFF4FF", - 100: "#E0EAFF", - 200: "#C7D7FE", - 300: "#A4BCFD", - 400: "#8098F9", - 500: "#6172F3", - 600: "#444CE7", - 700: "#3538CD", - 800: "#2D31A6", - 900: "#2D3282", - }, - violet: { - 25: "#FBFAFF", - 50: "#F5F3FF", - 100: "#ECE9FE", - 200: "#DDD6FE", - 300: "#C3B5FD", - 400: "#A48AFB", - 500: "#875BF7", - 600: "#7839EE", - 700: "#6927DA", - 800: "#5720B7", - 900: "#491C96", - }, - fuchsia: { - 25: "#FEFAFF", - 50: "#FDF4FF", - 100: "#FBE8FF", - 200: "#F6D0FE", - 300: "#EEAAFD", - 400: "#E478FA", - 500: "#D444F1", - 600: "#BA24D5", - 700: "#9F1AB1", - 800: "#821890", - 900: "#6F1877", - }, - pink: { - 25: "#FFFAFC", - 50: "#FEF0F7", - 100: "#FFD1E2", - 200: "#FFB1CE", - 300: "#FD8FBA", - 400: "#F86BA7", - 500: "#F23E94", - 600: "#D5327F", - 700: "#BA256B", - 800: "#9E1958", - 900: "#840B45", - }, - orange: { - 25: "#FFF9F5", - 50: "#FFF4ED", - 100: "#FFE6D5", - 200: "#FFD6AE", - 300: "#FF9C66", - 400: "#FF692E", - 500: "#FF4405", - 600: "#E62E05", - 700: "#BC1B06", - 800: "#97180C", - 900: "#771A0D", - }, - }, + colors, boxShadow: { none: "0 0 #0000", inner: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)",