lookout-ahoy 0.1.0

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 (223) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +99 -0
  4. data/Rakefile +24 -0
  5. data/app/assets/images/lookout/apple-touch-icon.png +0 -0
  6. data/app/assets/images/lookout/favicon-16x16.png +0 -0
  7. data/app/assets/images/lookout/favicon-32x32.png +0 -0
  8. data/app/assets/images/lookout/logo.png +0 -0
  9. data/app/assets/images/lookout/safari-pinned-tab.png +0 -0
  10. data/app/assets/images/lookout/safari-pinned-tab.svg +199 -0
  11. data/app/assets/javascript/lookout/application.js +2 -0
  12. data/app/assets/javascript/lookout/controllers/application.js +9 -0
  13. data/app/assets/javascript/lookout/controllers/application_controller.js +33 -0
  14. data/app/assets/javascript/lookout/controllers/combobox_controller.js +371 -0
  15. data/app/assets/javascript/lookout/controllers/details_modal_controller.js +18 -0
  16. data/app/assets/javascript/lookout/controllers/dropdown_label_controller.js +39 -0
  17. data/app/assets/javascript/lookout/controllers/filter/item_controller.js +12 -0
  18. data/app/assets/javascript/lookout/controllers/filter_form_controller.js +13 -0
  19. data/app/assets/javascript/lookout/controllers/filter_modal_controller.js +45 -0
  20. data/app/assets/javascript/lookout/controllers/frame_link_controller.js +20 -0
  21. data/app/assets/javascript/lookout/controllers/funnel_chart_controller.js +159 -0
  22. data/app/assets/javascript/lookout/controllers/index.js +4 -0
  23. data/app/assets/javascript/lookout/controllers/interval_controller.js +15 -0
  24. data/app/assets/javascript/lookout/controllers/line_chart_controller.js +251 -0
  25. data/app/assets/javascript/lookout/controllers/predicate_select_controller.js +10 -0
  26. data/app/assets/javascript/lookout/controllers/properties_controller.js +8 -0
  27. data/app/assets/javascript/lookout/controllers/property_filter_controller.js +45 -0
  28. data/app/assets/javascript/lookout/controllers/realtime_controller.js +30 -0
  29. data/app/assets/javascript/lookout/controllers/sparkline_controller.js +64 -0
  30. data/app/assets/javascript/lookout/controllers/tile_controller.js +33 -0
  31. data/app/assets/javascript/lookout/controllers/toggle_controller.js +17 -0
  32. data/app/assets/javascript/lookout/helpers/chart_utils.js +156 -0
  33. data/app/assets/javascript/lookout/helpers/countries.js +2261 -0
  34. data/app/assets/javascript/lookout/helpers/number_formatters.js +55 -0
  35. data/app/assets/manifest/lookout/manifest.js +2 -0
  36. data/app/components/lookout/combobox_component.html.erb +33 -0
  37. data/app/components/lookout/combobox_component.rb +13 -0
  38. data/app/components/lookout/comparison_link_component.html.erb +17 -0
  39. data/app/components/lookout/comparison_link_component.rb +44 -0
  40. data/app/components/lookout/dropdown_button_component.html.erb +16 -0
  41. data/app/components/lookout/dropdown_button_component.rb +14 -0
  42. data/app/components/lookout/dropdown_link_component.html.erb +17 -0
  43. data/app/components/lookout/dropdown_link_component.rb +19 -0
  44. data/app/components/lookout/filter/dropdown_component.html.erb +50 -0
  45. data/app/components/lookout/filter/dropdown_component.rb +51 -0
  46. data/app/components/lookout/filter/modal_component.html.erb +16 -0
  47. data/app/components/lookout/filter/modal_component.rb +13 -0
  48. data/app/components/lookout/filter/select_component.html.erb +25 -0
  49. data/app/components/lookout/filter/select_component.rb +64 -0
  50. data/app/components/lookout/filter/tag_component.html.erb +13 -0
  51. data/app/components/lookout/filter/tag_component.rb +14 -0
  52. data/app/components/lookout/filter/tag_container_component.html.erb +4 -0
  53. data/app/components/lookout/filter/tag_container_component.rb +6 -0
  54. data/app/components/lookout/previous_next_component.html.erb +8 -0
  55. data/app/components/lookout/previous_next_component.rb +11 -0
  56. data/app/components/lookout/stats/comparable_container_component.html.erb +25 -0
  57. data/app/components/lookout/stats/comparable_container_component.rb +103 -0
  58. data/app/components/lookout/stats/container_component.html.erb +23 -0
  59. data/app/components/lookout/stats/container_component.rb +28 -0
  60. data/app/components/lookout/sticky_nav_component.html.erb +32 -0
  61. data/app/components/lookout/sticky_nav_component.rb +24 -0
  62. data/app/components/lookout/table_component.html.erb +16 -0
  63. data/app/components/lookout/table_component.rb +48 -0
  64. data/app/components/lookout/tables/devices_table_component.rb +11 -0
  65. data/app/components/lookout/tables/dynamic_table.rb +13 -0
  66. data/app/components/lookout/tables/dynamic_table_component.rb +207 -0
  67. data/app/components/lookout/tables/goals_table_component.rb +17 -0
  68. data/app/components/lookout/tables/header_component.html.erb +6 -0
  69. data/app/components/lookout/tables/header_component.rb +18 -0
  70. data/app/components/lookout/tables/headers/header_component.html.erb +5 -0
  71. data/app/components/lookout/tables/headers/header_component.rb +16 -0
  72. data/app/components/lookout/tables/properties_table_component.rb +27 -0
  73. data/app/components/lookout/tables/row_component.html.erb +4 -0
  74. data/app/components/lookout/tables/rows/row_component.html.erb +6 -0
  75. data/app/components/lookout/tables/rows/row_component.rb +40 -0
  76. data/app/components/lookout/tile_component.html.erb +24 -0
  77. data/app/components/lookout/tile_component.rb +24 -0
  78. data/app/components/lookout/tooltip_component.html.erb +3 -0
  79. data/app/components/lookout/tooltip_component.rb +18 -0
  80. data/app/controllers/lookout/application_controller.rb +83 -0
  81. data/app/controllers/lookout/campaigns_controller.rb +19 -0
  82. data/app/controllers/lookout/devices_controller.rb +20 -0
  83. data/app/controllers/lookout/entry_pages_controller.rb +19 -0
  84. data/app/controllers/lookout/exit_pages_controller.rb +19 -0
  85. data/app/controllers/lookout/exports_controller.rb +14 -0
  86. data/app/controllers/lookout/filters/base_controller.rb +15 -0
  87. data/app/controllers/lookout/filters/goals_controller.rb +9 -0
  88. data/app/controllers/lookout/filters/locations_controller.rb +11 -0
  89. data/app/controllers/lookout/filters/operating_systems/names_controller.rb +13 -0
  90. data/app/controllers/lookout/filters/operating_systems/versions_controller.rb +13 -0
  91. data/app/controllers/lookout/filters/pages/actions_controller.rb +13 -0
  92. data/app/controllers/lookout/filters/pages/entry_pages_controller.rb +14 -0
  93. data/app/controllers/lookout/filters/pages/exit_pages_controller.rb +15 -0
  94. data/app/controllers/lookout/filters/properties/names_controller.rb +29 -0
  95. data/app/controllers/lookout/filters/properties/values_controller.rb +15 -0
  96. data/app/controllers/lookout/filters/screens_controller.rb +11 -0
  97. data/app/controllers/lookout/filters/sources_controller.rb +11 -0
  98. data/app/controllers/lookout/filters/utms_controller.rb +10 -0
  99. data/app/controllers/lookout/funnels_controller.rb +8 -0
  100. data/app/controllers/lookout/goals_controller.rb +7 -0
  101. data/app/controllers/lookout/locations/cities_controller.rb +22 -0
  102. data/app/controllers/lookout/locations/countries_controller.rb +22 -0
  103. data/app/controllers/lookout/locations/maps_controller.rb +24 -0
  104. data/app/controllers/lookout/locations/regions_controller.rb +22 -0
  105. data/app/controllers/lookout/properties_controller.rb +73 -0
  106. data/app/controllers/lookout/realtimes_controller.rb +7 -0
  107. data/app/controllers/lookout/roots_controller.rb +6 -0
  108. data/app/controllers/lookout/sources_controller.rb +21 -0
  109. data/app/controllers/lookout/stats/base_controller.rb +148 -0
  110. data/app/controllers/lookout/stats/bounce_rates_controller.rb +12 -0
  111. data/app/controllers/lookout/stats/total_pageviews_controller.rb +10 -0
  112. data/app/controllers/lookout/stats/total_visits_controller.rb +10 -0
  113. data/app/controllers/lookout/stats/unique_visitors_controller.rb +11 -0
  114. data/app/controllers/lookout/stats/views_per_visits_controller.rb +11 -0
  115. data/app/controllers/lookout/stats/visit_durations_controller.rb +10 -0
  116. data/app/controllers/lookout/stats_controller.rb +7 -0
  117. data/app/controllers/lookout/top_pages_controller.rb +20 -0
  118. data/app/decorators/lookout/application_decorator.rb +58 -0
  119. data/app/decorators/lookout/campaign_decorator.rb +27 -0
  120. data/app/decorators/lookout/city_decorator.rb +24 -0
  121. data/app/decorators/lookout/country_decorator.rb +38 -0
  122. data/app/decorators/lookout/device_decorator.rb +27 -0
  123. data/app/decorators/lookout/entry_page_decorator.rb +7 -0
  124. data/app/decorators/lookout/exit_page_decorator.rb +7 -0
  125. data/app/decorators/lookout/page_decorator.rb +27 -0
  126. data/app/decorators/lookout/region_decorator.rb +28 -0
  127. data/app/decorators/lookout/source_decorator.rb +27 -0
  128. data/app/decorators/lookout/top_page_decorator.rb +7 -0
  129. data/app/helpers/lookout/application_helper.rb +124 -0
  130. data/app/models/concerns/lookout/compare_mode.rb +19 -0
  131. data/app/models/concerns/lookout/limitable.rb +17 -0
  132. data/app/models/concerns/lookout/range_options.rb +8 -0
  133. data/app/models/lookout/comparison_mode.rb +72 -0
  134. data/app/models/lookout/export.rb +48 -0
  135. data/app/models/lookout/filter_parser.rb +82 -0
  136. data/app/models/lookout/range_from_params.rb +78 -0
  137. data/app/models/lookout/rangeable.rb +7 -0
  138. data/app/models/lookout/widget.rb +15 -0
  139. data/app/presenters/lookout/dashboard_presenter.rb +53 -0
  140. data/app/presenters/lookout/funnel_presenter.rb +75 -0
  141. data/app/presenters/lookout/goals_presenter.rb +72 -0
  142. data/app/queries/concerns/lookout/comparable_queries.rb +25 -0
  143. data/app/queries/concerns/lookout/comparable_query.rb +152 -0
  144. data/app/queries/concerns/lookout/lazy_comparable_query.rb +42 -0
  145. data/app/queries/lookout/application_query.rb +186 -0
  146. data/app/queries/lookout/campaign_query.rb +14 -0
  147. data/app/queries/lookout/city_query.rb +14 -0
  148. data/app/queries/lookout/country_query.rb +10 -0
  149. data/app/queries/lookout/device_query.rb +10 -0
  150. data/app/queries/lookout/entry_pages_query.rb +18 -0
  151. data/app/queries/lookout/event_query.rb +42 -0
  152. data/app/queries/lookout/exit_pages_query.rb +19 -0
  153. data/app/queries/lookout/region_query.rb +14 -0
  154. data/app/queries/lookout/source_query.rb +11 -0
  155. data/app/queries/lookout/stats/average_views_per_visit_query.rb +20 -0
  156. data/app/queries/lookout/stats/average_visit_duration_query.rb +34 -0
  157. data/app/queries/lookout/stats/base_query.rb +18 -0
  158. data/app/queries/lookout/stats/bounce_rates_query.rb +33 -0
  159. data/app/queries/lookout/stats/total_pageviews_query.rb +9 -0
  160. data/app/queries/lookout/stats/total_visitors_query.rb +9 -0
  161. data/app/queries/lookout/stats/unique_visitors_query.rb +9 -0
  162. data/app/queries/lookout/stats/views_per_visit_query.rb +17 -0
  163. data/app/queries/lookout/stats/visit_duration_query.rb +19 -0
  164. data/app/queries/lookout/top_page_query.rb +13 -0
  165. data/app/queries/lookout/visit_query.rb +42 -0
  166. data/app/views/lookout/campaigns/index.html+details.erb +4 -0
  167. data/app/views/lookout/campaigns/index.html.erb +3 -0
  168. data/app/views/lookout/devices/_table.html.erb +2 -0
  169. data/app/views/lookout/devices/index.html+details.erb +4 -0
  170. data/app/views/lookout/devices/index.html.erb +3 -0
  171. data/app/views/lookout/entry_pages/index.html+details.erb +4 -0
  172. data/app/views/lookout/entry_pages/index.html.erb +3 -0
  173. data/app/views/lookout/exit_pages/index.html+details.erb +4 -0
  174. data/app/views/lookout/exit_pages/index.html.erb +3 -0
  175. data/app/views/lookout/funnels/index.html.erb +7 -0
  176. data/app/views/lookout/funnels/show.html.erb +15 -0
  177. data/app/views/lookout/goals/index.html.erb +4 -0
  178. data/app/views/lookout/layouts/application.html.erb +144 -0
  179. data/app/views/lookout/layouts/shared/_tile_loader.html.erb +5 -0
  180. data/app/views/lookout/layouts/shared/_widget_disabled.html+details.erb +3 -0
  181. data/app/views/lookout/layouts/shared/_widget_disabled.html.erb +3 -0
  182. data/app/views/lookout/locations/cities/index.html+details.erb +4 -0
  183. data/app/views/lookout/locations/cities/index.html.erb +3 -0
  184. data/app/views/lookout/locations/countries/index.html+details.erb +5 -0
  185. data/app/views/lookout/locations/countries/index.html.erb +3 -0
  186. data/app/views/lookout/locations/maps/_simple_map.html.erb +26 -0
  187. data/app/views/lookout/locations/maps/show.html.erb +106 -0
  188. data/app/views/lookout/locations/regions/index.html+details.erb +4 -0
  189. data/app/views/lookout/locations/regions/index.html.erb +3 -0
  190. data/app/views/lookout/properties/_form.html.erb +6 -0
  191. data/app/views/lookout/properties/index.html.erb +3 -0
  192. data/app/views/lookout/properties/show.html.erb +6 -0
  193. data/app/views/lookout/realtimes/show.html.erb +9 -0
  194. data/app/views/lookout/roots/_filters.html.erb +80 -0
  195. data/app/views/lookout/roots/show.html.erb +191 -0
  196. data/app/views/lookout/sources/index.html+details.erb +4 -0
  197. data/app/views/lookout/sources/index.html.erb +3 -0
  198. data/app/views/lookout/stats/base/index.html.erb +40 -0
  199. data/app/views/lookout/stats/show.html.erb +15 -0
  200. data/app/views/lookout/top_pages/index.html+details.erb +4 -0
  201. data/app/views/lookout/top_pages/index.html.erb +3 -0
  202. data/config/routes.rb +69 -0
  203. data/lib/generators/lookout/install_generator.rb +31 -0
  204. data/lib/generators/lookout/migration_generator.rb +21 -0
  205. data/lib/generators/lookout/templates/config.rb.tt +185 -0
  206. data/lib/generators/lookout/templates/migration.rb.tt +7 -0
  207. data/lib/lookout/active_record.rb +108 -0
  208. data/lib/lookout/ahoy/event_methods.rb +75 -0
  209. data/lib/lookout/ahoy/visit_methods.rb +24 -0
  210. data/lib/lookout/configuration.rb +58 -0
  211. data/lib/lookout/database_adapter.rb +168 -0
  212. data/lib/lookout/engine.rb +47 -0
  213. data/lib/lookout/filter_configuration/filter.rb +16 -0
  214. data/lib/lookout/filter_configuration/filter_collection.rb +48 -0
  215. data/lib/lookout/filters_configuration.rb +77 -0
  216. data/lib/lookout/funnels.rb +44 -0
  217. data/lib/lookout/goals.rb +51 -0
  218. data/lib/lookout/period_collection.rb +115 -0
  219. data/lib/lookout/predicate_label.rb +7 -0
  220. data/lib/lookout/railtie.rb +9 -0
  221. data/lib/lookout/version.rb +3 -0
  222. data/lib/lookout.rb +78 -0
  223. metadata +673 -0
@@ -0,0 +1,17 @@
1
+ module Lookout
2
+ module Stats
3
+ class ViewsPerVisitQuery < BaseQuery
4
+ def build
5
+ events = event_query
6
+ .joins(:visit)
7
+ .select("#{::Lookout.visit.table_name}.started_at as started_at, count(#{Lookout.event.table_name}.name) / count(distinct #{Lookout.event.table_name}.visit_id) as views_per_visit")
8
+ .where(name: Lookout.config.event[:view_name])
9
+ .group("#{Lookout.visit.table_name}.started_at, #{Lookout.event.table_name}.visit_id")
10
+
11
+ ::Ahoy::Visit
12
+ .select("views_per_visit as views_per_visit")
13
+ .from(events, :views_per_visit_table)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Lookout
2
+ module Stats
3
+ class VisitDurationQuery < BaseQuery
4
+ def build
5
+ events = event_query
6
+ .reselect("max(#{Lookout.event.table_name}.time) - min(#{Lookout.event.table_name}.time) as duration, #{Lookout.event.table_name}.visit_id")
7
+ .group("#{Lookout.event.table_name}.visit_id")
8
+
9
+ # PostgreSQL has duration type, SQLite stores as numeric seconds
10
+ duration_cast = Lookout::DatabaseAdapter.postgresql? ? "duration::duration" : "duration"
11
+
12
+ ::Ahoy::Visit
13
+ .select("#{duration_cast} as duration, started_at")
14
+ .from(events, :views_per_visit_table)
15
+ .joins("inner join #{Lookout.visit.table_name} on #{Lookout.visit.table_name}.id = views_per_visit_table.visit_id")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module Lookout
2
+ class TopPageQuery < ApplicationQuery
3
+ def build
4
+ event_query.with_routes
5
+ .select(
6
+ "#{Lookout.config.event[:url_column]} as url",
7
+ "count(*) as count",
8
+ "sum(count(*)) over() as total_count"
9
+ ).group(Arel.sql ("(#{Lookout.config.event[:url_column]})"))
10
+ .order(Arel.sql("count(#{Lookout.config.event[:url_column]}) desc"))
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,42 @@
1
+ module Lookout
2
+ class VisitQuery < ApplicationQuery
3
+
4
+ def build
5
+ shared_context = Ransack::Context.for(Lookout.visit)
6
+
7
+ visit_params = ransack_params_for(:visit).reject { |k,v| k.start_with?("events_") }
8
+ event_params = ransack_params_for(:event).reject { |k,v| k.start_with?("visit_") }
9
+
10
+ # Detect if event_params contain only time constraints (and related visit time injects)
11
+ time_keys = %w[time_gt time_lt time_gteq time_lteq visit_started_at_gt visit_started_at_lt]
12
+ event_params_without_time = event_params.reject { |k,_| time_keys.include?(k.to_s) || k.to_s.start_with?("c[") }
13
+ # Keep composite conditions under :c if present (properties), so detect them as filters
14
+ has_event_filters = event_params_without_time.any? || event_params.key?(:c)
15
+
16
+ if has_event_filters
17
+ search_parents = Lookout.visit.ransack(visit_params, context: shared_context)
18
+ search_children = Lookout.event.ransack(event_params.transform_keys { |key| "events_#{key}" }, context: shared_context)
19
+
20
+ shared_conditions = [search_parents, search_children].map { |search|
21
+ Ransack::Visitor.new.accept(search.base)
22
+ }
23
+
24
+ Lookout.visit.joins(shared_context.join_sources)
25
+ .where(shared_conditions.reduce(&:and))
26
+ else
27
+ # No event filters: do not join events; rely on visit constraints only
28
+ search_parents = Lookout.visit.ransack(visit_params, context: shared_context)
29
+ Lookout.visit.where(Ransack::Visitor.new.accept(search_parents.base))
30
+ end
31
+
32
+ end
33
+
34
+ def is_a?(other)
35
+ if other == ActiveRecord::Relation
36
+ return true
37
+ end
38
+
39
+ super(other)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render Lookout::TableComponent.new(items: @campaigns, category_name: @campaign_type, unit_name: 'Visitors') %>
3
+ <span class="flex justify-center"><%= render_pagination %></span>
4
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :sources do %>
2
+ <%= render Lookout::TableComponent.new(items: @campaigns, category_name: @campaign_type, unit_name: 'Visitors') %>
3
+ <% end %>
@@ -0,0 +1,2 @@
1
+ <%= render Lookout::TableComponent.new(items: @devices,
2
+ table: ::Lookout::Tables::DevicesTableComponent) %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render 'table' %>
3
+ <span class="flex justify-center"><%= render_pagination %></span>
4
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :devices do %>
2
+ <%= render 'table' %>
3
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render Lookout::TableComponent.new(items: @pages, category_name: 'Entry Pages', unit_name: 'Visitors') %>
3
+ <span class="flex justify-center"><%= render_pagination %></span>
4
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :pages do %>
2
+ <%= render Lookout::TableComponent.new(items: @pages, category_name: 'Entry Pages', unit_name: 'Visitors') %>
3
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render Lookout::TableComponent.new(items: @pages, category_name: 'Exit Pages', unit_name: 'Visitors') %>
3
+ <span class="flex justify-center"><%= render_pagination %></span>
4
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :pages do %>
2
+ <%= render Lookout::TableComponent.new(items: @pages, category_name: 'Exit Pages', unit_name: 'Visitors') %>
3
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <%= turbo_frame_tag :funnels do %>
2
+ <ul>
3
+ <% @funnels.each do |label, count| %>
4
+ <li><%= label %>: <%= count %></li>
5
+ <% end %>
6
+ </ul>
7
+ <% end %>
@@ -0,0 +1,15 @@
1
+ <%= turbo_frame_tag :goals do %>
2
+ <div class="p-4">
3
+ <% if params.dig(:q, :goal_id) %>
4
+ <p>Funnels are unavailable if filtering by a goal.</p>
5
+ <% elsif @funnel.steps.empty? %>
6
+ <p class="text-gray-400">No funnel data available. Make sure your events match the configured goals.</p>
7
+ <% else %>
8
+ <div style="height: 400px; width: 100%; display: flex; justify-content: center; align-items: center; position: relative; overflow: visible;">
9
+ <div style="width: 70%; height: 100%; position: relative;">
10
+ <canvas data-controller="funnel-chart" data-data="<%= @funnel.to_json %>"></canvas>
11
+ </div>
12
+ </div>
13
+ <% end %>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :goals do %>
2
+ <%= render Lookout::TableComponent.new(items: @presenter.goals,
3
+ table: ::Lookout::Tables::GoalsTableComponent) %>
4
+ <% end %>
@@ -0,0 +1,144 @@
1
+ <!DOCTYPE html>
2
+ <html data-theme='<%= Lookout.config.theme %>'>
3
+ <head>
4
+ <title>Lookout</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+ <meta name="creator" content="joshmn">
9
+ <meta name="author" content="joshmn">
10
+ <meta name="contact" content="https://github.com/joshmn">
11
+ <meta name="twitter:creator" content="joshmn">
12
+ <meta name="twitter:site" content="josh.mn">
13
+ <meta name="og:site" content="josh.mn">
14
+ <meta name="description" content="Lookout">
15
+ <meta name="og:description" content="Lookout">
16
+ <link rel="icon" type="image/png" sizes="32x32" href="<%= image_path "lookout/favicon-32x32.png" %>">
17
+ <link rel="apple-touch-icon" sizes="180x180" href="<%= image_path "lookout/apple-touch-icon.png" %>">
18
+ <link rel="icon" type="image/png" sizes="16x16" href="<%= image_path "lookout/favicon-16x16.png" %>">
19
+ <link rel="mask-icon" href="<%= image_path "lookout/safari-pinned-tab.svg" %>" color="#5bbad5">
20
+ <meta name="msapplication-TileColor" content="#da532c">
21
+ <meta name="theme-color" content="#ffffff">
22
+ <link href="https://cdn.jsdelivr.net/npm/daisyui@3.5.0/dist/full.css" rel="stylesheet" type="text/css"/>
23
+ <script src="https://cdn.tailwindcss.com"></script>
24
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
25
+ <%= lookout_importmap_tags %>
26
+ <style>
27
+ /* Lookout Dark Theme - Maximum Contrast */
28
+ [data-theme="dark"] {
29
+ --b1: 215 25% 8%; /* Very dark blue-black background */
30
+ --b2: 215 20% 11%; /* Darker panels - subtle separation */
31
+ --b3: 215 18% 14%; /* Hover - slightly lighter */
32
+ --bc: 210 15% 92%; /* Bright text */
33
+ --n: 215 22% 10%; /* Neutral dark */
34
+ --nc: 210 15% 90%; /* Neutral content */
35
+ }
36
+
37
+ /* Force dark backgrounds */
38
+ html, body {
39
+ background-color: hsl(215, 25%, 8%) !important;
40
+ color: hsl(210, 15%, 92%) !important;
41
+ }
42
+
43
+ /* Smooth select dropdown styling */
44
+ :root {
45
+ --ss-bg-color: hsl(215, 20%, 11%);
46
+ --ss-font-color: hsl(210, 15%, 92%);
47
+ --ss-border-color: hsl(215, 18%, 16%);
48
+ }
49
+
50
+ .ss-content{
51
+ position:absolute;
52
+ display:none;
53
+ height:auto;
54
+ flex-direction:column;
55
+ width:auto;
56
+ max-height:var(--ss-content-height);
57
+ box-sizing:border-box;
58
+ border:solid 1px var(--ss-border-color);
59
+ background-color:var(--ss-bg-color);
60
+ transition:transform var(--ss-animation-timing),opacity var(--ss-animation-timing);
61
+ opacity:0;
62
+ transform:scaleY(0);
63
+ transform-origin:center top;
64
+ overflow:hidden;
65
+ z-index:10000;
66
+ }
67
+
68
+ .ss-content.ss-open-below{
69
+ opacity:1;
70
+ transform:scaleY(1);
71
+ transform-origin:center top;
72
+ border-bottom-left-radius:var(--ss-border-radius);
73
+ border-bottom-right-radius:var(--ss-border-radius);
74
+ bottom: -100%;
75
+ right: 41%;
76
+ display: flex;
77
+ }
78
+
79
+ .ss-open-below.ss-content.ss-relevant {
80
+ bottom: -100%;
81
+ right: 41%;
82
+ }
83
+
84
+ .pagy-nav.pagination {
85
+ isolation: isolate;
86
+ display: inline-flex;
87
+ margin-right: -1px;
88
+ /* border-radius: 0.375rem; */
89
+ box-shadow: 0 1px 2px 0 hsl(var(--n) / var(--tw-text-opacity, 1));
90
+ }
91
+
92
+ .page.next a, .page.prev a, .page.next.disabled, .page.prev.disabled, .page a, .page.gap {
93
+ position: relative;
94
+ display: inline-flex;
95
+ align-items: center;
96
+ border: 1px solid hsl(var(--n) / var(--tw-text-opacity, 1));
97
+ padding: 0.5rem 1rem;
98
+ font-size: 0.875rem;
99
+ font-weight: 500;
100
+ color: hsl(var(--bc) / var(--tw-text-opacity, 1));
101
+ }
102
+
103
+ .page.next a:hover, .page.prev a:hover, .page a:hover {
104
+ background-color: hsl(var(--b3) / var(--tw-text-opacity, 1));
105
+ color: hsl(var(--ac) / var(--tw-text-opacity, 1));
106
+ }
107
+
108
+ .page.next a:focus, .page.prev a:focus, .page.next.disabled:focus, .page.prev.disabled:focus, .page a:focus, .page.gap:focus {
109
+ z-index: 20;
110
+ }
111
+
112
+ .page.prev a, .page.next a {
113
+ border-radius: 0.375rem 0 0 0.375rem;
114
+ background-color: hsl(var(--b2) /1);
115
+ }
116
+
117
+
118
+ .page.next.disabled, .page.prev.disabled {
119
+ border-radius: 0.375rem 0 0 0.375rem;
120
+ background-color: hsl(var(--b2) / 0.5);
121
+ }
122
+
123
+ .page.active {
124
+ z-index: 10;
125
+ border-color: hsl(var(--a) / var(--tw-text-opacity, 1));
126
+ background-color: hsl(var(--b3) / var(--tw-text-opacity, 1));
127
+ color: hsl(var(--ac) / var(--tw-text-opacity, 1));
128
+ border-radius: 0.375rem;
129
+ padding: 0.5rem 1rem;
130
+ font-size: 0.875rem;
131
+ font-weight: 500;
132
+ }
133
+ </style>
134
+ </head>
135
+
136
+ <body data-controller='application'>
137
+ <%= yield %>
138
+ <div class="flex justify-center border-t-4 border-base-100 py-4">
139
+ <div class="flex justify-around space-x-4 my-4">
140
+ <h5>Powered by <a href='https://github.com/RubyOnVibes/lookout' target="_blank" class="underline">Lookout v<%= Lookout::VERSION %></a></h5>
141
+ </div>
142
+ </div>
143
+ </body>
144
+ </html>
@@ -0,0 +1,5 @@
1
+ <div class="mt-1">
2
+ <div role="status" class="animate-pulse opacity-30 py-1">
3
+ <div class="h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full w-32 sm:w-48"></div>
4
+ </div>
5
+ </div>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag "details" do %>
2
+ <p>Widget disabled.</p>
3
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag frame do %>
2
+ <p>Widget disabled.</p>
3
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render Lookout::TableComponent.new(items: @cities, category_name: 'City', unit_name: 'Visitors') %>
3
+ <span class="flex justify-center"><%= render_pagination %></span>
4
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :geography do %>
2
+ <%= render Lookout::TableComponent.new(items: @cities, category_name: 'City', unit_name: 'Visitors') %>
3
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render Lookout::TableComponent.new(items: @countries, category_name: 'Country', unit_name: 'Visitors') %>
3
+ <span class="flex justify-center"><%= render_pagination %></span>
4
+
5
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :geography do %>
2
+ <%= render Lookout::TableComponent.new(items: @countries, category_name: 'Country', unit_name: 'Visitors') %>
3
+ <% end %>
@@ -0,0 +1,26 @@
1
+ <%= turbo_frame_tag :geography do %>
2
+ <div class="w-full h-96 flex items-center justify-center bg-base-300 rounded-lg p-4">
3
+ <div class="text-center space-y-4 w-full">
4
+ <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 max-w-4xl mx-auto">
5
+ <% @countries.sort_by { |_, count| -count }.each do |country_code, count| %>
6
+ <div class="bg-base-200 rounded-lg p-3 hover:bg-base-100 transition-colors">
7
+ <div class="text-2xl mb-1"><%= country_flag(country_code) %></div>
8
+ <div class="font-semibold text-sm"><%= country_code %></div>
9
+ <div class="text-xs text-gray-400"><%= count %> visits</div>
10
+ </div>
11
+ <% end %>
12
+ </div>
13
+ </div>
14
+ </div>
15
+ <% end %>
16
+
17
+ <%# Helper method to get country flag emoji %>
18
+ <%
19
+ def country_flag(code)
20
+ return "🌍" if code.nil?
21
+ # Convert country code to flag emoji
22
+ code.upcase.chars.map { |c| (c.ord + 127397).chr(Encoding::UTF_8) }.join
23
+ rescue
24
+ "🌍"
25
+ end
26
+ %>
@@ -0,0 +1,106 @@
1
+ <%= turbo_frame_tag :geography do %>
2
+ <div style="width: 100%; height: 400px; position: relative;">
3
+ <canvas id="geo-map-canvas" style="width: 100%; height: 100%;"></canvas>
4
+ </div>
5
+
6
+ <script type="module">
7
+ import CountryMap from '<%= asset_path("lookout/helpers/countries.js") %>';
8
+
9
+ (function() {
10
+ const countryData = <%= raw @countries.to_json %>;
11
+
12
+ // Build numeric to ISO-2 mapping from our country data
13
+ const numericToIso = {};
14
+ Object.keys(CountryMap).forEach(key => {
15
+ numericToIso[CountryMap[key]['Numeric code']] = key;
16
+ });
17
+
18
+ // Load Chart.js first
19
+ const chartScript = document.createElement('script');
20
+ chartScript.src = 'https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js';
21
+ chartScript.onload = function() {
22
+ console.log('Chart.js loaded');
23
+
24
+ // Then load ChartGeo
25
+ const geoScript = document.createElement('script');
26
+ geoScript.src = 'https://cdn.jsdelivr.net/npm/chartjs-chart-geo@4.3.6/build/index.umd.min.js';
27
+ geoScript.onload = function() {
28
+ console.log('ChartGeo loaded');
29
+
30
+ // Now render the map
31
+ const canvas = document.getElementById('geo-map-canvas');
32
+ if (!canvas) {
33
+ console.error('Canvas not found');
34
+ return;
35
+ }
36
+
37
+ fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-50m.json')
38
+ .then(r => r.json())
39
+ .then(data => {
40
+ console.log('World atlas loaded');
41
+ console.log('Country data from server:', countryData);
42
+
43
+ const countries = ChartGeo.topojson.feature(data, data.objects.countries).features;
44
+
45
+ // Map country codes to values using the numeric ID
46
+ countries.forEach(country => {
47
+ const isoCode = numericToIso[country.id];
48
+ country.value = isoCode ? (countryData[isoCode] || 0) : 0;
49
+ if (country.value > 0) {
50
+ console.log(`Matched ${country.properties.name} (${country.id} -> ${isoCode}): ${country.value}`);
51
+ }
52
+ });
53
+
54
+ const dataWithValues = countries.filter(d => d.value > 0);
55
+ console.log(`Countries with data: ${dataWithValues.length}`, dataWithValues.map(d => ({name: d.properties.name, value: d.value})));
56
+
57
+ return countries;
58
+ })
59
+ .then(countries => {
60
+ new Chart(canvas.getContext('2d'), {
61
+ type: 'choropleth',
62
+ data: {
63
+ labels: countries.map(d => d.properties.name),
64
+ datasets: [{
65
+ label: 'Visits',
66
+ data: countries.map(d => ({feature: d, value: d.value}))
67
+ }]
68
+ },
69
+ options: {
70
+ showOutline: false,
71
+ showGraticule: false,
72
+ plugins: {
73
+ legend: { display: false }
74
+ },
75
+ scales: {
76
+ projection: {
77
+ axis: 'x',
78
+ projection: 'equalEarth'
79
+ },
80
+ color: {
81
+ axis: 'x',
82
+ quantize: 5,
83
+ legend: {
84
+ position: 'bottom-right',
85
+ align: 'right'
86
+ }
87
+ }
88
+ }
89
+ }
90
+ });
91
+ console.log('Map rendered successfully');
92
+ })
93
+ .catch(err => console.error('Map error:', err));
94
+ };
95
+ geoScript.onerror = function() {
96
+ console.error('Failed to load ChartGeo');
97
+ };
98
+ document.head.appendChild(geoScript);
99
+ };
100
+ chartScript.onerror = function() {
101
+ console.error('Failed to load Chart.js');
102
+ };
103
+ document.head.appendChild(chartScript);
104
+ })();
105
+ </script>
106
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_frame_tag :details do %>
2
+ <%= render Lookout::TableComponent.new(items: @regions, category_name: 'Region', unit_name: 'Visitors') %>
3
+ <span class="flex justify-center"><%= render_pagination %></span>
4
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :geography do %>
2
+ <%= render Lookout::TableComponent.new(items: @regions, category_name: 'Region', unit_name: 'Visitors') %>
3
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <select class="select text-accent select-sm w-full max-w-sm" data-controller="properties" data-action="change->properties#handleChange">
2
+ <option></option>
3
+ <% @options.each do |key, value| %>
4
+ <option value="<%= property_path(id: key) %>" <%= 'selected' if local_assigns[:selected] == value %>><%= value %></option>
5
+ <% end %>
6
+ </select>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag :goals do %>
2
+ <%= render 'lookout/properties/form' %>
3
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <%= turbo_frame_tag :goals do %>
2
+ <%= render 'lookout/properties/form', selected: Base64.strict_decode64(params[:id]) %>
3
+
4
+ <%= render Lookout::TableComponent.new(items: @properties,
5
+ table: ::Lookout::Tables::PropertiesTableComponent) %>
6
+ <% end %>
@@ -0,0 +1,9 @@
1
+ <%= turbo_frame_tag :realtime do %>
2
+ <div>
3
+ <a class="block ml-1 md:ml-2 mr-auto text-xs md:text-sm font-bold " title="" data-realtime-target="label">
4
+ <svg class="inline w-2 mr-1 md:mr-2 text-green-500 fill-current animate-pulse" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
5
+ <circle cx="8" cy="8" r="8"></circle>
6
+ </svg><%= @total %> <span class="hidden sm:inline-block">current visitors</span>
7
+ </a>
8
+ </div>
9
+ <% end %>
@@ -0,0 +1,80 @@
1
+ <%= form_with url: url_for(params.permit!.except(:q)), method: :get, data: { turbo_frame: "_top", controller: "filter-form", action: "reset->filter-form#handleReset" } do |form| %>
2
+ <% non_filter_ransack_params.each do |k,v| %>
3
+ <%= form.hidden_field k, value: v %>
4
+ <% end %>
5
+ <% Lookout.config.filters.each do |name, filters| %>
6
+ <%= render Lookout::Filter::ModalComponent.new(title: "Filter by #{name}", id: "#{filters.modal_name}") do |modal| %>
7
+ <% modal.with_modal_display do %>
8
+ <% filters.each do |filter| %>
9
+ <%= render Lookout::Filter::SelectComponent.new(label: filter.label, column: filter.column, url: public_send(filter.url), predicates: filter.predicates, multiple: filter.multiple, form: form) %>
10
+ <% end %>
11
+ <% end %>
12
+ <% end %>
13
+ <% end %>
14
+
15
+ <%= render Lookout::Filter::ModalComponent.new(title: "Filter by Custom Property", id: "customPropertyFilterModal") do |modal| %>
16
+ <% modal.with_modal_display do %>
17
+ <div data-controller="property-filter">
18
+ <fieldset class="flex space-x-4 items-end">
19
+ <% if current_property_filter %>
20
+ <%= render ::Lookout::ComboboxComponent.new(name: "", multiple: false, column: "", url: filters_properties_names_path, value: [current_property_filter[:key].delete_prefix("properties.")], select_html: { "data-property-filter-target" => 'name', id: "property-name" }) %>
21
+ <% else %>
22
+ <%= render ::Lookout::ComboboxComponent.new(name: "", multiple: false, column: "", url: filters_properties_names_path, select_html: { "data-property-filter-target" => 'name', id: "property-name" } ) %>
23
+ <% end %>
24
+
25
+ <div class="flex flex-col w-[20%]">
26
+ <select class='select select-primary bg-gray-900 ring-0 focus-within:ring-0 focus-within:ring-0 focus:ring-0 focus:outline-none inline-flex justify-between items-center w-full rounded-md border px-4 py-2 text-sm'>
27
+ <option value="in">in</option>
28
+ </select>
29
+ </div>
30
+
31
+ <% if current_property_filter %>
32
+ <%= render ::Lookout::ComboboxComponent.new(name: "q[#{current_property_filter[:key]}_in]", multiple: false, column: "", url: filters_properties_values_path, value: [current_property_filter[:value]], select_html: { "data-property-filter-target" => 'value', id: "property-value" } ) %>
33
+ <% else %>
34
+ <%= render ::Lookout::ComboboxComponent.new(name: "", multiple: false, column: "", url: filters_properties_values_path, select_html: { "data-property-filter-target" => 'value', id: "property-value" }, disabled: true) %>
35
+ <% end %>
36
+
37
+ </fieldset>
38
+
39
+ </div>
40
+ <% end %>
41
+ <% end %>
42
+
43
+ <%= render Lookout::Filter::ModalComponent.new(title: "Custom Range", id: "customRangeModal") do |modal| %>
44
+ <% modal.with_modal_display do %>
45
+ <div class="flex gap-2 w-full">
46
+ <div class="form-control w-full max-w-xs">
47
+ <label class="label" for="start_date">
48
+ <span class="label-text">Start Date</span>
49
+ </label>
50
+ <input type="datetime-local" id="start_date" name="start_date" class="input input-bordered w-full" value="<%= params[:start_date] %>" />
51
+ </div>
52
+ <div class="form-control w-full max-w-xs">
53
+ <label class="label" for="end_date">
54
+ <span class="label-text">End Date</span>
55
+ </label>
56
+ <input type="datetime-local" id="end_date" name="end_date" class="input input-bordered w-full" value="<%= params[:end_date] %>" />
57
+ </div>
58
+ </div>
59
+ <% end %>
60
+ <% end %>
61
+
62
+ <%= render Lookout::Filter::ModalComponent.new(title: "Custom Comparison", id: "customComparisonModal") do |modal| %>
63
+ <% modal.with_modal_display do %>
64
+ <div class="flex gap-2 w-full">
65
+ <div class="form-control w-full max-w-xs">
66
+ <label class="label" for="start_date">
67
+ <span class="label-text">Start Date</span>
68
+ </label>
69
+ <input type="datetime-local" id="start_date" name="compare_to_start_date" class="input input-bordered w-full" value="<%= params[:compare_to_start_date] %>" />
70
+ </div>
71
+ <div class="form-control w-full max-w-xs">
72
+ <label class="label" for="end_date">
73
+ <span class="label-text">End Date</span>
74
+ </label>
75
+ <input type="datetime-local" id="end_date" name="compare_to_end_date" class="input input-bordered w-full" value="<%= params[:compare_to_end_date] %>" />
76
+ </div>
77
+ </div>
78
+ <% end %>
79
+ <% end %>
80
+ <% end %>