testimonials 0.7.0 → 0.7.2

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/app/controllers/testimonials/widgets_controller.rb +8 -4
  4. data/app/views/layouts/testimonials/application.html.erb +4 -87
  5. data/app/views/layouts/testimonials/collection.html.erb +1 -0
  6. data/config/locales/testimonials.ar.yml +1 -1
  7. data/config/locales/testimonials.bg.yml +1 -1
  8. data/config/locales/testimonials.bn.yml +1 -1
  9. data/config/locales/testimonials.de.yml +1 -1
  10. data/config/locales/testimonials.el.yml +1 -1
  11. data/config/locales/testimonials.es.yml +1 -1
  12. data/config/locales/testimonials.fr.yml +1 -1
  13. data/config/locales/testimonials.hi.yml +1 -1
  14. data/config/locales/testimonials.hr.yml +1 -1
  15. data/config/locales/testimonials.id.yml +1 -1
  16. data/config/locales/testimonials.it.yml +1 -1
  17. data/config/locales/testimonials.ja.yml +1 -1
  18. data/config/locales/testimonials.ko.yml +1 -1
  19. data/config/locales/testimonials.lb.yml +1 -1
  20. data/config/locales/testimonials.pl.yml +1 -1
  21. data/config/locales/testimonials.pt.yml +1 -1
  22. data/config/locales/testimonials.ro.yml +1 -1
  23. data/config/locales/testimonials.ru.yml +1 -1
  24. data/config/locales/testimonials.th.yml +1 -1
  25. data/config/locales/testimonials.tr.yml +1 -1
  26. data/config/locales/testimonials.uk.yml +1 -1
  27. data/config/locales/testimonials.ur.yml +1 -1
  28. data/config/locales/testimonials.vi.yml +1 -1
  29. data/config/locales/testimonials.zh-CN.yml +1 -1
  30. data/config/routes.rb +1 -0
  31. data/lib/generators/testimonials/install/install_generator.rb +1 -1
  32. data/lib/generators/testimonials/install/templates/initializer.rb +2 -1
  33. data/lib/testimonials/dashboard.css +84 -0
  34. data/lib/testimonials/engine.rb +9 -0
  35. data/lib/testimonials/version.rb +1 -1
  36. data/lib/testimonials/widget.rb +13 -5
  37. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 691265663845bee9822f3ec8cb013ca50b36f5e17a763775aab8b6e3c9e74517
4
- data.tar.gz: 983d03927120255fd34eb9d37221ab7c3476c4afc58a31de151f9a44c423c3c7
3
+ metadata.gz: 9348e14150cc526771fea79c605b3e358104fa849575551a74ea4f022c2354a4
4
+ data.tar.gz: a9a79e6cb2a89c4b94c29ddcd0101f75a851a896bc32fa8b28da9bd0063eaca0
5
5
  SHA512:
6
- metadata.gz: 0e8d6cc2b5f8cad129912bdb4b0fb73a5348c63e7e7879649bdfc43d07e443abc440d2863ec2aa03e960c6a2f18aba96b66bc2664f2fe52858a68dba0106a966
7
- data.tar.gz: a4a1f73006da86ff677bf341e64d2f847fa95386bafb893b20680463ea431953650566a8eeced1812808b0328212fbde5873f78d3ce9a4911081fd948cf34a1a
6
+ metadata.gz: 8791c9a58e51b6492c826d9680be4a9a3509a0e08b92e920b76d73e9d7d0a318884373e6d14d5bbc7dac86260cb12a38068e81cd988bf424535679e0fe8a180f
7
+ data.tar.gz: 433e0412f79c440c1f79cac4858d72e70f22eae7558789e1ffc98282c5bab457b2fbb0b2d90f73f566e7ba36918b9048308e2e726eb389874997a8e27f26af7f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.2
4
+
5
+ - Added `mount_testimonials at: "/testimonials"` as the install-time route
6
+ helper. It keeps `config.mount_path` synchronized with the mounted engine
7
+ path while preserving manual `mount Testimonials::Engine` compatibility.
8
+ - Extracted the dashboard stylesheet into a same-origin, fingerprinted
9
+ `/dashboard.css` endpoint and added CSP meta tags to the engine layouts.
10
+ The public widget remains pipeline-free and controller-served.
11
+
12
+ ## 0.7.1
13
+
14
+ - Renamed the default admin dashboard title to `Testimonials` across shipped
15
+ locales while keeping it overridable through `testimonials.dashboard.title`.
16
+
3
17
  ## 0.7.0
4
18
 
5
19
  - New `config.storage_service`: store uploads (video, its poster frame, guest
@@ -7,7 +7,7 @@ module Testimonials
7
7
  # Drive swaps the <body> and re-runs body scripts under the *original*
8
8
  # page's CSP header, where a freshly minted inline nonce would be refused.
9
9
  class WidgetsController < ApplicationController
10
- # The script is static and carries no user data; without this, Rails'
10
+ # These assets are static and carry no user data; without this, Rails'
11
11
  # cross-origin JavaScript guard refuses to serve it to a plain
12
12
  # <script src> request.
13
13
  skip_forgery_protection
@@ -21,16 +21,20 @@ module Testimonials
21
21
  end
22
22
 
23
23
  def dashboard
24
- serve(Widget.dashboard_javascript, Widget.dashboard_fingerprint)
24
+ serve(Widget.dashboard_javascript, Widget.dashboard_fingerprint, 'text/javascript')
25
+ end
26
+
27
+ def dashboard_stylesheet
28
+ serve(Widget.dashboard_stylesheet, Widget.dashboard_stylesheet_fingerprint, 'text/css')
25
29
  end
26
30
 
27
31
  private
28
32
 
29
- def serve(source, fingerprint)
33
+ def serve(source, fingerprint, content_type = 'text/javascript')
30
34
  expires_in 1.year, public: true if params[:v] == fingerprint
31
35
  return unless stale?(etag: [Testimonials::VERSION, fingerprint])
32
36
 
33
- render plain: source, content_type: 'text/javascript'
37
+ render plain: source, content_type: content_type
34
38
  end
35
39
  end
36
40
  end
@@ -4,96 +4,13 @@
4
4
  <title><%= t('testimonials.dashboard.title', default: 'Testimonials') %></title>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
7
8
  <%# Same-origin script instead of inline handlers, so delete confirms and
8
9
  the auto-submitting filter work under strict script-src CSPs. %>
10
+ <%= stylesheet_link_tag "#{dashboard_stylesheet_path}?v=#{Testimonials::Widget.dashboard_stylesheet_fingerprint}",
11
+ 'data-turbo-track': 'reload' %>
9
12
  <%= javascript_include_tag "#{dashboard_script_path}?v=#{Testimonials::Widget.dashboard_fingerprint}",
10
- defer: true, nonce: true %>
11
- <style>
12
- :root {
13
- color-scheme: light dark;
14
- --bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
15
- --border: #e5e7eb; --accent: #2563eb; --accent-text: #fff;
16
- --pending: #d97706; --approved: #16a34a; --archived: #6b7280;
17
- --star: #f59e0b; --danger: #dc2626;
18
- --promoter: #16a34a; --passive: #d97706; --detractor: #dc2626;
19
- }
20
- @media (prefers-color-scheme: dark) {
21
- :root {
22
- --bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
23
- --border: #2a313a; --accent: #3b82f6;
24
- }
25
- }
26
- * { box-sizing: border-box; }
27
- body { margin: 0; background: var(--bg); color: var(--text);
28
- font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
29
- a { color: var(--accent); text-decoration: none; }
30
- a:hover { text-decoration: underline; }
31
- .container { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
32
- h1 { font-size: 22px; margin: 0 0 16px; }
33
- h1.title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
34
- .nav { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
35
- .nav a { padding: 6px 16px; border: 1px solid var(--border); border-radius: 999px;
36
- background: var(--surface); color: var(--muted); font-weight: 600; }
37
- .nav a:hover { text-decoration: none; color: var(--text); }
38
- .nav a.active { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
39
- .nav a.active:hover { color: var(--accent-text); }
40
- .share-link { margin-left: auto; font-size: 13px; font-weight: 400; }
41
- .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
42
- .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
43
- .tabs a.active { color: var(--text); font-weight: 600; border: 1px solid var(--border);
44
- border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px; }
45
- .tabs a:hover { text-decoration: none; color: var(--text); }
46
- .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
47
- background: var(--border); font-size: 12px; text-align: center; }
48
- .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
49
- .filters select, .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--border);
50
- border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
51
- .filters input[type=search] { flex: 1; max-width: 320px; }
52
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
53
- .card.pad { padding: 16px; overflow: visible; }
54
- table { width: 100%; border-collapse: collapse; }
55
- th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
56
- th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
57
- border-bottom: 1px solid var(--border); }
58
- tr + tr td { border-top: 1px solid var(--border); }
59
- .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
60
- color: #fff; white-space: nowrap; }
61
- .badge.status-pending { background: var(--pending); }
62
- .badge.status-approved { background: var(--approved); }
63
- .badge.status-archived { background: var(--archived); }
64
- .badge.nps-promoter { background: var(--promoter); }
65
- .badge.nps-passive { background: var(--passive); }
66
- .badge.nps-detractor { background: var(--detractor); }
67
- .stars { color: var(--star); letter-spacing: 1px; white-space: nowrap; }
68
- .stars .off { color: var(--border); }
69
- .muted { color: var(--muted); font-size: 13px; }
70
- .pager { margin-top: 16px; display: flex; gap: 16px; }
71
- .actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
72
- .actions form { display: inline; }
73
- td.row-actions { white-space: nowrap; text-align: right; }
74
- td.row-actions form { display: inline-block; }
75
- td.row-actions form + form { margin-left: 6px; }
76
- button, .button { padding: 8px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
77
- background: var(--surface); color: var(--text); font: inherit; }
78
- button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
79
- button.danger { color: var(--danger); }
80
- button.small { padding: 3px 10px; font-size: 13px; }
81
- .message { white-space: pre-wrap; overflow-wrap: anywhere; }
82
- dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
83
- dt { color: var(--muted); }
84
- dd { margin: 0; overflow-wrap: anywhere; }
85
- video.playback { width: 100%; max-width: 480px; border-radius: 10px; background: #000; }
86
- img.avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
87
- .empty { padding: 48px 16px; text-align: center; color: var(--muted); }
88
- .breadcrumb { margin-bottom: 12px; font-size: 13px; }
89
- .stat-row { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
90
- .stat { flex: 1; min-width: 140px; background: var(--surface); border: 1px solid var(--border);
91
- border-radius: 12px; padding: 14px 16px; }
92
- .stat b { display: block; font-size: 26px; }
93
- .stat span { color: var(--muted); font-size: 13px; }
94
- .best-line-form textarea { width: 100%; padding: 8px; border: 1px solid var(--border); border-radius: 8px;
95
- background: var(--surface); color: var(--text); font: inherit; resize: vertical; }
96
- </style>
13
+ 'data-turbo-track': 'reload', defer: true, nonce: true %>
97
14
  </head>
98
15
  <body>
99
16
  <div class="container">
@@ -4,6 +4,7 @@
4
4
  <title><%= t('testimonials.page.title', app: Testimonials.app_name, default: "Share your experience with #{Testimonials.app_name}") %></title>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
7
8
  <style>
8
9
  :root { color-scheme: light dark; }
9
10
  body { margin: 0; background: #f6f7f9; color: #1c2024;
@@ -64,7 +64,7 @@ ar:
64
64
  approved: "معتمد"
65
65
  archived: "مؤرشف"
66
66
  dashboard:
67
- title: "الشهادات"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "كل الأنواع"
70
70
  search: "بحث"
@@ -64,7 +64,7 @@ bg:
64
64
  approved: "Одобрени"
65
65
  archived: "Архивирани"
66
66
  dashboard:
67
- title: "Отзиви"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Всички видове"
70
70
  search: "Търсене"
@@ -64,7 +64,7 @@ bn:
64
64
  approved: "অনুমোদিত"
65
65
  archived: "আর্কাইভ করা"
66
66
  dashboard:
67
- title: "প্রশংসাপত্র"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "সব ধরন"
70
70
  search: "খুঁজুন"
@@ -64,7 +64,7 @@ de:
64
64
  approved: "Freigegeben"
65
65
  archived: "Archiviert"
66
66
  dashboard:
67
- title: "Erfahrungsberichte"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Alle Arten"
70
70
  search: "Suchen"
@@ -64,7 +64,7 @@ el:
64
64
  approved: "Εγκεκριμένο"
65
65
  archived: "Αρχειοθετημένο"
66
66
  dashboard:
67
- title: "Μαρτυρίες"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Όλα τα είδη"
70
70
  search: "Αναζήτηση"
@@ -64,7 +64,7 @@ es:
64
64
  approved: "Aprobado"
65
65
  archived: "Archivado"
66
66
  dashboard:
67
- title: "Testimonios"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Todos los tipos"
70
70
  search: "Buscar"
@@ -64,7 +64,7 @@ fr:
64
64
  approved: "Approuvé"
65
65
  archived: "Archivé"
66
66
  dashboard:
67
- title: "Témoignages"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Tous les types"
70
70
  search: "Rechercher"
@@ -64,7 +64,7 @@ hi:
64
64
  approved: "स्वीकृत"
65
65
  archived: "संग्रहीत"
66
66
  dashboard:
67
- title: "प्रशंसापत्र"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "सभी प्रकार"
70
70
  search: "खोजें"
@@ -64,7 +64,7 @@ hr:
64
64
  approved: "Odobreno"
65
65
  archived: "Arhivirano"
66
66
  dashboard:
67
- title: "Osvrti"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Sve vrste"
70
70
  search: "Traži"
@@ -64,7 +64,7 @@ id:
64
64
  approved: "Disetujui"
65
65
  archived: "Diarsipkan"
66
66
  dashboard:
67
- title: "Testimoni"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Semua jenis"
70
70
  search: "Cari"
@@ -64,7 +64,7 @@ it:
64
64
  approved: "Approvato"
65
65
  archived: "Archiviato"
66
66
  dashboard:
67
- title: "Testimonianze"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Tutti i tipi"
70
70
  search: "Cerca"
@@ -64,7 +64,7 @@ ja:
64
64
  approved: "承認済み"
65
65
  archived: "アーカイブ済み"
66
66
  dashboard:
67
- title: "お客様の声"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "すべての種類"
70
70
  search: "検索"
@@ -64,7 +64,7 @@ ko:
64
64
  approved: "승인됨"
65
65
  archived: "보관됨"
66
66
  dashboard:
67
- title: "고객 후기"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "모든 유형"
70
70
  search: "검색"
@@ -64,7 +64,7 @@ lb:
64
64
  approved: "Guttgeheescht"
65
65
  archived: "Archivéiert"
66
66
  dashboard:
67
- title: "Temoignagen"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "All Zorten"
70
70
  search: "Sichen"
@@ -64,7 +64,7 @@ pl:
64
64
  approved: "Zatwierdzone"
65
65
  archived: "Zarchiwizowane"
66
66
  dashboard:
67
- title: "Opinie"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Wszystkie rodzaje"
70
70
  search: "Szukaj"
@@ -64,7 +64,7 @@ pt:
64
64
  approved: "Aprovado"
65
65
  archived: "Arquivado"
66
66
  dashboard:
67
- title: "Depoimentos"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Todos os tipos"
70
70
  search: "Buscar"
@@ -64,7 +64,7 @@ ro:
64
64
  approved: "Aprobat"
65
65
  archived: "Arhivat"
66
66
  dashboard:
67
- title: "Testimoniale"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Toate tipurile"
70
70
  search: "Căutare"
@@ -64,7 +64,7 @@ ru:
64
64
  approved: "Одобрено"
65
65
  archived: "В архиве"
66
66
  dashboard:
67
- title: "Отзывы"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Все типы"
70
70
  search: "Поиск"
@@ -64,7 +64,7 @@ th:
64
64
  approved: "อนุมัติแล้ว"
65
65
  archived: "เก็บถาวรแล้ว"
66
66
  dashboard:
67
- title: "คำรับรอง"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "ทุกประเภท"
70
70
  search: "ค้นหา"
@@ -64,7 +64,7 @@ tr:
64
64
  approved: "Onaylandı"
65
65
  archived: "Arşivlendi"
66
66
  dashboard:
67
- title: "Görüşler"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Tüm türler"
70
70
  search: "Ara"
@@ -64,7 +64,7 @@ uk:
64
64
  approved: "Схвалено"
65
65
  archived: "В архіві"
66
66
  dashboard:
67
- title: "Відгуки"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Усі типи"
70
70
  search: "Пошук"
@@ -64,7 +64,7 @@ ur:
64
64
  approved: "منظور شدہ"
65
65
  archived: "محفوظ شدہ"
66
66
  dashboard:
67
- title: "تعریفی کلمات"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "تمام اقسام"
70
70
  search: "تلاش"
@@ -64,7 +64,7 @@ vi:
64
64
  approved: "Đã duyệt"
65
65
  archived: "Đã lưu trữ"
66
66
  dashboard:
67
- title: "Cảm nhận khách hàng"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "Tất cả các loại"
70
70
  search: "Tìm kiếm"
@@ -64,7 +64,7 @@ zh-CN:
64
64
  approved: "已批准"
65
65
  archived: "已归档"
66
66
  dashboard:
67
- title: "用户评价"
67
+ title: "Testimonials"
68
68
  nps: "NPS"
69
69
  all_kinds: "所有类型"
70
70
  search: "搜索"
data/config/routes.rb CHANGED
@@ -5,6 +5,7 @@ Testimonials::Engine.routes.draw do
5
5
  # the bottom own "/:id".
6
6
  get 'widget.js', to: 'widgets#show', as: :widget
7
7
  get 'dashboard.js', to: 'widgets#dashboard', as: :dashboard_script
8
+ get 'dashboard.css', to: 'widgets#dashboard_stylesheet', as: :dashboard_stylesheet
8
9
  get 'new', to: 'collection#show', as: :collection
9
10
  resources :events, only: :create
10
11
  resource :nps, only: :create, controller: 'nps'
@@ -22,7 +22,7 @@ module Testimonials
22
22
  end
23
23
 
24
24
  def mount_engine
25
- route %(mount Testimonials::Engine => "/testimonials")
25
+ route %(mount_testimonials at: "/testimonials")
26
26
  end
27
27
 
28
28
  def post_install
@@ -92,6 +92,7 @@ Testimonials.configure do |config|
92
92
  # Per-IP throttle for public endpoints (Rails 7.2+; ignored on 7.1).
93
93
  # config.rate_limit = { to: 5, within: 1.minute }
94
94
 
95
- # Keep in sync with the `mount` in config/routes.rb.
95
+ # Default mount path used by `mount_testimonials`.
96
+ # Override only if you mount the engine manually.
96
97
  # config.mount_path = "/testimonials"
97
98
  end
@@ -0,0 +1,84 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ --bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
4
+ --border: #e5e7eb; --accent: #2563eb; --accent-text: #fff;
5
+ --pending: #d97706; --approved: #16a34a; --archived: #6b7280;
6
+ --star: #f59e0b; --danger: #dc2626;
7
+ --promoter: #16a34a; --passive: #d97706; --detractor: #dc2626;
8
+ }
9
+ @media (prefers-color-scheme: dark) {
10
+ :root {
11
+ --bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
12
+ --border: #2a313a; --accent: #3b82f6;
13
+ }
14
+ }
15
+ * { box-sizing: border-box; }
16
+ body { margin: 0; background: var(--bg); color: var(--text);
17
+ font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
18
+ a { color: var(--accent); text-decoration: none; }
19
+ a:hover { text-decoration: underline; }
20
+ .container { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
21
+ h1 { font-size: 22px; margin: 0 0 16px; }
22
+ h1.title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
23
+ .nav { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
24
+ .nav a { padding: 6px 16px; border: 1px solid var(--border); border-radius: 999px;
25
+ background: var(--surface); color: var(--muted); font-weight: 600; }
26
+ .nav a:hover { text-decoration: none; color: var(--text); }
27
+ .nav a.active { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
28
+ .nav a.active:hover { color: var(--accent-text); }
29
+ .share-link { margin-left: auto; font-size: 13px; font-weight: 400; }
30
+ .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
31
+ .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
32
+ .tabs a.active { color: var(--text); font-weight: 600; border: 1px solid var(--border);
33
+ border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px; }
34
+ .tabs a:hover { text-decoration: none; color: var(--text); }
35
+ .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
36
+ background: var(--border); font-size: 12px; text-align: center; }
37
+ .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
38
+ .filters select, .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--border);
39
+ border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
40
+ .filters input[type=search] { flex: 1; max-width: 320px; }
41
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
42
+ .card.pad { padding: 16px; overflow: visible; }
43
+ table { width: 100%; border-collapse: collapse; }
44
+ th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
45
+ th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
46
+ border-bottom: 1px solid var(--border); }
47
+ tr + tr td { border-top: 1px solid var(--border); }
48
+ .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
49
+ color: #fff; white-space: nowrap; }
50
+ .badge.status-pending { background: var(--pending); }
51
+ .badge.status-approved { background: var(--approved); }
52
+ .badge.status-archived { background: var(--archived); }
53
+ .badge.nps-promoter { background: var(--promoter); }
54
+ .badge.nps-passive { background: var(--passive); }
55
+ .badge.nps-detractor { background: var(--detractor); }
56
+ .stars { color: var(--star); letter-spacing: 1px; white-space: nowrap; }
57
+ .stars .off { color: var(--border); }
58
+ .muted { color: var(--muted); font-size: 13px; }
59
+ .pager { margin-top: 16px; display: flex; gap: 16px; }
60
+ .actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
61
+ .actions form { display: inline; }
62
+ td.row-actions { white-space: nowrap; text-align: right; }
63
+ td.row-actions form { display: inline-block; }
64
+ td.row-actions form + form { margin-left: 6px; }
65
+ button, .button { padding: 8px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
66
+ background: var(--surface); color: var(--text); font: inherit; }
67
+ button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
68
+ button.danger { color: var(--danger); }
69
+ button.small { padding: 3px 10px; font-size: 13px; }
70
+ .message { white-space: pre-wrap; overflow-wrap: anywhere; }
71
+ dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
72
+ dt { color: var(--muted); }
73
+ dd { margin: 0; overflow-wrap: anywhere; }
74
+ video.playback { width: 100%; max-width: 480px; border-radius: 10px; background: #000; }
75
+ img.avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
76
+ .empty { padding: 48px 16px; text-align: center; color: var(--muted); }
77
+ .breadcrumb { margin-bottom: 12px; font-size: 13px; }
78
+ .stat-row { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
79
+ .stat { flex: 1; min-width: 140px; background: var(--surface); border: 1px solid var(--border);
80
+ border-radius: 12px; padding: 14px 16px; }
81
+ .stat b { display: block; font-size: 26px; }
82
+ .stat span { color: var(--muted); font-size: 13px; }
83
+ .best-line-form textarea { width: 100%; padding: 8px; border: 1px solid var(--border); border-radius: 8px;
84
+ background: var(--surface); color: var(--text); font: inherit; resize: vertical; }
@@ -20,5 +20,14 @@ module Testimonials
20
20
  extend Testimonials::HasTestimonials
21
21
  end
22
22
  end
23
+
24
+ initializer 'testimonials.routing' do
25
+ ActionDispatch::Routing::Mapper.include(Module.new do
26
+ def mount_testimonials(at: Testimonials.config.mount_path, **options)
27
+ Testimonials.config.mount_path = at
28
+ mount Testimonials::Engine, at:, **options
29
+ end
30
+ end)
31
+ end
23
32
  end
24
33
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Testimonials
4
- VERSION = '0.7.0'
4
+ VERSION = '0.7.2'
5
5
  end
@@ -5,14 +5,14 @@ require 'digest'
5
5
 
6
6
  module Testimonials
7
7
  # Serves the self-contained browser widget. The JavaScript is plain ES (no
8
- # framework, no build step) and styles itself inline, so it drops into any
9
- # Rails app regardless of its CSS or JS setup. It is inlined into the page
10
- # rather than served as a separate asset to avoid any dependency on the
11
- # host's asset pipeline and it lives under lib/ (not app/assets/) so a
12
- # host that *does* run a pipeline never ingests it either.
8
+ # framework, no build step) and injects its own widget styles, so it drops
9
+ # into any Rails app regardless of its CSS or JS setup. The code is served by
10
+ # the engine instead of the host's asset pipeline, and it lives under lib/ so
11
+ # a host that does run a pipeline never ingests it either.
13
12
  module Widget
14
13
  SOURCE = File.expand_path('widget.js', __dir__)
15
14
  DASHBOARD_SOURCE = File.expand_path('dashboard.js', __dir__)
15
+ DASHBOARD_STYLESHEET_SOURCE = File.expand_path('dashboard.css', __dir__)
16
16
 
17
17
  # Right-to-left scripts, so the form renders mirrored for those locales.
18
18
  # Matched on the language subtag, so region variants ("ar-EG") count too.
@@ -27,6 +27,10 @@ module Testimonials
27
27
  @dashboard_javascript ||= File.read(DASHBOARD_SOURCE)
28
28
  end
29
29
 
30
+ def dashboard_stylesheet
31
+ @dashboard_stylesheet ||= File.read(DASHBOARD_STYLESHEET_SOURCE)
32
+ end
33
+
30
34
  # Content fingerprints for cache-busting script URLs: a changed file is
31
35
  # a changed URL, so no browser can ever run stale widget code — Safari
32
36
  # has been caught ignoring must-revalidate on same-URL scripts.
@@ -38,6 +42,10 @@ module Testimonials
38
42
  @dashboard_fingerprint ||= Digest::MD5.hexdigest(dashboard_javascript)
39
43
  end
40
44
 
45
+ def dashboard_stylesheet_fingerprint
46
+ @dashboard_stylesheet_fingerprint ||= Digest::MD5.hexdigest(dashboard_stylesheet)
47
+ end
48
+
41
49
  # The two <script> tags the helper renders.
42
50
  #
43
51
  # The config rides in a `type="application/json"` block: it is *data*,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testimonials
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -98,6 +98,7 @@ files:
98
98
  - lib/generators/testimonials/tenant/tenant_generator.rb
99
99
  - lib/testimonials.rb
100
100
  - lib/testimonials/configuration.rb
101
+ - lib/testimonials/dashboard.css
101
102
  - lib/testimonials/dashboard.js
102
103
  - lib/testimonials/engine.rb
103
104
  - lib/testimonials/has_testimonials.rb