@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
@@ -0,0 +1,260 @@
1
+ <template>
2
+
3
+ <div class="w-full max-w-7xl mx-auto space-y-6">
4
+ <SettingsTitle :title="$t('settings.logs.title')" :description="$t('settings.logs.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('settings.logs.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
+ <button
20
+ v-if="logs?.length"
21
+ @click="exportToCSV"
22
+ class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none text-white focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-primary-foreground shadow h-9 px-4 py-2 bg-emerald-600 hover:bg-emerald-700">Експорт в CSV</button>
23
+ </div>
24
+ <div class="overflow-x-auto" v-if="logs?.length">
25
+ <table class="min-w-full divide-y divide-gray-200">
26
+ <thead class="bg-gray-100">
27
+ <tr>
28
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">№</th>
29
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('settings.logs.table.objectId') }}</th>
30
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('settings.logs.table.dateTime') }}</th>
31
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('settings.logs.table.changeType') }}</th>
32
+ <th class="px-4 py-2 text-left text-sm font-semibold text-gray-700"></th>
33
+ </tr>
34
+ </thead>
35
+ <tbody class="divide-y divide-gray-200" v-for="(log, index) in logs" :key="log?.change_id">
36
+
37
+ <tr class="cursor-pointer dark:text-white" :class="[log?.change_type == 'DELETE'?'pointer-events-none':'']" @click="toggle(log?.change_id)">
38
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ log?.num }}</td>
39
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ log?.entity_id }}</td>
40
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ formatDateTime(log?.cdate) }}</td>
41
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{log?.change_type}}</td>
42
+ <td class="px-4 py-2 text-sm text-gray-800 dark:text-white">
43
+ <div v-if="log?.change_type !== 'DELETE'">
44
+ <ArrowUp v-if="openIndex === log?.change_id"/>
45
+ <ArrowDown v-else/>
46
+ </div>
47
+ </td>
48
+ </tr>
49
+ <Transition
50
+ name="collapse"
51
+ @enter="onEnter"
52
+ @leave="onLeave"
53
+ >
54
+ <tr v-show="openIndex === log?.change_id && log?.change_type !== 'DELETE'">
55
+ <td colspan="5" class="bg-gray-50 px-4 py-3 text-sm text-gray-600 overflow-hidden">
56
+ <div ref="content" class="transition-all duration-300 ease-in-out">
57
+ <p v-for="(col,index) in log?.data_log" :key="col?.change_data_id">
58
+ <span v-if="col?.value_new !== col?.value_old">
59
+ <div class="flex flex-1 gap-2">
60
+ <div><strong>{{ index+1 }}. {{ col?.entity_key }}:</strong></div>
61
+ <div v-if="log?.change_type === 'UPDATE'" class="flex gap-1">{{ col?.value_old }} <ArrowRightFromLine class="w-[16px] h-[16px]"/> {{ col?.value_new }}</div>
62
+ <div v-if="log?.change_type === 'INSERT'" class="flex">{{ col?.value_new }}</div>
63
+ </div>
64
+ </span>
65
+ </p>
66
+ </div>
67
+ </td>
68
+ </tr>
69
+ </Transition>
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+ <div class='bg-white dark:bg-slate-800 backdrop-blur-sm0 text-center p-6 rounded-xl' v-else>
74
+ <h3 class='text-lg font-semibold'>{{ $t('settings.logs.table.noLogs') }}</h3>
75
+ </div>
76
+
77
+ <div class="my-5 pr-2" v-if="total !== null && total > 0">
78
+ <div class="paginationWrapper relative flex justify-center items-center gap-4" >
79
+ <button
80
+ @click="handlePreviousPage"
81
+ :disabled="page === 1"
82
+ class="inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors border shadow-sm rounded-md text-xs h-8 w-8 p-0 bg-white border-slate-300 text-slate-600
83
+ disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed
84
+ hover:text-accent-foreground hover:bg-slate-50
85
+ dark:bg-slate-700 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-600"
86
+ >
87
+ <ChevronLeft class="w-5 h-5" />
88
+ </button>
89
+ <button
90
+ @click="handleNextPage"
91
+ :disabled="page >= totalPages"
92
+ class="inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors border shadow-sm rounded-md text-xs h-8 w-8 p-0 bg-white border-slate-300 text-slate-600
93
+ disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed
94
+ hover:text-accent-foreground hover:bg-slate-50
95
+ dark:bg-slate-700 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-600"
96
+ >
97
+ <ChevronRight class="w-5 h-5" />
98
+ </button>
99
+ </div>
100
+ </div>
101
+
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </template>
106
+ <script>
107
+ import { useRoute } from 'vue-router'
108
+ import SettingsTitle from '@/components/settings/SettingsTitle.vue';
109
+ import {
110
+ TableCellsMerge,
111
+ ArrowDown,
112
+ ArrowUp,
113
+ ArrowRightFromLine,
114
+ ChevronLeft,
115
+ ChevronRight,
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
+ ChevronLeft,
127
+ ChevronRight,
128
+ VsFilter
129
+ },
130
+ data() {
131
+ return {
132
+ logs: null,
133
+ total: null,
134
+ openIndex: null,
135
+ limit: 16,
136
+ page: 1,
137
+ filterValue: '',
138
+ filterScheme: [],
139
+ }
140
+ },
141
+ computed: {
142
+ totalPages() {
143
+ return Math.ceil(this.total / this.limit);
144
+ }
145
+ },
146
+ async created() {
147
+ /*this.route = useRoute()
148
+ this.filterValue = this.route.query?.filter || '';*/
149
+ this.$router.replace({
150
+ query: { page: 1 }
151
+ })
152
+ await this.getData();
153
+ },
154
+ methods: {
155
+ async getData() {
156
+ try {
157
+ const response = await fetch(`/api/user-logs?limit=${this.limit}&page=${this.page}${(this.filterValue ==='no-url') ? '':`&filter=${this.filterValue}`}`);
158
+ const data = await response.json();
159
+ this.logs = data?.rows;
160
+ this.total = data?.total;
161
+ this.filterScheme = data?.filters;
162
+ } catch (err) {
163
+ console.error(err.toString())
164
+ }
165
+ },
166
+ async exportToCSV() {
167
+ window.open(`/api/export-user-logs${(this.filterValue ==='no-url') ? '':`?filter=${this.filterValue}`}`, '_blank');
168
+ /*try {
169
+ await fetch(`/api/export-user-logs${(this.filterValue ==='no-url') ? '':`?filter=${this.filterValue}`}`);
170
+
171
+ } catch (err) {
172
+ console.error(err.toString())
173
+ }*/
174
+ },
175
+ async handleChange(event) {
176
+ const filterStr = Object.entries(event?.data)
177
+ .filter(([, v]) => v != null)
178
+ .map(([key, val]) => `${key}=${val}`)
179
+ .join('|');
180
+
181
+ this.filterValue = filterStr;
182
+ if (this.filterValue ==='no-url') {
183
+ this.$router.replace({
184
+ query: { page: 1 }
185
+ })
186
+ } else {
187
+ this.$router.replace({
188
+ query: { filter:this.filterValue, page: 1 }
189
+ })
190
+
191
+ };
192
+ this.page = 1;
193
+ await this.getData();
194
+ },
195
+ async handlePageChange(page) {
196
+ this.page = page;
197
+ if (this.filterValue==='no-url' || !this.filterValue) {
198
+ this.$router.replace({
199
+ query: { page }
200
+ })
201
+ } else {
202
+ this.$router.replace({
203
+ query: { filter:this.filterValue, page}
204
+ })
205
+
206
+ }
207
+ await this.getData();
208
+ },
209
+ async handlePreviousPage() {
210
+ if (this.page > 1) {
211
+ await this.handlePageChange(this.page - 1);
212
+ }
213
+ },
214
+ async handleNextPage() {
215
+ if (this.page < this.totalPages) {
216
+ await this.handlePageChange(this.page + 1);
217
+ }
218
+ },
219
+ async changeLimit(limit) {
220
+ this.limit = limit;
221
+ this.page = 1;
222
+ await this.getData();
223
+ },
224
+ toggle(index) {
225
+ this.openIndex = this.openIndex === index ? null : index
226
+ },
227
+ onEnter(el) {
228
+ el.style.height = '0'
229
+ el.style.opacity = '0'
230
+ requestAnimationFrame(() => {
231
+ el.style.transition = 'all 0.3s ease'
232
+ el.style.height = el.scrollHeight + 'px'
233
+ el.style.opacity = '1'
234
+ })
235
+ },
236
+ onLeave(el) {
237
+ el.style.height = el.scrollHeight + 'px'
238
+ el.style.opacity = '1'
239
+ requestAnimationFrame(() => {
240
+ el.style.transition = 'all 0.3s ease'
241
+ el.style.height = '0'
242
+ el.style.opacity = '0'
243
+ })
244
+ },
245
+ formatDateTime(dateTimeString) {
246
+ if (!dateTimeString) return ''
247
+
248
+ const [datePart, timePart] = dateTimeString.split(' ')
249
+ if (!datePart || !timePart) return dateTimeString
250
+
251
+ const [year, month, day] = datePart.split('-')
252
+ const [hour, minute, secondWithMs] = timePart.split(':')
253
+ const second = secondWithMs?.slice(0, 2)
254
+
255
+ return `${day}.${month}.${year} ${hour}:${minute}:${second}`
256
+ }
257
+ }
258
+
259
+ }
260
+ </script>
@@ -0,0 +1,237 @@
1
+ <template>
2
+ <div class="space-y-6 w-full max-w-7xl mx-auto">
3
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
4
+ <h1 class="text-3xl font-bold text-slate-800 dark:text-white mb-2">{{$t('settings.permissions.title')}}</h1>
5
+ <button
6
+ class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-9 px-4 py-2 bg-blue-600 text-white shadow-md transition-all duration-200 transform
7
+ hover:bg-blue-700 hover:shadow-lg hover:scale-105
8
+ focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
9
+ disabled:pointer-events-none disabled:opacity-50"
10
+ >
11
+ <Plus class="w-4 h-4 mr-2" />
12
+ {{$t('settings.permissions.inviteUser')}}
13
+ </button>
14
+ </div>
15
+ <div class="grid grid-cols-1 md:grid-cols-12 gap-6">
16
+ <div class="md:col-span-8 bg-white dark:bg-gray-800 shadow-sm rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
17
+ <div class="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700">
18
+ <div class="flex items-center">
19
+ <div class="relative flex-1">
20
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
21
+ <Search class="h-5 w-5 text-gray-400" />
22
+ </div>
23
+ <input
24
+ v-model="searchQuery"
25
+ type="text"
26
+ class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md leading-5 bg-white dark:bg-gray-700 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-sky-500 focus:border-sky-500 text-gray-900 dark:text-white sm:text-sm transition-colors"
27
+ :placeholder="$t('settings.permissions.searchUsers')"
28
+ />
29
+ </div>
30
+ </div>
31
+ </div>
32
+ <div class="overflow-x-auto">
33
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
34
+ <thead class="bg-gray-50 dark:bg-gray-750">
35
+ <tr>
36
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
37
+ {{$t('settings.permissions.user')}}
38
+ </th>
39
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
40
+ {{$t('settings.permissions.role')}}
41
+ </th>
42
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
43
+ {{$t('settings.permissions.lastActive')}}
44
+ </th>
45
+ <th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
46
+ {{$t('settings.permissions.actions')}}
47
+ </th>
48
+ </tr>
49
+ </thead>
50
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
51
+ <tr v-for="user in filteredUsers" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
52
+ <td class="px-6 py-4 whitespace-nowrap">
53
+ <div class="flex items-center">
54
+ <div class="h-10 w-10 rounded-full bg-sky-100 dark:bg-sky-900 flex items-center justify-center text-sky-600 dark:text-sky-300 font-medium text-sm">
55
+ {{user.name.charAt(0).toUpperCase()}}
56
+ </div>
57
+ <div class="ml-4">
58
+ <div class="text-sm font-medium text-gray-900 dark:text-white">{{user.name}}</div>
59
+ <div class="text-sm text-gray-500 dark:text-gray-400">{{user.email}}</div>
60
+ </div>
61
+ </div>
62
+ </td>
63
+ <td class="px-6 py-4 whitespace-nowrap">
64
+ <span :class="['px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full', getRoleBadgeColor(user.role)]">
65
+ {{$t('settings.permissions.roles.' + user.role)}}
66
+ </span>
67
+ </td>
68
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
69
+ {{user.lastActive}}
70
+ </td>
71
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
72
+ <button class="text-sky-600 hover:text-sky-900 dark:text-sky-400 dark:hover:text-sky-300 mr-3 transition-colors">
73
+ {{$t('settings.permissions.edit')}}
74
+ </button>
75
+ <button class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 transition-colors">
76
+ {{$t('settings.permissions.remove')}}
77
+ </button>
78
+ </td>
79
+ </tr>
80
+ </tbody>
81
+ </table>
82
+ </div>
83
+ </div>
84
+ <div class="md:col-span-4 bg-white dark:bg-gray-800 shadow-sm rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
85
+ <div class="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700">
86
+ <h2 class="text-lg font-medium text-gray-900 dark:text-white">{{$t('settings.permissions.rolePermissions')}}</h2>
87
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
88
+ {{$t('settings.permissions.rolePermissionsDescription')}}
89
+ </p>
90
+ </div>
91
+ <div class="overflow-x-auto">
92
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
93
+ <thead class="bg-gray-50 dark:bg-gray-750">
94
+ <tr>
95
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
96
+ {{$t('settings.permissions.permission')}}
97
+ </th>
98
+ <th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
99
+ {{$t('settings.permissions.admin')}}
100
+ </th>
101
+ <th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
102
+ {{$t('settings.permissions.editor')}}
103
+ </th>
104
+ <th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
105
+ {{$t('settings.permissions.author')}}
106
+ </th>
107
+ <th scope="col" class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
108
+ {{$t('settings.permissions.viewer')}}
109
+ </th>
110
+ </tr>
111
+ </thead>
112
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
113
+ <tr v-for="(permission, index) in permissions" :key="index" class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
114
+ <td class="px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
115
+ {{permission.name}}
116
+ </td>
117
+ <td class="px-6 py-3 whitespace-nowrap text-center">
118
+ <Check v-if="permission.admin" class="mx-auto h-5 w-5 text-green-500" />
119
+ <X v-else class="mx-auto h-5 w-5 text-red-500" />
120
+ </td>
121
+ <td class="px-6 py-3 whitespace-nowrap text-center">
122
+ <Check v-if="permission.editor" class="mx-auto h-5 w-5 text-green-500" />
123
+ <X v-else class="mx-auto h-5 w-5 text-red-500" />
124
+ </td>
125
+ <td class="px-6 py-3 whitespace-nowrap text-center">
126
+ <Check v-if="permission.author" class="mx-auto h-5 w-5 text-green-500" />
127
+ <X v-else class="mx-auto h-5 w-5 text-red-500" />
128
+ </td>
129
+ <td class="px-6 py-3 whitespace-nowrap text-center">
130
+ <Check v-if="permission.viewer" class="mx-auto h-5 w-5 text-green-500" />
131
+ <X v-else class="mx-auto h-5 w-5 text-red-500" />
132
+ </td>
133
+ </tr>
134
+ </tbody>
135
+ </table>
136
+ </div>
137
+ <div class="p-4 border-t border-gray-200 dark:border-gray-700">
138
+ <button class="inline-flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md shadow-sm text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 transition-colors w-full">
139
+ {{$t('settings.permissions.customizePermissions')}}
140
+ </button>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </template>
146
+
147
+ <script setup lang="ts">
148
+ import { ref, computed, onMounted, getCurrentInstance } from 'vue';
149
+ import { Plus, Search, Check, X } from 'lucide-vue-next';
150
+ import { useI18n } from 'vue-i18n';
151
+ import { useHead } from '@vueuse/head';
152
+ import { notify } from '@opengis/core'
153
+ const { t } = useI18n();
154
+ useHead({
155
+ title: () => t('settings.permissions.title') + ' | CMS'
156
+ });
157
+
158
+
159
+ const fetchPermissions = async () => {
160
+ let userId = null;
161
+
162
+ try {
163
+ const response = await fetch('/user');
164
+ const data = await response.json();
165
+ userId = data?.user?.uid;
166
+ } catch (error) {
167
+ console.error(error);
168
+ notify({
169
+ title: t('settings.permissions.error'),
170
+ message: t('settings.permissions.errorMessage'),
171
+ type: 'error',
172
+ });
173
+ }
174
+
175
+ try {
176
+ const response = await fetch(`/api/cms-permissions/${userId}`);
177
+ const data = await response.json();
178
+ permissions.value = data?.permissions || [];
179
+ } catch (error) {
180
+ console.error(error);
181
+ notify({
182
+ title: t('settings.permissions.error'),
183
+ message: t('settings.permissions.errorMessage'),
184
+ type: 'error',
185
+ });
186
+ }
187
+ };
188
+
189
+ const searchQuery = ref('');
190
+ interface User {
191
+ id: string;
192
+ name: string;
193
+ email: string;
194
+ role: string;
195
+ lastActive: string;
196
+ }
197
+
198
+ const users = ref<User[]>([
199
+ // { id: '1', name: 'Alex Morgan', email: 'alex@example.com', role: 'admin', lastActive: t('table.time.hoursAgo', { count: 2 }) },
200
+ // { id: '2', name: 'Jamie Chen', email: 'jamie@example.com', role: 'editor', lastActive: t('table.time.hoursAgo', { count: 4 }) },
201
+ // { id: '3', name: 'Sam Wilson', email: 'sam@example.com', role: 'author', lastActive: t('table.time.dayAgo') },
202
+ // { id: '4', name: 'Riley Thompson', email: 'riley@example.com', role: 'viewer', lastActive: t('table.time.daysAgo', { count: 3 }) },
203
+ // { id: '5', name: 'Jordan Lee', email: 'jordan@example.com', role: 'editor', lastActive: t('table.time.weekAgo', { count: 1 }) },
204
+ ]);
205
+
206
+ const permissions = ref([
207
+ { name: t('settings.permissions.createContent'), admin: true, editor: true, author: true, viewer: false },
208
+ { name: t('settings.permissions.editContent'), admin: true, editor: true, author: true, viewer: false },
209
+ { name: t('settings.permissions.deleteContent'), admin: true, editor: true, author: false, viewer: false },
210
+ { name: t('settings.permissions.publishContent'), admin: true, editor: true, author: false, viewer: false },
211
+ { name: t('settings.permissions.manageMedia'), admin: true, editor: true, author: true, viewer: false },
212
+ { name: t('settings.permissions.manageUsers'), admin: true, editor: false, author: false, viewer: false },
213
+ { name: t('settings.permissions.manageSettings'), admin: true, editor: false, author: false, viewer: false },
214
+ ]);
215
+
216
+ const filteredUsers = computed(() => {
217
+ if (!searchQuery.value) return users.value;
218
+ return users.value.filter(user => user.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || user.email.toLowerCase().includes(searchQuery.value.toLowerCase()));
219
+ });
220
+
221
+ function getRoleBadgeColor(role: string) {
222
+ switch (role) {
223
+ case 'admin':
224
+ return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
225
+ case 'editor':
226
+ return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
227
+ case 'author':
228
+ return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
229
+ case 'viewer':
230
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
231
+ default:
232
+ return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
233
+ }
234
+ }
235
+
236
+ onMounted(fetchPermissions);
237
+ </script>