@opengis/cms 0.0.21 → 0.0.22

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 (183) hide show
  1. package/package.json +2 -2
  2. package/src/App.css +52 -0
  3. package/src/App.vue +62 -0
  4. package/src/assets/image.png +0 -0
  5. package/src/assets/main.css +3 -0
  6. package/src/assets/tailwind-3.4.17.js +113 -0
  7. package/src/components/LanguageSwitcher.vue +73 -0
  8. package/src/components/SettingsCard.vue +40 -0
  9. package/src/components/builder/CreateForm.vue +128 -0
  10. package/src/components/builder/formTypeSchema.js +145 -0
  11. package/src/components/builder/tabs/index.ts +9 -0
  12. package/src/components/builder/tabs/vs-builder-edit.vue +133 -0
  13. package/src/components/builder/tabs/vs-builder-monaco.vue +29 -0
  14. package/src/components/builder/tabs/vs-builder-preview.vue +39 -0
  15. package/src/components/builder/vs-builder-datatable-controls.vue +138 -0
  16. package/src/components/builder/vs-builder-datatable-form.vue +80 -0
  17. package/src/components/builder/vs-builder-datatable.vue +191 -0
  18. package/src/components/builder/vs-builder-list-item.vue +110 -0
  19. package/src/components/collections/CollectionsBreadcrumb.vue +52 -0
  20. package/src/components/collections/CollectionsGrid.vue +176 -0
  21. package/src/components/collections/ContentBlock.vue +75 -0
  22. package/src/components/collections/formWrapper.vue +156 -0
  23. package/src/components/dashboard/ContentItem.vue +82 -0
  24. package/src/components/dashboard/DashboardHeader.vue +33 -0
  25. package/src/components/dashboard/QuickActions.vue +84 -0
  26. package/src/components/dashboard/RecentContent.vue +54 -0
  27. package/src/components/dashboard/StatCard.vue +63 -0
  28. package/src/components/dashboard/StatsGrid.vue +28 -0
  29. package/src/components/form-components/MonacoEditor.vue +104 -0
  30. package/src/components/form-components/VsFormMeta.vue +40 -0
  31. package/src/components/form-components/VsFormTags.vue +150 -0
  32. package/src/components/form-components/custom-datatable/vs-form-custom-datatable-add.vue +84 -0
  33. package/src/components/form-components/custom-datatable/vs-form-custom-datatable-controls.vue +106 -0
  34. package/src/components/form-components/index.js +23 -0
  35. package/src/components/form-components/reference/vs-form-reference-add.vue +92 -0
  36. package/src/components/form-components/reference/vs-form-reference-controls.vue +101 -0
  37. package/src/components/form-components/reference-list/referenceOptionList.js +78 -0
  38. package/src/components/form-components/reference-list/vs-form-reference-add.vue +145 -0
  39. package/src/components/form-components/reference-list/vs-form-reference-choce.vue +39 -0
  40. package/src/components/form-components/reference-list/vs-form-reference-controls.vue +110 -0
  41. package/src/components/form-components/reference-skeleton/about-skeleton.vue +37 -0
  42. package/src/components/form-components/reference-skeleton/banner-skeleton.vue +29 -0
  43. package/src/components/form-components/reference-skeleton/body-skeleton.vue +56 -0
  44. package/src/components/form-components/reference-skeleton/cards-skeleton.vue +47 -0
  45. package/src/components/form-components/reference-skeleton/documents-skeleton.vue +64 -0
  46. package/src/components/form-components/reference-skeleton/faq-skeleton.vue +64 -0
  47. package/src/components/form-components/reference-skeleton/form-skeleton.vue +41 -0
  48. package/src/components/form-components/reference-skeleton/index.js +36 -0
  49. package/src/components/form-components/reference-skeleton/infoLine-skeleton.vue +37 -0
  50. package/src/components/form-components/reference-skeleton/news-skeleton.vue +54 -0
  51. package/src/components/form-components/reference-skeleton/slider-skeleton.vue +41 -0
  52. package/src/components/form-components/reference-skeleton/tabs-skeleton.vue +40 -0
  53. package/src/components/form-components/reference-skeleton/team-skeleton.vue +103 -0
  54. package/src/components/form-components/reference-skeleton/usefulLinks-skeleton.vue +52 -0
  55. package/src/components/form-components/reference-skeleton/video-skeleton.vue +36 -0
  56. package/src/components/form-components/testReferenceTypes.js +773 -0
  57. package/src/components/form-components/vs-form-color-picker.vue +29 -0
  58. package/src/components/form-components/vs-form-custom-datatable.vue +214 -0
  59. package/src/components/form-components/vs-form-integer.vue +86 -0
  60. package/src/components/form-components/vs-form-key-value.vue +201 -0
  61. package/src/components/form-components/vs-form-marcdown-md.vue +3 -0
  62. package/src/components/form-components/vs-form-media-select.vue +780 -0
  63. package/src/components/form-components/vs-form-reference-list.vue +97 -0
  64. package/src/components/form-components/vs-form-reference.vue +59 -0
  65. package/src/components/form-components/vs-form-relation.vue +30 -0
  66. package/src/components/form-components/vs-form-reletion-link.vue +34 -0
  67. package/src/components/form-components/vs-form-select-collection.vue +0 -0
  68. package/src/components/form-components/vs-form-slug.vue +72 -0
  69. package/src/components/form-components/vs-form-tiptap.vue +7 -0
  70. package/src/components/form-components/vs-richtext-md.vue +3 -0
  71. package/src/components/icons/BellIcon.vue +17 -0
  72. package/src/components/icons/GlobeIcon.vue +18 -0
  73. package/src/components/icons/KeyIcon.vue +20 -0
  74. package/src/components/icons/PaletteIcon.vue +22 -0
  75. package/src/components/icons/SettingsIcon.vue +19 -0
  76. package/src/components/icons/ShieldIcon.vue +18 -0
  77. package/src/components/icons/UsersIcon.vue +19 -0
  78. package/src/components/icons/icon-chevron-right.vue +16 -0
  79. package/src/components/icons/icon-drag.vue +20 -0
  80. package/src/components/icons/icon-file-text.vue +21 -0
  81. package/src/components/icons/icon-folder.vue +18 -0
  82. package/src/components/icons/icon-grid.vue +17 -0
  83. package/src/components/icons/icon-group.vue +19 -0
  84. package/src/components/icons/icon-home.vue +16 -0
  85. package/src/components/icons/icon-image.vue +18 -0
  86. package/src/components/icons/icon-list.vue +20 -0
  87. package/src/components/icons/icon-more.vue +17 -0
  88. package/src/components/icons/icon-plus.vue +17 -0
  89. package/src/components/icons-types/icon-array.vue +22 -0
  90. package/src/components/icons-types/icon-boolean.vue +18 -0
  91. package/src/components/icons-types/icon-datalist.vue +22 -0
  92. package/src/components/icons-types/icon-date.vue +20 -0
  93. package/src/components/icons-types/icon-datetime.vue +20 -0
  94. package/src/components/icons-types/icon-file.vue +21 -0
  95. package/src/components/icons-types/icon-gallery.vue +18 -0
  96. package/src/components/icons-types/icon-image.vue +19 -0
  97. package/src/components/icons-types/icon-integer.vue +20 -0
  98. package/src/components/icons-types/icon-merkdown.vue +18 -0
  99. package/src/components/icons-types/icon-multiselect.vue +22 -0
  100. package/src/components/icons-types/icon-number.vue +20 -0
  101. package/src/components/icons-types/icon-radio.vue +22 -0
  102. package/src/components/icons-types/icon-reference-list.vue +22 -0
  103. package/src/components/icons-types/icon-reference.vue +20 -0
  104. package/src/components/icons-types/icon-relation.vue +22 -0
  105. package/src/components/icons-types/icon-richtext.vue +18 -0
  106. package/src/components/icons-types/icon-select.vue +22 -0
  107. package/src/components/icons-types/icon-slug.vue +19 -0
  108. package/src/components/icons-types/icon-text.vue +19 -0
  109. package/src/components/icons-types/index.js +43 -0
  110. package/src/components/layout/Layout.vue +67 -0
  111. package/src/components/layout/Sidebar.vue +128 -0
  112. package/src/components/media/FileUploadProgress.vue +29 -0
  113. package/src/components/media/MediaBreadcrumb.vue +42 -0
  114. package/src/components/media/MediaCreateFolder.vue +59 -0
  115. package/src/components/media/MediaFileInfo.vue +148 -0
  116. package/src/components/media/MediaGrid.vue +148 -0
  117. package/src/components/media/MediaList.vue +148 -0
  118. package/src/components/media/MediaViewControls.vue +38 -0
  119. package/src/components/media/TypeTag.vue +23 -0
  120. package/src/components/menu/AddNewItemInTree.vue +75 -0
  121. package/src/components/menu/MenuBody.vue +149 -0
  122. package/src/components/menu/MenuItem.vue +73 -0
  123. package/src/components/menu/MenuList.vue +101 -0
  124. package/src/components/referencec/index.ts +7 -0
  125. package/src/components/referencec/vs-reference-faq.vue +61 -0
  126. package/src/components/referencec/vs-reference-user-card.vue +40 -0
  127. package/src/components/settings/NotificationSettings.vue +32 -0
  128. package/src/components/settings/SettingsTable.vue +50 -0
  129. package/src/components/settings/SettingsTitle.vue +33 -0
  130. package/src/components/settings/SettingsToggleItem.vue +25 -0
  131. package/src/components/sidebar/DropdownMenu.vue +34 -0
  132. package/src/components/sidebar/SettingsSidebar.vue +121 -0
  133. package/src/components/sidebar/SidebarFooter.vue +52 -0
  134. package/src/components/sidebar/SidebarHeader.vue +57 -0
  135. package/src/components/sidebar/SidebarMenu.vue +78 -0
  136. package/src/components/ui/EmptyData.vue +76 -0
  137. package/src/components/ui/UniversalTable.vue +310 -0
  138. package/src/components/ui/UniversalTableFilters.vue +0 -0
  139. package/src/components/ui/UniversalTablePagination.vue +118 -0
  140. package/src/components/ui/VsPreview.vue +75 -0
  141. package/src/composables/useCollectionView.ts +21 -0
  142. package/src/composables/useDebounce.ts +26 -0
  143. package/src/composables/useMonaco.ts +28 -0
  144. package/src/composables/useTheme.ts +40 -0
  145. package/src/content/test-slug/metadata.json +1 -0
  146. package/src/i18n.ts +75 -0
  147. package/src/index.css +3 -0
  148. package/src/locales/en.json +778 -0
  149. package/src/locales/uk.json +797 -0
  150. package/src/main.ts +41 -0
  151. package/src/pages/Dashboard.vue +168 -0
  152. package/src/pages/EmailPage.vue +183 -0
  153. package/src/pages/FeedbackPage.vue +232 -0
  154. package/src/pages/MediaPage.vue +372 -0
  155. package/src/pages/TagsPage.vue +207 -0
  156. package/src/pages/builder/BuilderPage.vue +195 -0
  157. package/src/pages/builder/EditCollectionPage.vue +163 -0
  158. package/src/pages/collections/ArticlesPage.vue +385 -0
  159. package/src/pages/collections/CollectionsPage.vue +146 -0
  160. package/src/pages/collections/SingletonsPage.vue +119 -0
  161. package/src/pages/collections/contentForm.vue +484 -0
  162. package/src/pages/collections/schema/seo.ts +27 -0
  163. package/src/pages/menu/MenuAddPage.vue +123 -0
  164. package/src/pages/menu/MenuItemPage.vue +183 -0
  165. package/src/pages/menu/MenuPage.vue +133 -0
  166. package/src/pages/settings/ApiKeys.vue +75 -0
  167. package/src/pages/settings/Appearance.vue +80 -0
  168. package/src/pages/settings/Logs.vue +260 -0
  169. package/src/pages/settings/PermissionsPage.vue +237 -0
  170. package/src/pages/settings/Settings.vue +186 -0
  171. package/src/pages/settings/Users.vue +109 -0
  172. package/src/pages/settings/general.vue +154 -0
  173. package/src/pages/settings/generalScheme.js +132 -0
  174. package/src/pages/users/AddUser.vue +106 -0
  175. package/src/pages/users/UsersPage.vue +98 -0
  176. package/src/props/builder.ts +67 -0
  177. package/src/props/content.ts +56 -0
  178. package/src/props/media.ts +63 -0
  179. package/src/router/index.ts +181 -0
  180. package/src/types/fastify-auth.d.ts +4 -0
  181. package/src/utils/getField.js +270 -0
  182. package/src/utils/translit.js +19 -0
  183. package/src/vite-env.d.ts +1 -0
package/src/main.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { createApp } from "vue";
2
+ /*import formComponents from "./components/form-components";
3
+ import referenceComponents from "./components/referencec";
4
+ import iconsTypes from "./components/icons-types";*/
5
+ import App from "./App.vue";
6
+
7
+ import '@opengis/core/dist/index.css'
8
+ import '@opengis/form/dist/index.css'
9
+ // import v3core from '@opengis/v3-core';
10
+ // import v3core from '../../v3-core/src/misc/import-file.ts';
11
+ //
12
+
13
+ import richtext from '@opengis/richtext'
14
+ //import richtext from '../../richtext/src/misc/import-file.js'
15
+
16
+ import router from "./router";
17
+ import { i18n, initI18n } from "./i18n.ts";
18
+ // import "./assets/main.css";
19
+ // import '@opengis/v3-core/dist/style.css';
20
+
21
+ const app = createApp(App);
22
+ /*v3core.install(app, createApp, {
23
+ i18nConfig: i18n,
24
+ });*/
25
+
26
+ /*Object.entries({ ...formComponents, ...iconsTypes, ...referenceComponents }).forEach(([key, value]) => {
27
+ app.component(key, value);
28
+ });*/
29
+
30
+ app.use(router);
31
+ app.use(i18n);
32
+ app.use(richtext, { i18nConfig: i18n });
33
+ app.config.globalProperties.$settings = {
34
+ locale: 'uk',
35
+ locales: ['en', 'uk']
36
+ }
37
+
38
+ // Ініціалізуємо i18n перед монтуванням
39
+ initI18n().then(() => {
40
+ app.mount("#app");
41
+ });
@@ -0,0 +1,168 @@
1
+ <template>
2
+ <div class="w-full max-w-7xl mx-auto space-y-8">
3
+ <!-- Header Section -->
4
+ <DashboardHeader />
5
+
6
+ <!-- Stats Cards -->
7
+ <StatsGrid :stats="statsData" />
8
+
9
+ <!-- Main Content Grid -->
10
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
11
+ <!-- Recent Content -->
12
+ <RecentContent
13
+ :content="recentContent"
14
+ class="lg:col-span-2"
15
+ />
16
+
17
+ <!-- Quick Actions -->
18
+ <div>
19
+ <QuickActions />
20
+ </div>
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { ref, onMounted } from 'vue';
27
+ import DashboardHeader from '../components/dashboard/DashboardHeader.vue';
28
+ import StatsGrid from '../components/dashboard/StatsGrid.vue';
29
+ import RecentContent from '../components/dashboard/RecentContent.vue';
30
+ import QuickActions from '../components/dashboard/QuickActions.vue';
31
+ import { useI18n } from 'vue-i18n';
32
+
33
+ const { t } = useI18n();
34
+
35
+ // Types
36
+ interface StatItem {
37
+ id: string;
38
+ title: string;
39
+ value: string;
40
+ description: string;
41
+ change: {
42
+ value: string;
43
+ isPositive: boolean;
44
+ };
45
+ icon: string;
46
+ color: string;
47
+ }
48
+
49
+ interface ContentItem {
50
+ id: number;
51
+ type: 'Article' | 'Post';
52
+ status: 'published' | 'draft' | 'review';
53
+ title: string;
54
+ author: string;
55
+ updated_at: string;
56
+ views: number;
57
+ }
58
+
59
+
60
+
61
+ // Data
62
+ const statsData = ref<StatItem[]>([
63
+ {
64
+ id: 'total-content',
65
+ title: t('dashboard.totalContent'),
66
+ value: '2,847',
67
+ description: t('dashboard.articlesPostsPages'),
68
+ change: { value: '+12.5%', isPositive: true },
69
+ icon: 'file-text',
70
+ color: 'blue'
71
+ },
72
+ {
73
+ id: 'active-users',
74
+ title: t('dashboard.activeUsers'),
75
+ value: '1,234',
76
+ description: t('dashboard.monthlyActiveUsers'),
77
+ change: { value: '+8.2%', isPositive: true },
78
+ icon: 'users',
79
+ color: 'emerald'
80
+ },
81
+ {
82
+ id: 'media-files',
83
+ title: t('dashboard.mediaFiles'),
84
+ value: '5,692',
85
+ description: t('dashboard.imagesVideosDocs'),
86
+ change: { value: '+23.1%', isPositive: true },
87
+ icon: 'image',
88
+ color: 'purple'
89
+ },
90
+ {
91
+ id: 'new-media',
92
+ title: t('dashboard.newMediaFiles'),
93
+ value: '89.2K',
94
+ description: t('dashboard.thisMonth'),
95
+ change: { value: '-2.4%', isPositive: false },
96
+ icon: 'eye',
97
+ color: 'orange'
98
+ }
99
+ ]);
100
+
101
+ const recentContent = ref<ContentItem[]>([
102
+ {
103
+ id: 1,
104
+ type: 'Article',
105
+ status: 'published',
106
+ title: t('dashboard.gettingStartedWithNextJs14'),
107
+ author: t('dashboard.sarahJohnson'),
108
+ updated_at: '2024-01-15',
109
+ views: 1247
110
+ },
111
+ {
112
+ id: 2,
113
+ type: 'Post',
114
+ status: 'draft',
115
+ title: t('dashboard.advancedReactPatterns'),
116
+ author: t('dashboard.mikeChen'),
117
+ updated_at: '2024-01-14',
118
+ views: 0
119
+ },
120
+ {
121
+ id: 3,
122
+ type: 'Article',
123
+ status: 'published',
124
+ title: t('dashboard.uiUxDesignPrinciples'),
125
+ author: t('dashboard.emmaWilson'),
126
+ updated_at: '2024-01-13',
127
+ views: 892
128
+ },
129
+ {
130
+ id: 4,
131
+ type: 'Post',
132
+ status: 'review',
133
+ title: t('dashboard.databaseOptimizationTips'),
134
+ author: t('dashboard.alexRodriguez'),
135
+ updated_at: '2024-01-12',
136
+ views: 0
137
+ }
138
+ ]);
139
+
140
+ const rewriteStatsData = (data: any) => {
141
+ statsData.value = statsData.value.map(stat => {
142
+ switch (stat.id) {
143
+ case 'total-content':
144
+ return { ...stat, value: data.totalContent?.toLocaleString() ?? stat.value };
145
+ case 'active-users':
146
+ return { ...stat, value: data.totalUsers?.toLocaleString() ?? stat.value };
147
+ case 'media-files':
148
+ return { ...stat, value: data.totalMediaFiles?.toLocaleString() ?? stat.value };
149
+ case 'new-media':
150
+ return { ...stat, value: data.newMediaFiles?.toLocaleString() ?? stat.value };
151
+ default:
152
+ return stat;
153
+ }
154
+ });
155
+ }
156
+
157
+ const fetchDashboardData = async () => {
158
+ const res = await fetch('api/cms-stat');
159
+ const data = await res.json();
160
+
161
+ recentContent.value = data.recentContent;
162
+ rewriteStatsData(data.stat);
163
+ };
164
+
165
+ onMounted(() => {
166
+ fetchDashboardData();
167
+ });
168
+ </script>
@@ -0,0 +1,183 @@
1
+ <template>
2
+
3
+ <div class="w-full max-w-7xl mx-auto space-y-6">
4
+ <SettingsTitle :title="$t('email.title')" :description="$t('email.description')" />
5
+ <div class="rounded-xl text-card-foreground shadow-lg border-0 bg-white dark:bg-slate-800 backdrop-blur-sm">
6
+ <div class="flex flex-col space-y-1.5 p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-slate-50 dark:from-slate-800 to-white dark:to-slate-800">
7
+ <h3 class="tracking-tight text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
8
+ <TableCellsMerge class="text-emerald-500 mr-2"/>
9
+ {{ $t('email.title') }}
10
+ </h3>
11
+ </div>
12
+ <div class="p-6 space-y-4">
13
+ <div class="flex justify-between">
14
+ <VsFilter
15
+ :schema="filterScheme"
16
+ view="inline"
17
+ @change="handleChange"
18
+ />
19
+ </div>
20
+ <div class="overflow-x-auto" v-if="email?.length">
21
+ <table class="min-w-full divide-y divide-gray-200">
22
+ <thead class="bg-gray-100">
23
+ <tr>
24
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">№</th>
25
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.email') }}</th>
26
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.cdate') }}</th>
27
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700"></th>
28
+ </tr>
29
+ </thead>
30
+ <tbody class="divide-y divide-gray-200" v-for="(email, index) in email" :key="email?.message_id">
31
+
32
+ <tr class="cursor-pointer dark:text-white">
33
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ email?.num }}</td>
34
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ email?.email }}</td>
35
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ formatDate(email?.created_at) }}</td>
36
+ </tr>
37
+ </tbody>
38
+ </table>
39
+ </div>
40
+ <div class='bg-white dark:bg-slate-800 backdrop-blur-sm0 text-center p-6 rounded-xl' v-else>
41
+ <h3 class='text-lg font-semibold'>{{ $t('feedback.table.noFeedback') }}</h3>
42
+ </div>
43
+
44
+ <div class="my-5 pr-2">
45
+ <div class="paginationWrapper relative flex justify-center">
46
+ <VsPagination
47
+ :total
48
+ :maxPages="6"
49
+ :defaultPage="page"
50
+ :pageSize="limit"
51
+ :goTo="false"
52
+ size="medium"
53
+ backgroundColor="#e5e7eb"
54
+ :borderedCellSelected="true"
55
+ :pageSizes="[16, 32, 48]"
56
+ @pageChange="handlePageChange"
57
+ @pageSizeChange="changeLimit"
58
+ />
59
+ </div>
60
+ </div>
61
+
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </template>
66
+ <script>
67
+ import SettingsTitle from '@/components/settings/SettingsTitle.vue';
68
+ import VsFilter from '@opengis/filter';
69
+ import {
70
+ TableCellsMerge,
71
+ ArrowDown,
72
+ ArrowUp,
73
+ ArrowRightFromLine,
74
+ } from "lucide-vue-next";
75
+
76
+ export default {
77
+ components: {
78
+ SettingsTitle,
79
+ VsFilter,
80
+ TableCellsMerge,
81
+ ArrowDown,
82
+ ArrowUp,
83
+ ArrowRightFromLine
84
+ },
85
+ data() {
86
+ return {
87
+ email: null,
88
+ total: 0,
89
+ openIndex: null,
90
+ limit: 16,
91
+ page: 1,
92
+ filterValue: '',
93
+ filterScheme: [],
94
+ }
95
+ },
96
+ async created() {
97
+ this.$router.replace({
98
+ query: { page: 1 }
99
+ })
100
+ await this.getData();
101
+ },
102
+ methods: {
103
+ async getData() {
104
+ try {
105
+ const response = await fetch(`/api/email-list?limit=${this.limit}&page=${this.page}`);
106
+ const data = await response.json();
107
+ this.email = data?.rows;
108
+ this.total = data?.total;
109
+ this.filterScheme = data?.filters;
110
+ } catch (err) {
111
+ console.error(err.toString())
112
+ }
113
+ },
114
+ async handleChange(filters) {
115
+ const filterStr = Object.entries(filters?.data)
116
+ .filter(([, v]) => v != null)
117
+ .map(([key, val]) => `${key}=${val}`)
118
+ .join('|');
119
+
120
+ this.filterValue = filterStr;
121
+
122
+ if (this.filterValue ==='no-url') {
123
+ this.$router.replace({
124
+ query: { page: 1 }
125
+ })
126
+ } else {
127
+ this.$router.replace({
128
+ query: { filter:this.filterValue, page: 1 }
129
+ })
130
+
131
+ };
132
+ this.page = 1;
133
+ await this.getData();
134
+ },
135
+ async handlePageChange(page) {
136
+ this.page = page;
137
+ if (this.filterValue==='no-url' || !this.filterValue) {
138
+ this.$router.replace({
139
+ query: { page }
140
+ })
141
+ } else {
142
+ this.$router.replace({
143
+ query: { filter:this.filterValue, page}
144
+ })
145
+
146
+ }
147
+ await this.getData();
148
+ },
149
+ async changeLimit(limit) {
150
+ this.limit = limit;
151
+ this.page = 1;
152
+ await this.getData();
153
+ },
154
+ toggle(index) {
155
+ this.openIndex = this.openIndex === index ? null : index
156
+ },
157
+ onEnter(el) {
158
+ el.style.height = '0'
159
+ el.style.opacity = '0'
160
+ requestAnimationFrame(() => {
161
+ el.style.transition = 'all 0.3s ease'
162
+ el.style.height = el.scrollHeight + 'px'
163
+ el.style.opacity = '1'
164
+ })
165
+ },
166
+ onLeave(el) {
167
+ el.style.height = el.scrollHeight + 'px'
168
+ el.style.opacity = '1'
169
+ requestAnimationFrame(() => {
170
+ el.style.transition = 'all 0.3s ease'
171
+ el.style.height = '0'
172
+ el.style.opacity = '0'
173
+ })
174
+ },
175
+ formatDate(date) {
176
+ if (!date) return null;
177
+ const options = { year: "numeric", month: "2-digit", day: "2-digit" };
178
+ return new Date(date).toLocaleDateString("uk-UA", options);
179
+ }
180
+ }
181
+
182
+ }
183
+ </script>
@@ -0,0 +1,232 @@
1
+ <template>
2
+
3
+ <div class="w-full max-w-7xl mx-auto space-y-6">
4
+ <SettingsTitle :title="$t('feedback.title')" :description="$t('feedback.description')" />
5
+ <div class="rounded-xl text-card-foreground shadow-lg border-0 bg-white dark:bg-slate-800 backdrop-blur-sm">
6
+ <div class="flex flex-col space-y-1.5 p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-slate-50 dark:from-slate-800 to-white dark:to-slate-800">
7
+ <h3 class="tracking-tight text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
8
+ <TableCellsMerge class="text-emerald-500 mr-2"/>
9
+ {{ $t('feedback.title') }}
10
+ </h3>
11
+ </div>
12
+ <div class="p-6 space-y-4">
13
+ <div class="flex justify-between">
14
+ <VsFilter
15
+ :schema="filterScheme"
16
+ view="inline"
17
+ @change="handleChange"
18
+ />
19
+ </div>
20
+ <div class="overflow-x-auto" v-if="feedback?.length">
21
+ <table class="min-w-full divide-y divide-gray-200">
22
+ <thead class="bg-gray-100">
23
+ <tr>
24
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">№</th>
25
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.userName') }}</th>
26
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.email') }}</th>
27
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.cdate') }}</th>
28
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.phone') }}</th>
29
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700"></th>
30
+ </tr>
31
+ </thead>
32
+ <tbody class="divide-y divide-gray-200" v-for="(feedback, index) in feedback" :key="feedback?.message_id">
33
+
34
+ <tr class="cursor-pointer dark:text-white" @click="toggle(feedback?.message_id)">
35
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ feedback?.num }}</td>
36
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{feedback?.user_name}}</td>
37
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ feedback?.email }}</td>
38
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ formatDate(feedback?.created_at) }}</td>
39
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{feedback?.phone}}</td>
40
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">
41
+ <div>
42
+ <ArrowUp v-if="openIndex === feedback?.message_id"/>
43
+ <ArrowDown v-else/>
44
+ </div>
45
+ </td>
46
+ </tr>
47
+ <Transition name="collapse" @enter="onEnter" @leave="onLeave">
48
+ <tr v-show="openIndex === feedback?.message_id">
49
+ <td colspan="6" class="bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:text-slate-300">
50
+ <div ref="content" class="transition-all duration-300 ease-in-out space-y-2">
51
+
52
+ <div v-if="feedback?.message_text">
53
+ <b>{{ $t('feedback.table.messageText') }}:</b>
54
+ <p class="whitespace-pre-line mt-1">{{ feedback.message_text }}</p>
55
+ </div>
56
+
57
+ <div v-if="feedback?.feedback_type">
58
+ <b>{{ $t('feedback.table.feedbackType') }}:</b>
59
+ <span>{{ feedback.feedback_type }}</span>
60
+ </div>
61
+
62
+ <div v-if="feedback?.telegram_link">
63
+ <b>Telegram:</b>
64
+ <a :href="feedback.telegram_link" class="text-blue-500 underline " target="_blank">{{ feedback.telegram_link }}</a>
65
+ </div>
66
+
67
+ <div v-if="feedback?.linkedin_link">
68
+ <b>LinkedIn:</b>
69
+ <a :href="feedback.linkedin_link" class="text-blue-500 underline " target="_blank">{{ feedback.linkedin_link }}</a>
70
+ </div>
71
+
72
+ <div v-if="feedback?.portfolio_link">
73
+ <b>{{ $t('feedback.table.portfolioLink') }}:</b>
74
+ <a :href="feedback.portfolio_link" class="text-blue-500 underline " target="_blank">{{ feedback.portfolio_link }}</a>
75
+ </div>
76
+ </div>
77
+ </td>
78
+ </tr>
79
+ </Transition>
80
+ </tbody>
81
+ </table>
82
+ </div>
83
+ <div class='bg-white dark:bg-slate-800 backdrop-blur-sm0 text-center p-6 rounded-xl' v-else>
84
+ <h3 class='text-lg font-semibold'>{{ $t('feedback.table.noFeedback') }}</h3>
85
+ </div>
86
+
87
+ <div class="my-5 pr-2">
88
+ <div class="paginationWrapper relative flex justify-center">
89
+ <VsPagination
90
+ :total
91
+ :maxPages="6"
92
+ :defaultPage="page"
93
+ :pageSize="limit"
94
+ :goTo="false"
95
+ size="medium"
96
+ backgroundColor="#e5e7eb"
97
+ :borderedCellSelected="true"
98
+ :pageSizes="[16, 32, 48]"
99
+ @pageChange="handlePageChange"
100
+ @pageSizeChange="changeLimit"
101
+ />
102
+ </div>
103
+ </div>
104
+
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </template>
109
+ <script>
110
+ import SettingsTitle from '@/components/settings/SettingsTitle.vue';
111
+ import {
112
+ TableCellsMerge,
113
+ ArrowDown,
114
+ ArrowUp,
115
+ ArrowRightFromLine,
116
+ } from "lucide-vue-next";
117
+ import VsFilter from '@opengis/filter';
118
+
119
+ export default {
120
+ components: {
121
+ SettingsTitle,
122
+ TableCellsMerge,
123
+ ArrowDown,
124
+ ArrowUp,
125
+ ArrowRightFromLine,
126
+ VsFilter
127
+ },
128
+ data() {
129
+ return {
130
+ feedback: null,
131
+ total: 0,
132
+ openIndex: null,
133
+ limit: 16,
134
+ page: 1,
135
+ filterValue: '',
136
+ filterScheme: [],
137
+ }
138
+ },
139
+ async created() {
140
+ this.$router.replace({
141
+ query: { page: 1 }
142
+ })
143
+ await this.getData();
144
+ },
145
+ methods: {
146
+ async getData() {
147
+ try {
148
+ const response = await fetch(`/api/feedback?limit=${this.limit}&page=${this.page}${(this.filterValue ==='no-url') ? '':`&filter=${this.filterValue}`}`);
149
+ const data = await response.json();
150
+ this.feedback = data?.rows;
151
+ this.total = data?.total;
152
+
153
+ // Clean filter schema to only include valid IFilterItem properties
154
+ const cleanFilters = (data?.filters || []).map((filter) => {
155
+ const { extra, title, ...cleanFilter } = filter;
156
+ return cleanFilter;
157
+ });
158
+ this.filterScheme = cleanFilters;
159
+ } catch (err) {
160
+ console.error(err.toString())
161
+ }
162
+ },
163
+ async handleChange(filters) {
164
+ const filterStr = Object.entries(filters?.data)
165
+ .filter(([, v]) => v != null)
166
+ .map(([key, val]) => `${key}=${val}`)
167
+ .join('|');
168
+
169
+ this.filterValue = filterStr;
170
+
171
+ if (this.filterValue ==='no-url') {
172
+ this.$router.replace({
173
+ query: { page: 1 }
174
+ })
175
+ } else {
176
+ this.$router.replace({
177
+ query: { filter:this.filterValue, page: 1 }
178
+ })
179
+
180
+ };
181
+ this.page = 1;
182
+ await this.getData();
183
+ },
184
+ async handlePageChange(page) {
185
+ this.page = page;
186
+ if (this.filterValue==='no-url' || !this.filterValue) {
187
+ this.$router.replace({
188
+ query: { page }
189
+ })
190
+ } else {
191
+ this.$router.replace({
192
+ query: { filter:this.filterValue, page}
193
+ })
194
+
195
+ }
196
+ await this.getData();
197
+ },
198
+ async changeLimit(limit) {
199
+ this.limit = limit;
200
+ this.page = 1;
201
+ await this.getData();
202
+ },
203
+ toggle(index) {
204
+ this.openIndex = this.openIndex === index ? null : index
205
+ },
206
+ onEnter(el) {
207
+ el.style.height = '0'
208
+ el.style.opacity = '0'
209
+ requestAnimationFrame(() => {
210
+ el.style.transition = 'all 0.3s ease'
211
+ el.style.height = el.scrollHeight + 'px'
212
+ el.style.opacity = '1'
213
+ })
214
+ },
215
+ onLeave(el) {
216
+ el.style.height = el.scrollHeight + 'px'
217
+ el.style.opacity = '1'
218
+ requestAnimationFrame(() => {
219
+ el.style.transition = 'all 0.3s ease'
220
+ el.style.height = '0'
221
+ el.style.opacity = '0'
222
+ })
223
+ },
224
+ formatDate(date) {
225
+ if (!date) return null;
226
+ const options = { year: "numeric", month: "2-digit", day: "2-digit" };
227
+ return new Date(date).toLocaleDateString("uk-UA", options);
228
+ }
229
+ }
230
+
231
+ }
232
+ </script>