Thursday, June 04, 2026 1:25:36 AM
> notifications_controller.rb
# frozen_string_literal: true

class NotificationsController < AuthenticatedController
  before_action :check_for_community_access!
  before_action :redirect_if_player_mode!

  def index
    render locals: {
      colors: load_colors,
      filters: load_filters,
      grouped_notification_types: load_grouped_notification_types,
      notifications: load_notifications,
      variables: load_variables
    }
  end

  def create
    permitted_params = permit_params

    current_community.notifications.create!(permitted_params)

    flash[:success] = "Notification created"
    redirect_to community_notifications_path(current_community, filter: "recent")
  end

  def edit
    notification = find_notification
    not_found! if notification.nil?

    notification_color = notification.notification_color.downcase
    if notification_color != "random" && !ESM::Color::Toast.to_h.value?(notification_color)
      custom_color = notification_color
      notification_color = "custom"
    end

    render locals: {
      notification:,
      notification_color:,
      custom_color:,
      colors: load_colors,
      grouped_notification_types: load_grouped_notification_types,
      variables: load_variables
    }
  end

  def update
    notification = find_notification
    not_found! if notification.nil?

    permitted_params = permit_params
    notification.update!(permitted_params)

    render turbo_stream: [
      turbo_stream.replace(
        notification.dom_id,
        partial: "notification_card",
        locals: {notification:}
      ),
      hide_modal("#edit_notification_modal"),
      create_success_toast("Notification updated")
    ]
  end

  def destroy
    notification = find_notification
    not_found! if notification.nil?

    notification.destroy!

    # Determine if we need to refresh the page (with 0 left) or remove and update the counter
    notifications = current_community.notifications.order(:community_id)
    remaining_count = filter_notifications(notifications).size

    if remaining_count == 0
      flash[:success] = "Notification deleted"
      actions = [turbo_stream.refresh(request_id: nil)]
    else
      actions = [
        turbo_stream.remove(notification.dom_id),
        turbo_stream.update("notification_count", remaining_count.to_s),
        create_success_toast("Notification deleted")
      ]
    end

    render turbo_stream: actions
  end

  private

  def permit_params
    permitted_params = params.require(:notification).permit(
      :notification_type, :notification_color, :notification_custom_color,
      :notification_title, :notification_description
    )

    category, type = permitted_params[:notification_type]&.split("_")
    permitted_params[:notification_category] = category || "xm8"
    permitted_params[:notification_type] = type || "base-raid"

    custom_color = permitted_params.delete(:notification_custom_color)
    if (color = permitted_params[:notification_color]) && color == "custom"
      permitted_params[:notification_color] = custom_color
    end

    permitted_params
  end

  def find_notification
    current_community.notifications.find_by(public_id: params[:notification_id])
  end

  def load_notifications
    notifications = current_community.notifications.order(:community_id, :notification_type)

    filter_notifications(notifications)
      .load # Avoid extra queries
      .sort_by(&:notification_category)
  end

  def filter_notifications(notifications)
    category, type = (params[:filter] || "all").split("_")
    return notifications if category == "all"

    case category
    when "recent"
      return notifications.where("created_at >= ?", 1.day.ago).order(created_at: :desc)
    when "all"
      return notifications
    end

    if ESM::Notification::CATEGORIES.include?(category)
      notifications = notifications.with_category(category)
    end

    case type
    when "actions"
      notifications.with_any_type("heal", "kill")
    when "currency"
      notifications.with_any_type("money", "locker", "respect")
    when "flag"
      notifications.with_any_type("flag-stolen", "flag-restored", "flag-steal-started")
    when "loss"
      notifications.with_type("loss")
    when "money"
      notifications.with_any_type(
        "protection-money-due", "protection-money-paid", "marxet-item-sold"
      )
    when "raid"
      notifications.with_any_type(
        "base-raid", "charge-plant-started", "grind-started", "hack-started"
      )
    when "won"
      notifications.with_type("won")
    else
      # xm8_all, player_all, gambling_all
      notifications
    end
  end

  def load_colors
    ESM::Color::Toast.to_h
      .map { |name, hex| [name.to_s.titleize, hex.downcase] }
      .sort_by(&:first) # Name
      .push(
        ["─────", "", disabled: true],
        ["Custom", "custom"],
        ["Random", "random"]
      )
  end

  def load_filters
    [
      ["All", "all"],
      ["Recently Created", "recent"],
      ["XM8", "xm8_all"],
      ["Gambling", "gambling_all"],
      ["Player", "player_all"],
      ["─────", "", disabled: true],
      ["Flag Events", "xm8_flag"],
      ["Gambling Losses", "gambling_loss"],
      ["Gambling Wins", "gambling_won"],
      ["Money Events", "xm8_money"],
      ["Player Actions", "player_actions"],
      ["Player Currency", "player_currency"],
      ["Raid Events", "xm8_raid"]
    ]
  end

  def load_grouped_notification_types
    [
      ["Gambling Events", [
        ["Loss", "gambling_loss"],
        ["Won", "gambling_won"]
      ]],
      ["Player Management", [
        ["Healed", "player_heal"],
        ["Killed", "player_kill"],
        ["Locker Changed", "player_locker"],
        ["Money Changed", "player_money"],
        ["Respect Changed", "player_respect"]
      ]],
      ["Territory Events", [
        ["Base Raid", "xm8_base-raid"],
        ["Charge Plant Started", "xm8_charge-plant-started"],
        ["Flag Restored", "xm8_flag-restored"],
        ["Flag Steal Started", "xm8_flag-steal-started"],
        ["Flag Stolen", "xm8_flag-stolen"],
        ["Grind Started", "xm8_grind-started"],
        ["Hack Started", "xm8_hack-started"],
        ["MarXet Item Sold", "xm8_marxet-item-sold"],
        ["Protection Money Due", "xm8_protection-money-due"],
        ["Protection Money Paid", "xm8_protection-money-paid"]
      ]]
    ]
  end

  def load_variables
    sample_server = current_community.servers.sample
    sample_username = current_user.discord_username || "PlayerName"

    {
      global: {
        serverID: {
          description: "Shows which server triggered this event (e.g. 'abc4_altis')",
          placeholder: sample_server&.server_id || "#{current_community.community_id}_example"
        },
        serverName: {
          description: "Friendly server name players will recognize",
          placeholder: sample_server&.server_name&.presence || "Example Server"
        },
        communityID: {
          description: "Your community's short ID for player reference",
          placeholder: current_community.community_id
        },
        userName: {
          description: "Steam username of the player involved in this event",
          placeholder: sample_username
        },
        userTag: {
          description: "Discord-style mention format (@username)",
          placeholder: "@#{sample_username}"
        }
      },

      gambling: {
        amountChanged: {
          description: "Net gain/loss from the gambling attempt",
          placeholder: helpers.number_with_delimiter(
            Faker::Commerce.price(range: 1_000..100_000).to_i
          )
        },
        amountGambled: {
          description: "Original bet amount the player risked",
          placeholder: helpers.number_with_delimiter(
            Faker::Commerce.price(range: 1_000..25_000).to_i
          )
        },
        lockerBefore: {
          description: "Player's bank balance before gambling",
          placeholder: helpers.number_with_delimiter(
            Faker::Commerce.price(range: 50_000..500_000).to_i
          )
        },
        lockerAfter: {
          description: "Player's bank balance after gambling",
          placeholder: helpers.number_with_delimiter(
            Faker::Commerce.price(range: 60_000..600_000).to_i
          )
        }
      },

      xm8: {
        territoryID: {
          description: "Territory's public ID that players use in commands",
          placeholder: Faker::Alphanumeric.alpha(number: 5).downcase
        },
        territoryName: {
          description: "Custom territory name set by the owner",
          placeholder: ["Awesome Base", "Fort Knox", "The Compound", "Safe Haven"].sample
        }
      },

      marxet: {
        item: {
          description: "Item classname that was sold on the marketplace",
          placeholder: %w[Exile_Item_PowerDrink Exile_Item_EnergyDrink Exile_Weapon_AK74].sample
        },
        amount: {
          description: "Poptabs received from the marketplace sale",
          placeholder: helpers.number_with_delimiter(
            Faker::Commerce.price(range: 500..50_000).to_i
          )
        }
      },

      player_actions: {
        targetUID: {
          description: "Steam UID of the player who was healed/killed",
          placeholder: "76561198#{Faker::Number.number(digits: 9)}"
        }
      },

      player_currency: {
        targetUID: {
          description: "Steam UID of the player whose currency was modified",
          placeholder: "76561198#{Faker::Number.number(digits: 9)}"
        },
        modifiedAmount: {
          description: "Amount of poptabs/respect that was added or removed",
          placeholder: helpers.number_with_delimiter(
            Faker::Commerce.price(range: 1_000..50_000).to_i
          )
        },
        previousAmount: {
          description: "Player's balance before the admin modification",
          placeholder: helpers.number_with_delimiter(
            Faker::Commerce.price(range: 10_000..100_000).to_i
          )
        },
        newAmount: {
          description: "Player's final balance after the modification",
          placeholder: helpers.number_with_delimiter(
            Faker::Commerce.price(range: 15_000..150_000).to_i
          )
        }
      }
    }
  end
end
All opinions represented herein are my own
- © 2024 - 2026 itsthedevman
- build 4294fb2