@escalated-dev/escalated 0.7.0 → 0.7.1

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 (179) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +31 -0
  3. package/package.json +12 -6
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -55
  6. package/src/components/ActivityTimeline.vue +83 -83
  7. package/src/components/AdminDashboard.stories.js +1423 -1095
  8. package/src/components/AgentLoadIndicator.stories.js +37 -37
  9. package/src/components/AgentLoadIndicator.vue +49 -49
  10. package/src/components/AgentPerformanceCard.vue +44 -44
  11. package/src/components/ArticleEditor.vue +82 -82
  12. package/src/components/AssigneeSelect.vue +37 -37
  13. package/src/components/AttachmentList.vue +13 -1
  14. package/src/components/AuditLogEntry.vue +95 -95
  15. package/src/components/BulkActionBar.vue +166 -166
  16. package/src/components/CategoryTree.vue +89 -89
  17. package/src/components/ChartWidget.vue +46 -46
  18. package/src/components/ChatActionBar.vue +226 -0
  19. package/src/components/ChatBubble.vue +162 -0
  20. package/src/components/ChatComposer.vue +308 -0
  21. package/src/components/ChatQueue.vue +222 -0
  22. package/src/components/ChatThread.vue +197 -0
  23. package/src/components/CollisionWarning.stories.js +30 -30
  24. package/src/components/CollisionWarning.vue +68 -68
  25. package/src/components/ConditionalFieldRules.vue +135 -135
  26. package/src/components/ContextPanel.vue +66 -66
  27. package/src/components/ContextPanelSection.vue +65 -65
  28. package/src/components/CustomFieldRenderer.vue +159 -159
  29. package/src/components/EscalatedLayout.vue +29 -6
  30. package/src/components/FileDropzone.vue +57 -57
  31. package/src/components/FollowButton.stories.js +30 -30
  32. package/src/components/FollowButton.vue +88 -88
  33. package/src/components/KeyboardShortcutHelp.vue +213 -213
  34. package/src/components/KnowledgePanel.vue +122 -122
  35. package/src/components/KpiCard.stories.js +32 -32
  36. package/src/components/KpiCard.vue +88 -88
  37. package/src/components/MacroDropdown.vue +150 -150
  38. package/src/components/MentionDropdown.vue +154 -0
  39. package/src/components/MetricComparison.vue +89 -0
  40. package/src/components/PercentileChart.vue +67 -0
  41. package/src/components/PeriodSelector.vue +98 -0
  42. package/src/components/PermissionMatrix.vue +107 -107
  43. package/src/components/PinnedNotes.vue +129 -129
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +46 -2
  46. package/src/components/PriorityBadge.stories.js +33 -33
  47. package/src/components/PriorityBadge.vue +46 -46
  48. package/src/components/ReplyComposer.vue +81 -18
  49. package/src/components/ReplyThread.vue +40 -2
  50. package/src/components/ReportChart.vue +395 -0
  51. package/src/components/ReportExportButton.vue +108 -0
  52. package/src/components/SatisfactionRating.vue +180 -180
  53. package/src/components/SaveViewDialog.vue +172 -0
  54. package/src/components/SavedViewSidebar.vue +208 -0
  55. package/src/components/ScheduleEditor.vue +104 -104
  56. package/src/components/SideConversation.vue +165 -165
  57. package/src/components/SideConversationList.vue +147 -147
  58. package/src/components/SkillTagManager.vue +79 -79
  59. package/src/components/SlaComplianceChart.vue +46 -53
  60. package/src/components/SlaTimer.stories.js +35 -35
  61. package/src/components/SlaTimer.vue +62 -62
  62. package/src/components/SnoozeButton.vue +247 -0
  63. package/src/components/StatsCard.stories.js +24 -24
  64. package/src/components/StatsCard.vue +58 -58
  65. package/src/components/StatusBadge.stories.js +53 -53
  66. package/src/components/StatusBadge.vue +78 -78
  67. package/src/components/TagSelect.vue +84 -84
  68. package/src/components/TicketFilters.vue +270 -225
  69. package/src/components/TicketLinkPanel.vue +193 -193
  70. package/src/components/TicketList.vue +655 -594
  71. package/src/components/TicketMergeDialog.vue +157 -157
  72. package/src/components/TicketSplitDialog.vue +98 -0
  73. package/src/components/TotpInput.stories.js +9 -9
  74. package/src/components/TotpInput.vue +81 -81
  75. package/src/components/TwoFactorSetup.vue +113 -113
  76. package/src/components/VariableTokenInput.vue +111 -0
  77. package/src/components/WorkflowActionConfig.vue +287 -0
  78. package/src/components/WorkflowActionList.vue +350 -0
  79. package/src/components/WorkflowConditionBuilder.vue +265 -0
  80. package/src/components/WorkflowTriggerSelector.vue +125 -0
  81. package/src/composables/useChat.js +187 -0
  82. package/src/composables/useI18n.js +80 -80
  83. package/src/composables/useKeyboardShortcuts.js +46 -46
  84. package/src/composables/useMentions.js +134 -0
  85. package/src/composables/usePluginExtensions.js +159 -159
  86. package/src/composables/usePluginHooks.js +65 -65
  87. package/src/composables/useRealtime.js +182 -0
  88. package/src/defineEscalatedPlugin.js +76 -76
  89. package/src/index.js +84 -72
  90. package/src/locales/ar.json +727 -0
  91. package/src/locales/de.json +678 -430
  92. package/src/locales/en.json +717 -455
  93. package/src/locales/es.json +679 -430
  94. package/src/locales/fr.json +680 -431
  95. package/src/locales/index.js +12 -2
  96. package/src/locales/it.json +727 -0
  97. package/src/locales/ja.json +727 -0
  98. package/src/locales/ko.json +727 -0
  99. package/src/locales/nl.json +727 -0
  100. package/src/locales/pl.json +727 -0
  101. package/src/locales/pt-BR.json +727 -0
  102. package/src/locales/ru.json +727 -0
  103. package/src/locales/tr.json +727 -0
  104. package/src/locales/zh-CN.json +727 -0
  105. package/src/pages/Admin/ApiTokens/Index.vue +381 -381
  106. package/src/pages/Admin/AuditLog/Index.vue +154 -154
  107. package/src/pages/Admin/BusinessHours/Form.vue +172 -172
  108. package/src/pages/Admin/BusinessHours/Index.vue +113 -113
  109. package/src/pages/Admin/CannedResponses/Index.vue +127 -127
  110. package/src/pages/Admin/Capacity/Index.vue +134 -134
  111. package/src/pages/Admin/CustomFields/Form.vue +195 -195
  112. package/src/pages/Admin/CustomFields/Index.vue +134 -134
  113. package/src/pages/Admin/CustomObjects/Form.vue +202 -202
  114. package/src/pages/Admin/CustomObjects/Index.vue +127 -127
  115. package/src/pages/Admin/CustomObjects/Records.vue +228 -228
  116. package/src/pages/Admin/Departments/Form.vue +75 -75
  117. package/src/pages/Admin/Departments/Index.vue +110 -109
  118. package/src/pages/Admin/EscalationRules/Form.vue +157 -157
  119. package/src/pages/Admin/EscalationRules/Index.vue +178 -178
  120. package/src/pages/Admin/Import/Connect.vue +0 -1
  121. package/src/pages/Admin/Import/Index.vue +1 -1
  122. package/src/pages/Admin/Import/Progress.vue +1 -1
  123. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -142
  124. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -195
  125. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -193
  126. package/src/pages/Admin/Macros/Index.vue +448 -448
  127. package/src/pages/Admin/Plugins/Index.vue +327 -327
  128. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  129. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -115
  130. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  131. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  132. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  133. package/src/pages/Admin/Reports/CsatReport.vue +127 -127
  134. package/src/pages/Admin/Reports/Dashboard.vue +36 -21
  135. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  136. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  137. package/src/pages/Admin/Reports/SlaReport.vue +162 -166
  138. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  139. package/src/pages/Admin/Reports.vue +206 -206
  140. package/src/pages/Admin/Roles/Form.vue +71 -71
  141. package/src/pages/Admin/Roles/Index.vue +122 -122
  142. package/src/pages/Admin/Settings/CsatSettings.vue +165 -165
  143. package/src/pages/Admin/Settings/DataRetention.vue +182 -182
  144. package/src/pages/Admin/Settings/EmailSettings.vue +243 -243
  145. package/src/pages/Admin/Settings/Sandbox.vue +135 -135
  146. package/src/pages/Admin/Settings/SsoSettings.vue +218 -218
  147. package/src/pages/Admin/Settings/TwoFactor.vue +110 -110
  148. package/src/pages/Admin/Settings.vue +1008 -468
  149. package/src/pages/Admin/Skills/Form.vue +53 -53
  150. package/src/pages/Admin/Skills/Index.vue +94 -94
  151. package/src/pages/Admin/SlaPolicies/Form.vue +118 -118
  152. package/src/pages/Admin/SlaPolicies/Index.vue +121 -120
  153. package/src/pages/Admin/Statuses/Form.vue +114 -114
  154. package/src/pages/Admin/Statuses/Index.vue +153 -153
  155. package/src/pages/Admin/Tags/Index.vue +191 -191
  156. package/src/pages/Admin/Tickets/Index.vue +126 -91
  157. package/src/pages/Admin/Tickets/Show.vue +29 -0
  158. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -120
  159. package/src/pages/Admin/Webhooks/Form.vue +110 -110
  160. package/src/pages/Admin/Webhooks/Index.vue +136 -136
  161. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  162. package/src/pages/Admin/Workflows/Index.vue +235 -0
  163. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  164. package/src/pages/Agent/TicketIndex.vue +112 -88
  165. package/src/pages/Agent/TicketShow.vue +192 -60
  166. package/src/pages/Customer/Create.vue +1 -1
  167. package/src/pages/Customer/Index.vue +31 -31
  168. package/src/pages/Customer/KnowledgeBase/Article.vue +4 -2
  169. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -151
  170. package/src/pages/Customer/Show.vue +69 -63
  171. package/src/pages/Guest/Create.vue +40 -15
  172. package/src/pages/Guest/Show.vue +124 -93
  173. package/src/plugin.js +25 -0
  174. package/src/utils/mentionHighlight.js +24 -0
  175. package/src/utils/sanitizeHtml.js +78 -64
  176. package/src/widget/EscalatedWidget.vue +804 -0
  177. package/src/widget/index.js +254 -0
  178. package/src/pages/Admin/Automations/Form.vue +0 -184
  179. package/src/pages/Admin/Automations/Index.vue +0 -129
@@ -2,8 +2,9 @@
2
2
  import { computed, inject, provide } from 'vue';
3
3
  import { usePage, Link } from '@inertiajs/vue3';
4
4
  import { usePluginExtensions } from '../composables/usePluginExtensions';
5
+ import ActiveChatsPanel from './ActiveChatsPanel.vue';
5
6
 
6
- const props = defineProps({
7
+ defineProps({
7
8
  title: { type: String, default: 'Support' },
8
9
  });
9
10
 
@@ -24,6 +25,11 @@ const isAgentSection = computed(() => currentUrl.value?.includes('/agent'));
24
25
  const isPanel = computed(() => isAdminSection.value || isAgentSection.value);
25
26
  const isDark = computed(() => isPanel.value && panelConfig.mode !== 'light');
26
27
  const showPoweredBy = computed(() => page.props.escalated?.show_powered_by !== false);
28
+ const kbEnabled = computed(() => page.props.escalated?.knowledge_base_enabled !== false);
29
+ const kbPublic = computed(() => page.props.escalated?.knowledge_base_public !== false);
30
+ const chatEnabled = computed(() => page.props.escalated?.chat_enabled === true);
31
+ const activeChats = computed(() => page.props.escalated?.active_chats || []);
32
+ const agentId = computed(() => page.props.auth?.user?.id);
27
33
 
28
34
  provide('esc-dark', isDark);
29
35
 
@@ -57,7 +63,7 @@ const adminLinks = computed(() => {
57
63
  position: 40,
58
64
  },
59
65
  {
60
- href: `${p}/admin/knowledge-base/articles`,
66
+ href: `${p}/admin/kb-articles`,
61
67
  label: 'Knowledge Base',
62
68
  icon: 'M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25',
63
69
  position: 45,
@@ -69,10 +75,10 @@ const adminLinks = computed(() => {
69
75
  position: 50,
70
76
  },
71
77
  {
72
- href: `${p}/admin/automations`,
73
- label: 'Automations',
74
- icon: 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182',
75
- position: 52,
78
+ href: `${p}/admin/workflows`,
79
+ label: 'Workflows',
80
+ icon: 'M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z M15 12a3 3 0 11-6 0 3 3 0 016 0z',
81
+ position: 53,
76
82
  },
77
83
  {
78
84
  href: `${p}/admin/webhooks`,
@@ -505,6 +511,9 @@ function isActive(href) {
505
511
  Powered by Escalated
506
512
  </a>
507
513
  </footer>
514
+
515
+ <!-- Active Chats Panel (agent layout only, when chat enabled) -->
516
+ <ActiveChatsPanel v-if="chatEnabled && agentId" :chats="activeChats" :agent-id="agentId" />
508
517
  </div>
509
518
 
510
519
  <!-- MODE 3: Customer pages — use host app layout if provided -->
@@ -514,6 +523,13 @@ function isActive(href) {
514
523
  <h2 class="text-xl font-semibold leading-tight text-gray-800">{{ title }}</h2>
515
524
  <nav class="flex items-center gap-4 text-sm">
516
525
  <Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
526
+ <Link
527
+ v-if="kbEnabled && (kbPublic || page.props.auth?.user)"
528
+ :href="`${prefix}/kb`"
529
+ class="text-gray-600 hover:text-gray-900"
530
+ >
531
+ Knowledge Base
532
+ </Link>
517
533
  <Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">
518
534
  Agent Panel
519
535
  </Link>
@@ -538,6 +554,13 @@ function isActive(href) {
538
554
  <span class="text-lg font-bold text-gray-900">{{ title }}</span>
539
555
  <div class="flex items-center gap-4 text-sm">
540
556
  <Link :href="prefix" class="text-gray-600 hover:text-gray-900">My Tickets</Link>
557
+ <Link
558
+ v-if="kbEnabled && (kbPublic || page.props.auth?.user)"
559
+ :href="`${prefix}/kb`"
560
+ class="text-gray-600 hover:text-gray-900"
561
+ >
562
+ Knowledge Base
563
+ </Link>
541
564
  <Link v-if="isAgent" :href="`${prefix}/agent`" class="text-gray-600 hover:text-gray-900">
542
565
  Agent Panel
543
566
  </Link>
@@ -1,57 +1,57 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
-
4
- const emit = defineEmits(['files']);
5
- const dark = inject(
6
- 'esc-dark',
7
- computed(() => false),
8
- );
9
-
10
- const dragging = ref(false);
11
- const fileInput = ref(null);
12
-
13
- function onDrop(e) {
14
- dragging.value = false;
15
- if (e.dataTransfer?.files?.length) {
16
- emit('files', Array.from(e.dataTransfer.files));
17
- }
18
- }
19
-
20
- function onFileSelect(e) {
21
- if (e.target.files?.length) {
22
- emit('files', Array.from(e.target.files));
23
- e.target.value = '';
24
- }
25
- }
26
-
27
- function browse() {
28
- fileInput.value?.click();
29
- }
30
- </script>
31
-
32
- <template>
33
- <div
34
- role="button"
35
- aria-label="Drop files here or click to upload"
36
- :class="[
37
- 'cursor-pointer rounded-lg border-2 border-dashed px-4 py-3 text-center text-xs transition-colors',
38
- dark
39
- ? dragging
40
- ? 'border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)]'
41
- : 'border-[var(--esc-panel-border)] hover:border-[var(--esc-panel-border-input)]'
42
- : dragging
43
- ? 'border-blue-400 bg-blue-50'
44
- : 'border-gray-300 hover:border-gray-400',
45
- ]"
46
- @dragover.prevent="dragging = true"
47
- @dragleave="dragging = false"
48
- @drop.prevent="onDrop"
49
- @click="browse"
50
- >
51
- <p :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'">
52
- Drop files here or
53
- <span :class="['font-medium', dark ? 'text-[var(--esc-panel-text)]' : 'text-blue-600']">browse</span>
54
- </p>
55
- <input ref="fileInput" type="file" multiple aria-label="Upload files" class="hidden" @change="onFileSelect" />
56
- </div>
57
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+
4
+ const emit = defineEmits(['files']);
5
+ const dark = inject(
6
+ 'esc-dark',
7
+ computed(() => false),
8
+ );
9
+
10
+ const dragging = ref(false);
11
+ const fileInput = ref(null);
12
+
13
+ function onDrop(e) {
14
+ dragging.value = false;
15
+ if (e.dataTransfer?.files?.length) {
16
+ emit('files', Array.from(e.dataTransfer.files));
17
+ }
18
+ }
19
+
20
+ function onFileSelect(e) {
21
+ if (e.target.files?.length) {
22
+ emit('files', Array.from(e.target.files));
23
+ e.target.value = '';
24
+ }
25
+ }
26
+
27
+ function browse() {
28
+ fileInput.value?.click();
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div
34
+ role="button"
35
+ aria-label="Drop files here or click to upload"
36
+ :class="[
37
+ 'cursor-pointer rounded-lg border-2 border-dashed px-4 py-3 text-center text-xs transition-colors',
38
+ dark
39
+ ? dragging
40
+ ? 'border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)]'
41
+ : 'border-[var(--esc-panel-border)] hover:border-[var(--esc-panel-border-input)]'
42
+ : dragging
43
+ ? 'border-blue-400 bg-blue-50'
44
+ : 'border-gray-300 hover:border-gray-400',
45
+ ]"
46
+ @dragover.prevent="dragging = true"
47
+ @dragleave="dragging = false"
48
+ @drop.prevent="onDrop"
49
+ @click="browse"
50
+ >
51
+ <p :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'">
52
+ Drop files here or
53
+ <span :class="['font-medium', dark ? 'text-[var(--esc-panel-text)]' : 'text-blue-600']">browse</span>
54
+ </p>
55
+ <input ref="fileInput" type="file" multiple aria-label="Upload files" class="hidden" @change="onFileSelect" />
56
+ </div>
57
+ </template>
@@ -1,30 +1,30 @@
1
- import FollowButton from './FollowButton.vue';
2
-
3
- export default {
4
- title: 'Components/FollowButton',
5
- component: FollowButton,
6
- };
7
-
8
- export const NotFollowing = {
9
- args: {
10
- action: '#',
11
- isFollowing: false,
12
- followersCount: 3,
13
- },
14
- };
15
-
16
- export const Following = {
17
- args: {
18
- action: '#',
19
- isFollowing: true,
20
- followersCount: 5,
21
- },
22
- };
23
-
24
- export const NoFollowers = {
25
- args: {
26
- action: '#',
27
- isFollowing: false,
28
- followersCount: 0,
29
- },
30
- };
1
+ import FollowButton from './FollowButton.vue';
2
+
3
+ export default {
4
+ title: 'Components/FollowButton',
5
+ component: FollowButton,
6
+ };
7
+
8
+ export const NotFollowing = {
9
+ args: {
10
+ action: '#',
11
+ isFollowing: false,
12
+ followersCount: 3,
13
+ },
14
+ };
15
+
16
+ export const Following = {
17
+ args: {
18
+ action: '#',
19
+ isFollowing: true,
20
+ followersCount: 5,
21
+ },
22
+ };
23
+
24
+ export const NoFollowers = {
25
+ args: {
26
+ action: '#',
27
+ isFollowing: false,
28
+ followersCount: 0,
29
+ },
30
+ };
@@ -1,88 +1,88 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
- import { router } from '@inertiajs/vue3';
4
-
5
- const props = defineProps({
6
- action: { type: String, required: true },
7
- isFollowing: { type: Boolean, default: false },
8
- followersCount: { type: Number, default: 0 },
9
- });
10
-
11
- const escDark = inject(
12
- 'esc-dark',
13
- computed(() => false),
14
- );
15
- const following = ref(props.isFollowing);
16
- const count = ref(props.followersCount);
17
- const processing = ref(false);
18
-
19
- function toggle() {
20
- if (processing.value) return;
21
- processing.value = true;
22
- router.post(
23
- props.action,
24
- {},
25
- {
26
- preserveScroll: true,
27
- onSuccess: () => {
28
- following.value = !following.value;
29
- count.value += following.value ? 1 : -1;
30
- },
31
- onFinish: () => {
32
- processing.value = false;
33
- },
34
- },
35
- );
36
- }
37
- </script>
38
-
39
- <template>
40
- <button
41
- :disabled="processing"
42
- :class="[
43
- 'inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
44
- escDark
45
- ? following
46
- ? 'bg-cyan-500/15 text-[var(--esc-panel-text)] ring-1 ring-[var(--esc-panel-accent)]/20 hover:bg-cyan-500/25'
47
- : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]'
48
- : following
49
- ? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300 hover:bg-blue-200'
50
- : 'border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-800',
51
- processing && 'opacity-50 cursor-not-allowed',
52
- ]"
53
- :aria-pressed="following"
54
- :aria-label="following ? 'Unfollow ticket' : 'Follow ticket'"
55
- @click="toggle"
56
- >
57
- <!-- Eye icon -->
58
- <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
59
- <template v-if="following">
60
- <path
61
- stroke-linecap="round"
62
- stroke-linejoin="round"
63
- d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
64
- />
65
- <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
66
- </template>
67
- <template v-else>
68
- <path
69
- stroke-linecap="round"
70
- stroke-linejoin="round"
71
- d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
72
- />
73
- </template>
74
- </svg>
75
- {{ following ? 'Following' : 'Follow' }}
76
- <span
77
- v-if="count > 0"
78
- :class="[
79
- 'rounded-full px-1.5 py-0.5 text-xs',
80
- escDark
81
- ? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)]'
82
- : 'bg-gray-100 text-gray-500',
83
- ]"
84
- >
85
- {{ count }}
86
- </span>
87
- </button>
88
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+ import { router } from '@inertiajs/vue3';
4
+
5
+ const props = defineProps({
6
+ action: { type: String, required: true },
7
+ isFollowing: { type: Boolean, default: false },
8
+ followersCount: { type: Number, default: 0 },
9
+ });
10
+
11
+ const escDark = inject(
12
+ 'esc-dark',
13
+ computed(() => false),
14
+ );
15
+ const following = ref(props.isFollowing);
16
+ const count = ref(props.followersCount);
17
+ const processing = ref(false);
18
+
19
+ function toggle() {
20
+ if (processing.value) return;
21
+ processing.value = true;
22
+ router.post(
23
+ props.action,
24
+ {},
25
+ {
26
+ preserveScroll: true,
27
+ onSuccess: () => {
28
+ following.value = !following.value;
29
+ count.value += following.value ? 1 : -1;
30
+ },
31
+ onFinish: () => {
32
+ processing.value = false;
33
+ },
34
+ },
35
+ );
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <button
41
+ :disabled="processing"
42
+ :class="[
43
+ 'inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
44
+ escDark
45
+ ? following
46
+ ? 'bg-cyan-500/15 text-[var(--esc-panel-text)] ring-1 ring-[var(--esc-panel-accent)]/20 hover:bg-cyan-500/25'
47
+ : 'border border-[var(--esc-panel-border-input)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)] hover:bg-[var(--esc-panel-hover)] hover:text-[var(--esc-panel-text-secondary)]'
48
+ : following
49
+ ? 'bg-blue-100 text-blue-700 ring-1 ring-blue-300 hover:bg-blue-200'
50
+ : 'border border-gray-300 bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-800',
51
+ processing && 'opacity-50 cursor-not-allowed',
52
+ ]"
53
+ :aria-pressed="following"
54
+ :aria-label="following ? 'Unfollow ticket' : 'Follow ticket'"
55
+ @click="toggle"
56
+ >
57
+ <!-- Eye icon -->
58
+ <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
59
+ <template v-if="following">
60
+ <path
61
+ stroke-linecap="round"
62
+ stroke-linejoin="round"
63
+ d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
64
+ />
65
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
66
+ </template>
67
+ <template v-else>
68
+ <path
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
72
+ />
73
+ </template>
74
+ </svg>
75
+ {{ following ? 'Following' : 'Follow' }}
76
+ <span
77
+ v-if="count > 0"
78
+ :class="[
79
+ 'rounded-full px-1.5 py-0.5 text-xs',
80
+ escDark
81
+ ? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-tertiary)]'
82
+ : 'bg-gray-100 text-gray-500',
83
+ ]"
84
+ >
85
+ {{ count }}
86
+ </span>
87
+ </button>
88
+ </template>