ahoy_captain 0.83 → 0.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -13
  3. data/app/assets/javascript/ahoy_captain/controllers/active_links_controller.js +6 -3
  4. data/app/assets/javascript/ahoy_captain/controllers/filter/item_controller.js +12 -0
  5. data/app/assets/javascript/ahoy_captain/controllers/line_chart_controller.js +4 -4
  6. data/app/assets/javascript/ahoy_captain/controllers/toggle_controller.js +17 -0
  7. data/app/components/ahoy_captain/dropdown_button_component.html.erb +5 -5
  8. data/app/components/ahoy_captain/dropdown_link_component.html.erb +5 -5
  9. data/app/components/ahoy_captain/filter/dropdown_component.html.erb +48 -0
  10. data/app/components/ahoy_captain/filter/dropdown_component.rb +51 -0
  11. data/app/components/ahoy_captain/filter/modal_component.html.erb +2 -2
  12. data/app/components/ahoy_captain/filter/select_component.html.erb +3 -3
  13. data/app/components/ahoy_captain/filter/select_component.rb +22 -8
  14. data/app/components/ahoy_captain/filter/tag_component.html.erb +8 -4
  15. data/app/components/ahoy_captain/filter/tag_component.rb +6 -30
  16. data/app/components/ahoy_captain/filter/tag_container_component.html.erb +2 -3
  17. data/app/components/ahoy_captain/filter/tag_container_component.rb +1 -8
  18. data/app/components/ahoy_captain/stats/container_component.html.erb +8 -0
  19. data/app/components/ahoy_captain/stats/container_component.rb +12 -0
  20. data/app/components/ahoy_captain/sticky_nav_component.html.erb +7 -19
  21. data/app/components/ahoy_captain/sticky_nav_component.rb +8 -0
  22. data/app/components/ahoy_captain/table_component.html.erb +2 -2
  23. data/app/components/ahoy_captain/table_component.rb +3 -0
  24. data/app/components/ahoy_captain/tables/headers/goals_header_component.html.erb +1 -1
  25. data/app/components/ahoy_captain/tables/headers/header_component.html.erb +1 -1
  26. data/app/components/ahoy_captain/tables/rows/goals_row_component.html.erb +1 -1
  27. data/app/components/ahoy_captain/tables/rows/goals_row_component.rb +8 -0
  28. data/app/components/ahoy_captain/tables/rows/row_component.rb +2 -2
  29. data/app/components/ahoy_captain/tile_component.html.erb +4 -3
  30. data/app/components/ahoy_captain/tile_component.rb +1 -1
  31. data/app/components/ahoy_captain/tooltip_component.html.erb +2 -2
  32. data/app/controllers/ahoy_captain/filters/sources_controller.rb +1 -1
  33. data/app/controllers/ahoy_captain/stats/base_controller.rb +3 -3
  34. data/app/helpers/ahoy_captain/application_helper.rb +33 -0
  35. data/app/models/ahoy_captain/filter_parser.rb +67 -0
  36. data/app/presenters/ahoy_captain/goals_presenter.rb +3 -2
  37. data/app/queries/ahoy_captain/stats/average_views_per_visit_query.rb +1 -1
  38. data/app/queries/ahoy_captain/stats/visit_duration_query.rb +1 -1
  39. data/app/views/ahoy_captain/funnels/show.html.erb +5 -2
  40. data/app/views/ahoy_captain/layouts/application.html.erb +3 -3
  41. data/app/views/ahoy_captain/realtimes/show.html.erb +1 -1
  42. data/app/views/ahoy_captain/roots/_filters.html.erb +34 -0
  43. data/app/views/ahoy_captain/roots/show.html.erb +19 -89
  44. data/app/views/ahoy_captain/stats/base/index.html.erb +3 -3
  45. data/app/views/ahoy_captain/stats/show.html.erb +7 -52
  46. data/lib/ahoy_captain/ahoy/event_methods.rb +12 -3
  47. data/lib/ahoy_captain/ahoy/visit_methods.rb +1 -1
  48. data/lib/ahoy_captain/configuration.rb +16 -6
  49. data/lib/ahoy_captain/engine.rb +17 -0
  50. data/lib/ahoy_captain/filter_configuration/filter.rb +16 -0
  51. data/lib/ahoy_captain/filter_configuration/filter_collection.rb +48 -0
  52. data/lib/ahoy_captain/filters_configuration.rb +73 -0
  53. data/lib/ahoy_captain/goals.rb +1 -1
  54. data/lib/ahoy_captain/predicate_label.rb +7 -0
  55. data/lib/ahoy_captain/version.rb +1 -1
  56. data/lib/ahoy_captain.rb +1 -0
  57. data/lib/generators/ahoy_captain/templates/config.rb.tt +25 -0
  58. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc0360816004f1a5f8cf41e01f4a55f30b0ff40db97084122678c1b179adf930
4
- data.tar.gz: a05b89988c2220ca1077bc7eecc80ad1454f777f3eff8621d91b323c52ce6ad7
3
+ metadata.gz: bdc2d607961f89d3b62c551c8aa157c962ab05e0fe7a61179c6c1c9de1c830d6
4
+ data.tar.gz: feef80dd54cb92203aab1468fa80c134e46e8e04f5bf2c6684a83d856ab05ec4
5
5
  SHA512:
6
- metadata.gz: 1e9806cda9cb8366c5639db6d87fe3983cdd327cff484af95782e8c68134fb9c0f3ed55f8b4b7d269ce41b125bbb5da2faf2f4cf2fc1f190faa5c77bf486e2cc
7
- data.tar.gz: 3ac41065fd0f767d951785c9a46373318ace8d2c4099390d33ae17f2d39701843a4d8c478dbb8232880d8c9b2cc89bf83a0a7ec7ec6065a52281db41d33e9b70
6
+ metadata.gz: 43b863dd93ad7a3139c2c73564b1edde7f1d4bbba9540466852cb5cce0f930c0eef8f9505eb513e98c1800a6a0c3f2f8563d20568b17a5b0c65c4e61ad52e433
7
+ data.tar.gz: 6078f2d0937c955073c150c10979ea9099be46ae2d2382f317369f429b5237d4d381303d28e1aa0e6a3b917e1010b170d5dfcf1bf331383889e6f7d5e699f260
data/README.md CHANGED
@@ -1,17 +1,12 @@
1
- # AhoyCaptain
1
+ # <img src="logo.png" style="max-height:100px" /> AhoyCaptain
2
2
 
3
- <img src="logo.png" style="max-width:100px" />
4
3
 
5
4
  A full-featured, mountable analytics dashboard for your Rails app, shamelessly inspired by Plausible Analytics, powered by the Ahoy gem.
6
5
 
7
- <a href="https://github.com/joshmn/ahoy_captain/blob/main/ss.jpg"><img src="ss.jpg" style="max-width:300px" /></a>
6
+ <a href="https://github.com/joshmn/ahoy_captain/blob/main/ss.png"><img src="ss.png" style="max-width:300px" /></a>
8
7
  ## Notice
9
8
 
10
- While this is fine to use in production, it was only built against a PostgreSQL instance. Some of the queries are certainly broken.
11
-
12
- ## Some assumptions
13
-
14
- Some hardcoded stuff as of writing; this will be more fully-featured in due time.
9
+ Currently requires using PG and a JSONB column for your data.
15
10
 
16
11
  ## Installation
17
12
 
@@ -29,11 +24,24 @@ $ bundle add ahoy_captain
29
24
  $ rails g ahoy_captain:install
30
25
  ```
31
26
 
32
- ### 3. Star this repo
27
+ ### 3. Make sure your events are setup correctly
28
+
29
+ By default, AhoyCaptain assumes you're tracking `controller` and `action` in your `Ahoy::Event` properties, and a page view event is named `$view`.
30
+
31
+ For a quick sanity check:
32
+
33
+ ```ruby
34
+ AhoyCaptain.event.where(name: AhoyCaptain.config.event[:view_name]).count
35
+ AhoyCaptain.event.with_routes.count
36
+ ```
37
+
38
+ See the initializer `config/initializers/ahoy_captain.rb` for further customization.
39
+
40
+ ### 4. Star this repo
33
41
 
34
42
  No, seriously, I need all the internet clout I can get.
35
43
 
36
- ### 4. Analyze your nightmares
44
+ ### 5. Analyze your nightmares
37
45
 
38
46
  If you have a large dataset (> 1GB) you probably want some indexes. `rails g ahoy_captain:migration`
39
47
 
@@ -52,12 +60,12 @@ If you have a large dataset (> 1GB) you probably want some indexes. `rails g aho
52
60
  * Device type
53
61
  * OS
54
62
  * UTM tags
63
+ * Goal
64
+ * CSV exports
55
65
 
56
66
  ## Coming soon ™️
57
67
 
58
68
  * Date comparison
59
- * More filters
60
- * CSV exports
61
69
 
62
70
  ## Contributors
63
71
 
@@ -65,7 +73,7 @@ This was built during the Rails Hackathon in July 2023 with [afogel](https://git
65
73
 
66
74
  ## Contributions
67
75
 
68
- Please and thank you in advance!
76
+ Do your worst; please and thank you in advance! :)
69
77
 
70
78
  ## License
71
79
 
@@ -2,14 +2,17 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
4
  static targets = ["link"]
5
+ static values = {
6
+ classes: { type: Array, default: ["text-primary", "font-semibold"] }
7
+ }
8
+
5
9
  connect() {
6
10
  this.handleLinkClick = (event) => {
7
- this.linkTargets.forEach(link => link.classList.remove('text-primary', 'font-semibold'))
8
- event.target.classList.add('text-primary', 'font-semibold')
11
+ this.linkTargets.forEach(link => this.classesValue.forEach(klass => link.classList.remove(klass)))
12
+ this.classesValue.forEach(klass => event.target.classList.add(klass))
9
13
  }
10
14
  this.linkTargets.forEach(link => {
11
15
  link.addEventListener('click', this.handleLinkClick)
12
16
  })
13
17
  }
14
-
15
18
  }
@@ -0,0 +1,12 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="filter--item"
4
+ export default class extends Controller {
5
+ static values = {
6
+ modal: String
7
+ };
8
+
9
+ openModal() {
10
+ document.getElementById(this.modalValue).showModal()
11
+ }
12
+ }
@@ -9,7 +9,6 @@ export default class extends Controller {
9
9
  const getCSS = (varname) => {
10
10
  return `hsl(${getComputedStyle(document.documentElement).getPropertyValue(varname)})`
11
11
  }
12
- Chart.register(Chart.Colors)
13
12
 
14
13
  new Chart(this.element,
15
14
  {
@@ -20,9 +19,9 @@ export default class extends Controller {
20
19
  {
21
20
  label: this.labelValue,
22
21
  data: Object.values(this.dataValue),
23
- borderColor: getCSS('--p'),
24
- backgroundColor: getCSS('--af'),
25
- color: getCSS('--pc')
22
+ borderColor: getCSS('--s'),
23
+ backgroundColor: getCSS('--sc'),
24
+ color: getCSS('--sf')
26
25
  }
27
26
  ]
28
27
  },
@@ -33,5 +32,6 @@ export default class extends Controller {
33
32
  }
34
33
  },
35
34
  )
35
+
36
36
  }
37
37
  }
@@ -0,0 +1,17 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // Connects to data-controller="toggle"
4
+ export default class extends Controller {
5
+ static targets = ['toggleable'];
6
+ static values = {
7
+ enable: Boolean
8
+ }
9
+
10
+ trigger() {
11
+ if (this.enableValue) {
12
+ this.toggleableTargets.forEach(element => {
13
+ element.classList.toggle('hidden');
14
+ });
15
+ }
16
+ }
17
+ }
@@ -1,16 +1,16 @@
1
1
  <div class="dropdown dropdown-end">
2
- <label
3
- tabindex="0"
4
- class="btn btn-ghost dark:hover:bg-neutral flex"
2
+ <label
3
+ tabindex="0"
4
+ class="btn btn-ghost dark:hover:bg-neutral flex"
5
5
  >
6
6
  <%= header_icon %>
7
7
  <span><%= title %></span>
8
8
  </label>
9
- <ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
9
+ <ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box">
10
10
  <% options.each do |option| %>
11
11
  <li>
12
12
  <%= option %>
13
13
  <li>
14
14
  <% end %>
15
15
  </ul>
16
- </div>
16
+ </div>
@@ -1,7 +1,7 @@
1
- <div class="dropdown dropdown-end" data-controller='dropdown-label'>
2
- <label
3
- tabindex="0"
4
- class="cursor-pointer flex <%= classes %>"
1
+ <div class="dropdown dropdown-end " data-controller='dropdown-label'>
2
+ <label
3
+ tabindex="0"
4
+ class="cursor-pointer flex <%= classes %>"
5
5
  data-action='click->dropdown-label#removeHidden'
6
6
  >
7
7
  <span data-dropdown-label-target="label"><%= title %></span>
@@ -16,4 +16,4 @@
16
16
  <li>
17
17
  <% end %>
18
18
  </ul>
19
- </div>
19
+ </div>
@@ -0,0 +1,48 @@
1
+ <div class="dropdown dropdown-end">
2
+ <label
3
+ tabindex="0"
4
+ class="btn btn-ghost dark:hover:bg-neutral flex"
5
+ >
6
+ <%= header_icon %>
7
+ <span><%= title %></span>
8
+ </label>
9
+
10
+ <ul class="w-72 dropdown-content z-[1] p-2 shadow bg-base-100 rounded-box" data-controller='toggle' data-toggle-enable-value="<%= advanced_filter_menu? %>">
11
+ <% if advanced_filter_menu? %>
12
+ <div id="advanced-filters" class="w-full text-sm leading-tight" >
13
+ <button class='w-full cursor-pointer block pl-4 pt-1 text-left hover:text-primary' data-action="click->toggle#trigger">+ Add filter</button>
14
+ <div class="divider my-1"></div>
15
+ <% filters.each do |_, filter| %>
16
+ <li class='flex flex-inline px-4 items-center' data-toggle-target='toggleable'>
17
+ <button title="Edit filter: <%= filter.title %>"
18
+ class="flex w-full justify-between link no-underline items-center group <% if filter.modal %>cursor-pointer<% else %>cursor-text<% end %> text-left"
19
+ onclick="<% if filter.modal %><%= filter.modal %>.showModal() <% end %>">
20
+ <span class="truncate w-48 ">
21
+ <%= filter.description %>
22
+ </span>
23
+ <% if filter.modal %>
24
+ <span class="group-hover:text-primary hover:text-primary ">
25
+ <%= edit_icon %>
26
+ </span>
27
+ <% end %>
28
+ </button>
29
+ <a title="Remove filter: <%= filter.title %>"
30
+ class="hover:text-primary link no-underline pl-2" href="<%= filter.url %>">
31
+ <%= remove_icon %>
32
+ </a>
33
+ <li>
34
+ <div class="divider my-1" data-toggle-target='toggleable'></div>
35
+ <% end %>
36
+ <li data-toggle-target='toggleable'>
37
+ <a class="block mx-auto pl-4 pb-1 " href="<%= AhoyCaptain::Engine.app.url_helpers.root_path %>">Clear all filters</a>
38
+ </li>
39
+ </div>
40
+ <% end %>
41
+
42
+ <div id="core-filters" class="menu <%= 'hidden' if advanced_filter_menu? %> pt-0" data-toggle-target='toggleable'>
43
+ <% AhoyCaptain.config.filters.each do |label, filter_group| %>
44
+ <li><button onClick="<%= filter_group.modal_name %>.showModal()" class='link no-underline' data-action="click->toggle#trigger"><%= label %></button></li>
45
+ <% end %>
46
+ </div>
47
+ </ul>
48
+ </div>
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AhoyCaptain::Filter::DropdownComponent < ViewComponent::Base
4
+ def initialize(filters:)
5
+ @filters = filters
6
+ end
7
+
8
+ private
9
+
10
+ attr_reader :filters
11
+
12
+ def header_icon
13
+ advanced_filter_menu? ? filters_icon : magnifier_icon
14
+ end
15
+
16
+ def title
17
+ advanced_filter_menu? ? "#{filters.size} Filters" : 'Filter'
18
+ end
19
+
20
+ def advanced_filter_menu?
21
+ filter_categories.count >= ::AhoyCaptain::FilterParser::FILTER_MENU_MAX_SIZE
22
+ end
23
+
24
+ def filter_categories
25
+ filters.values.map(&:values).flatten
26
+ end
27
+
28
+ def magnifier_icon
29
+ %Q(
30
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="-ml-1 mr-1 h-4 w-4 md:h-4 md:w-4"><path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd"></path></svg>
31
+ ).html_safe
32
+ end
33
+
34
+ def filters_icon
35
+ %Q(
36
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="-ml-1 mr-1 h-4 w-4"><path d="M17 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM17 15.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM3.75 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5a.75.75 0 01.75-.75zM4.5 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM10 11a.75.75 0 01.75.75v5.5a.75.75 0 01-1.5 0v-5.5A.75.75 0 0110 11zM10.75 2.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM10 6a2 2 0 100 4 2 2 0 000-4zM3.75 10a2 2 0 100 4 2 2 0 000-4zM16.25 10a2 2 0 100 4 2 2 0 000-4z"></path></svg>
37
+ ).html_safe
38
+ end
39
+
40
+ def edit_icon
41
+ %Q(
42
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="w-4 h-4 ml-1 cursor-pointer"><path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"></path><path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"></path></svg>
43
+ ).html_safe
44
+ end
45
+
46
+ def remove_icon
47
+ %Q(
48
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="w-4 h-4"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"></path></svg>
49
+ ).html_safe
50
+ end
51
+ end
@@ -3,8 +3,8 @@
3
3
  <fieldset>
4
4
  <h5><%= title %></h5>
5
5
  <%= modal_display %>
6
- <button class="btn btn-primary mt-4" type="submit">Apply</button>
7
- <button class="btn btn-primary mt-4" type="reset">Reset</button>
6
+ <button class="btn btn-active btn-primary mt-4" type="submit">Apply</button>
7
+ <button class="btn btn-active btn-primary mt-4" type="reset">Reset</button>
8
8
  </fieldset>
9
9
  </div>
10
10
  <label class="modal-backdrop">
@@ -2,9 +2,9 @@
2
2
  <div class="flex flex-col w-[20%]">
3
3
  <label class="label"><%= label %></label>
4
4
  <select class='select select-bordered' data-action='change->predicate-select#handleChange'>
5
- <% predicate_options.each do |predicate| %>
6
- <option value="<%= option_value(predicate) %>" <%= 'selected' if selected_predicate == predicate %>>
7
- <%= predicate.to_s.humanize %>
5
+ <% @predicates.each do |predicate| %>
6
+ <option value="<%= option_value(predicate) %>" <%= 'selected' if selected_predicate?(predicate) %>>
7
+ <%= predicate_label(predicate) %>
8
8
  </option>
9
9
  <% end %>
10
10
  </select>
@@ -12,27 +12,41 @@ class AhoyCaptain::Filter::SelectComponent < ViewComponent::Base
12
12
 
13
13
  private
14
14
 
15
- def selected_predicate
16
- predicate_options.detect { |option| params.dig(:q, option) }
15
+ def selected_predicate?(predicate)
16
+ params.dig(:q, predicate_name(predicate)).present?
17
17
  end
18
18
 
19
19
  def option_value(predicate)
20
- name = "q[#{predicate}]"
20
+ name = "q[#{predicate_name(predicate)}]"
21
21
  name += "[]" if multiple
22
22
  name
23
23
  end
24
24
 
25
+ def predicate_name(predicate)
26
+ "#{@column}_#{predicate}"
27
+ end
28
+
29
+ def selected_predicate
30
+ @predicates.each do |predicate|
31
+ if params.dig(:q, predicate_name(predicate))
32
+ return predicate
33
+ end
34
+ end
35
+
36
+ nil
37
+ end
38
+
25
39
  def column_name_with_predicate
26
40
  if selected_predicate
27
41
  option_value(selected_predicate)
28
42
  else
29
- option_value(predicate_options.first)
43
+ option_value(@predicates.first)
30
44
  end
31
45
  end
32
46
 
33
47
  def values
34
- predicate_options.each do |predicate|
35
- option = params.dig(:q, predicate)
48
+ @predicates.each do |predicate|
49
+ option = params.dig(:q, predicate_name(predicate))
36
50
  if option
37
51
  return option
38
52
  end
@@ -41,8 +55,8 @@ class AhoyCaptain::Filter::SelectComponent < ViewComponent::Base
41
55
  []
42
56
  end
43
57
 
44
- def predicate_options
45
- @predicate_options ||= @predicates.map { |predicate| "#{@column}_#{predicate}" }
58
+ def predicate_label(predicate)
59
+ AhoyCaptain::PredicateLabel[predicate]
46
60
  end
47
61
 
48
62
  attr_reader :label, :column, :url, :predicates, :form, :multiple
@@ -1,7 +1,11 @@
1
- <div class="bg-base-100 py-2 px-4 mx-2 flex items-center" data-controller='filter-tag' data-filter-tag-category-value="<%= category %>" data-filter-tag-column-predicate-value="<%= column_predicate %>">
2
- <%= column_with_predicate %>
3
- <span class="font-bold mx-2"> <%= category %></span>
4
- <a class="hover:text-primary" href="<%= url %>">
1
+ <div class="bg-base-200 py-2 px-4 mx-2 flex items-center text-base-content" data-controller="filter--item" data-filter--item-modal-value="<%= tag_item.modal %>">
2
+ <span data-action='click->filter--item#openModal'>
3
+ <%= tag_item.column.titleize %> <%= tag_item.predicate %>
4
+ <span class="font-bold">
5
+ <%= tag_item.label %>
6
+ </span>
7
+ </span>
8
+ <a class="hover:text-primary" href="<%= tag_item.url %>">
5
9
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="w-4 h-4">
6
10
  <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"></path>
7
11
  </svg>
@@ -1,38 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class AhoyCaptain::Filter::TagComponent < ViewComponent::Base
4
- PREDICATES = {
5
- eq: 'equals',
6
- not_eq: 'not equals',
7
- cont: 'contains',
8
- in: 'in',
9
- not_in: 'not in',
10
- }
11
- def initialize(column_predicate:, category:)
12
- @column_predicate = column_predicate
13
- @category = category
4
+ def initialize(tag_item:)
5
+ @tag_item = tag_item
14
6
  end
15
7
 
16
- def url
17
- search_params = helpers.search_params.deep_dup
18
- if search_params["q"][@column_predicate].is_a?(Array)
19
- search_params["q"][@column_predicate] = search_params["q"][@column_predicate] - [@category]
20
- else
21
- search_params["q"].delete(@column_predicate)
22
- end
8
+ private
9
+ attr_reader :tag_item
23
10
 
24
- request.path + "?" + search_params.to_query
25
- end
26
-
27
- private
28
- attr_reader :column_predicate, :category
29
-
30
- def column_with_predicate
31
- PREDICATES.keys.map(&:to_s).each do |predicate|
32
- if column_predicate.include?(predicate)
33
- before, match, after = column_predicate.partition(predicate)
34
- return [before.gsub('_',' '), PREDICATES[match.to_sym]].join(' ').humanize
35
- end
36
- end
11
+ def modal
12
+ tag_item.modal
37
13
  end
38
14
  end
@@ -1,5 +1,4 @@
1
- <% filters.each do |param| %>
2
- <% column_predicate, category = param %>
3
- <%= render AhoyCaptain::Filter::TagComponent.new(column_predicate: column_predicate, category: category) %>
1
+ <% ::AhoyCaptain::FilterParser.parse(request).each do |_, filter| %>
2
+ <%= render AhoyCaptain::Filter::TagComponent.new(tag_item: filter) %>
4
3
  <% end %>
5
4
 
@@ -1,13 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class AhoyCaptain::Filter::TagContainerComponent < ViewComponent::Base
4
- def initialize(filters:)
5
- @filters = filters.to_unsafe_h.to_a.map do |key, values|
6
- Array(values).map { |value| [key, value] }
7
- end.flatten(1).reject { |k,v| v.blank? }
8
-
4
+ def initialize
9
5
  end
10
-
11
- private
12
- attr_reader :filters
13
6
  end
@@ -0,0 +1,8 @@
1
+ <a href="<%= @url %>" class="hover:text-primary text-base-content link no-underline" data-turbo-frame="chart">
2
+ <dt class="font-normal <%= 'text-primary' if @selected %>" data-active-links-target="link"><%= @label %></dt>
3
+ <dd class="mt-1 flex items-baseline justify-between md:block lg:flex">
4
+ <div class="flex items-baseline text-2xl font-semibold">
5
+ <%= @value %>
6
+ </div>
7
+ </dd>
8
+ </a>
@@ -0,0 +1,12 @@
1
+ module AhoyCaptain
2
+ module Stats
3
+ class ContainerComponent < ViewComponent::Base
4
+ def initialize(url, label, value, selected = false)
5
+ @url = url
6
+ @label = label
7
+ @value = value
8
+ @selected = selected
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,37 +1,25 @@
1
- <div class="max-w-6xl flex justify-between sticky top-0 min-h-sm z-[99999] bg-base-300 py-4">
1
+ <div class="max-w-6xl flex justify-between sticky top-0 min-h-sm z-[99999] py-4 bg-base-100">
2
2
  <div class="flex items-center">
3
3
  <a href="/">
4
4
  <img src="<%= image_path "ahoy_captain/logo.png" %>" alt="AhoyCaptainLogo" class='w-16 h-16 rounded-full'>
5
5
  </a>
6
- <% if params[:q] %>
7
- <%= render AhoyCaptain::Filter::TagContainerComponent.new(filters: params.require(:q)) %>
6
+ <% if tag_list_hidden? %>
7
+ <%= render AhoyCaptain::Filter::TagContainerComponent.new %>
8
8
  <% else %>
9
9
  <%= realtime_update %>
10
10
  <% end %>
11
11
  </div>
12
12
  <div class="flex flex-row-reverse col-span-2 items-center">
13
- <%= render AhoyCaptain::DropdownLinkComponent.new(title: params[:start_date] ? "Custom Range" : (AhoyCaptain.config.ranges.find(params[:period]).try(:label) || "Period"), classes: 'btn btn-base-100 no-underline text-base-content hover:text-base-content hover:bg-base-100') do |dropdown| %>
13
+ <%= render AhoyCaptain::DropdownLinkComponent.new(title: params[:start_date] ? "Custom Range" : (AhoyCaptain.config.ranges.find(params[:period]).try(:label) || "Period"), classes: 'btn btn-base-100 no-underline hover:bg-base-100') do |dropdown| %>
14
14
  <% dropdown.with_option do %>
15
15
  <% AhoyCaptain.config.ranges.each do |param, range| %>
16
16
  <a class='link no-underline' href="<%= request.path %>?<%= request.query_parameters.except("start_date", "end_date").merge("period" => param).to_query %>"><%= range.label %></a>
17
17
  <% end %>
18
18
 
19
- <a class='link no-underline' href='#' onclick="event.preventDefault(); customRangeModal.showModal()">Custom range</a>
20
- <% end %>
21
- <% end %>
22
- <%= render AhoyCaptain::DropdownButtonComponent.new(title: 'Filter') do |dropdown| %>
23
- <% dropdown.with_header_icon do %>
24
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="-ml-1 mr-1 h-4 w-4 md:h-4 md:w-4">
25
- <path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd"></path>
26
- </svg>
27
- <% end %>
28
- <% dropdown.with_option do %>
29
- <button onClick="pageModal.showModal()" class='link no-underline'>Page</button>
30
- <button onClick="countryModal.showModal()" class='link no-underline'>Geography</button>
31
- <button onClick="screenModal.showModal()" class='link no-underline'>Screen</button>
32
- <button onClick="osModal.showModal()" class='link no-underline'>Operating System</button>
33
- <button onClick="utmModal.showModal()" class='link no-underline'>UTM Tags</button>
19
+ <a class='link no-underline ' href='#' onclick="event.preventDefault(); customRangeModal.showModal()">Custom range</a>
34
20
  <% end %>
35
21
  <% end %>
22
+
23
+ <%= render AhoyCaptain::Filter::DropdownComponent.new(filters: filters) %>
36
24
  </div>
37
25
  </div>
@@ -2,4 +2,12 @@
2
2
 
3
3
  class AhoyCaptain::StickyNavComponent < ViewComponent::Base
4
4
  renders_one :realtime_update
5
+
6
+ def filters
7
+ @filters ||= ::AhoyCaptain::FilterParser.parse(request)
8
+ end
9
+
10
+ def tag_list_hidden?
11
+ filters.values.map(&:values).flatten.size < ::AhoyCaptain::FilterParser::FILTER_MENU_MAX_SIZE
12
+ end
5
13
  end
@@ -1,6 +1,6 @@
1
- <div class="flex flex-col min-h-[380px] w-full pt-4">
1
+ <div class="flex flex-col <%= 'min-h-[380px]' if fixed_height? %> w-full pt-4">
2
2
  <%= render @header %>
3
- <div class='min-h-[420px]'>
3
+ <div class='<%= 'min-h-[420px]' if fixed_height? %>'>
4
4
  <div class="grow">
5
5
  <% if items.respond_to?(:each) && items.any? %>
6
6
  <% items.each do |item| %>
@@ -33,4 +33,7 @@ class AhoyCaptain::TableComponent < ViewComponent::Base
33
33
  @total ||= items.first.total_count
34
34
  end
35
35
 
36
+ def fixed_height?
37
+ !@header.is_a?(::AhoyCaptain::Tables::Headers::GoalsHeaderComponent)
38
+ end
36
39
  end
@@ -1,4 +1,4 @@
1
- <div class="flex text-sm font-bold text-base-content mb-4">
1
+ <div class="flex text-sm font-bold mb-4">
2
2
  <span class="grow">Goal</span>
3
3
  <span class="w-8 ml-8 text-right">Uniques</span>
4
4
  <span class="w-8 ml-8 text-right">Total</span>
@@ -1,4 +1,4 @@
1
- <div class="flex text-sm font-bold text-base-content mb-4">
1
+ <div class=" flex text-sm font-bold mb-4">
2
2
  <span class="grow"><%= @category_name %></span>
3
3
  <span ><%= @unit_name %></span>
4
4
  <%= content %>
@@ -1,4 +1,4 @@
1
- <%= progress_bar(@item.cr, 100, @item.name) %>
1
+ <%= progress_bar(@item.cr, 100, link_to(@item.name, url, target: :_top)) %>
2
2
  <%= item do %>
3
3
  <%= tooltip(@item.unique_visits) %>
4
4
  <% end %>
@@ -2,6 +2,14 @@ module AhoyCaptain
2
2
  module Tables
3
3
  module Rows
4
4
  class GoalsRowComponent < RowComponent
5
+ def search_params
6
+ view_context.search_params
7
+ end
8
+
9
+ def url
10
+ query = search_params.dup.merge(q: { goal_in: @item.goal_id}).to_query
11
+ AhoyCaptain::Engine.app.url_helpers.root_path + "?#{query}"
12
+ end
5
13
 
6
14
  def total
7
15
  @item.total_count
@@ -10,7 +10,7 @@ module AhoyCaptain
10
10
  def progress_bar(value, max, label)
11
11
  items = []
12
12
  items << view_context.content_tag(:progress, "", class: "progress-primary bg-base-100 h-8 grow", value: value, max: max)
13
- items << view_context.content_tag(:span, class: "grow text-elipsis overflow-hidden absolute left-4 bottom-3 h-8 text-base-content") do
13
+ items << view_context.content_tag(:span, class: "grow text-elipsis overflow-hidden absolute left-4 bottom-3 h-8 text-primary-content") do
14
14
  label
15
15
  end
16
16
 
@@ -18,7 +18,7 @@ module AhoyCaptain
18
18
  end
19
19
 
20
20
  def item(value = nil, &block)
21
- view_context.content_tag(:span, class: "w-8 ml-8 text-right") do
21
+ view_context.content_tag(:span, class: "w-8 ml-8 text-right ") do
22
22
  if value
23
23
  value
24
24
  else