@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,310 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from "vue";
3
+ import { useI18n } from 'vue-i18n';
4
+ const { t } = useI18n();
5
+ import {
6
+ Edit,
7
+ Eye,
8
+ Trash2,
9
+ CheckCircle,
10
+ XCircle,
11
+ Pencil,
12
+ Layers,
13
+ File,
14
+ } from "lucide-vue-next";
15
+
16
+
17
+ const selectedRow = ref<any>(null);
18
+
19
+ const props = defineProps<{
20
+ rows: any[];
21
+ // columns: Array<{ name: string; title?: string }>;
22
+ query?: string;
23
+ filterFn?: (row: any, query: string) => boolean;
24
+ onEdit?: (row: any) => void;
25
+ onView?: (row: any) => void;
26
+ onDelete?: (row: any) => void;
27
+ columns: any[];
28
+ onMore?: (row: any) => void;
29
+ customRender?: Record<string, (value: any, row: any) => any>;
30
+ }>();
31
+
32
+ const emit = defineEmits<{
33
+ (e: "edit", row: any): void;
34
+ (e: "view", row: any): void;
35
+ (e: "delete", row: any): void;
36
+ (e: "more", row: any): void;
37
+ }>();
38
+
39
+ const filteredRows = computed(() => {
40
+ if (!props.query || !props.filterFn) return props.rows;
41
+ return props.rows.filter((row) => props.filterFn!(row, props.query!));
42
+ });
43
+
44
+ const formatDate = (dateStr: string) => {
45
+ if (!dateStr) return "";
46
+ const date = new Date(dateStr);
47
+ if (isNaN(date.getTime())) return '';
48
+ return date.toISOString().slice(0, 16).replace('T', ' ');
49
+ };
50
+
51
+ const formatDateTime = (dateStr: string) => {
52
+ if (!dateStr) return "";
53
+ const date = new Date(dateStr);
54
+ if (isNaN(date.getTime())) return '';
55
+
56
+ const hasTime = dateStr.includes('T') || dateStr.includes(' ') || dateStr.includes(':');
57
+
58
+ if (hasTime) {
59
+ return date.toLocaleString('uk-UA', { dateStyle: 'medium', timeStyle: 'medium' });
60
+ } else {
61
+ return date.toLocaleDateString('uk-UA', { dateStyle: 'medium' });
62
+ }
63
+ };
64
+
65
+ const getStatusBadge = (status: string) => {
66
+ switch (status) {
67
+ case "published":
68
+ return {
69
+ icon: CheckCircle,
70
+ text: t("common.status.published"),
71
+ classes:
72
+ "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 border-green-200 dark:border-green-700",
73
+ };
74
+ case "draft":
75
+ return {
76
+ icon: Edit,
77
+ text: t("common.status.draft"),
78
+ classes:
79
+ "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-200 border-yellow-200 dark:border-yellow-700",
80
+ };
81
+ case "archived":
82
+ return {
83
+ icon: XCircle,
84
+ text: t("common.status.archived"),
85
+ classes:
86
+ "bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300 border-gray-200 dark:border-gray-700",
87
+ };
88
+ default:
89
+ return null;
90
+ }
91
+ };
92
+
93
+ const handleAction = (action: string, row: any) => {
94
+ switch (action) {
95
+ case "edit":
96
+ emit("edit", row);
97
+ if (props.onEdit) props.onEdit(row);
98
+ break;
99
+ case "view":
100
+ emit("view", row);
101
+ if (props.onView) props.onView(row);
102
+ break;
103
+ case "delete":
104
+ emit("delete", row);
105
+ break;
106
+ case "more":
107
+ emit("more", row);
108
+ if (props.onMore) props.onMore(row);
109
+ break;
110
+ }
111
+ };
112
+
113
+ const handleRowClick = (row: any) => {
114
+ handleAction("edit", row);
115
+ };
116
+
117
+ const timeAgo = (date: string) => {
118
+ const now = new Date();
119
+ const past = new Date(date);
120
+ const diffMs = now - past;
121
+ const diffSec = Math.floor(diffMs / 1000);
122
+ const diffMin = Math.floor(diffSec / 60);
123
+ const diffHour = Math.floor(diffMin / 60);
124
+ const diffDay = Number(Math.floor(diffHour / 24));
125
+ const diffWeek = Number(Math.floor(diffDay / 7));
126
+
127
+ if (diffWeek >= 2) return t('table.time.weeksAgo', { count: diffWeek });
128
+ if (diffWeek === 1) return t('table.time.weekAgo');
129
+ if (diffDay >= 2) return t('table.time.daysAgo', { count: diffDay });
130
+ if (diffDay === 1) return t('table.time.dayAgo');
131
+ if (diffHour >= 2) return t('table.time.hoursAgo', { count: diffHour });
132
+ if (diffHour === 1) return t('table.time.hourAgo');
133
+ if (diffMin >= 2) return t('table.time.minutesAgo', { count: diffMin });
134
+ if (diffMin === 1) return t('table.time.minuteAgo');
135
+ return t('table.time.justNow');
136
+ }
137
+
138
+ </script>
139
+
140
+ <template>
141
+ <div class="text-card-foreground border-0 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm" >
142
+ <div class="p-0">
143
+ <div class="table-scroll overflow-x-auto">
144
+ <div class="relative w-full overflow-auto max-h-[calc(100vh-270px)]">
145
+ <table class="w-full caption-bottom text-sm" >
146
+ <thead>
147
+ <tr class="border-b border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-700/50 hover:bg-slate-50/80 dark:hover:bg-slate-700/80 transition-colors">
148
+ <th
149
+ v-for="col in columns"
150
+ :key="col.name"
151
+ class="h-10 px-4 text-left align-middle font-semibold text-slate-700 dark:text-slate-300 max-w-80"
152
+ :class="{
153
+ 'min-w-[200px]': ['title','name'].includes(col.name ),
154
+ 'min-w-[150px]': col.name === 'slug',
155
+ 'min-w-[250px]': col.name === 'description',
156
+ }"
157
+ >
158
+ <div class="flex items-center gap-2">
159
+ <component :is="col.icon" v-if="col.icon" class="w-5 h-5" />
160
+ {{ col.label || col.title || col.ua ||col.name }}
161
+ </div>
162
+ </th>
163
+ <th
164
+ class="h-10 text-center px-4 font-semibold text-slate-700 dark:text-slate-300 max-w-32"
165
+ >
166
+ {{ $t('table.actions') }}
167
+ </th>
168
+ </tr>
169
+ </thead>
170
+ <tbody>
171
+ <tr
172
+ v-for="row in filteredRows" :key="row.id || row._id || row.name"
173
+ class="transition-all duration-200 hover:bg-slate-50/60 dark:hover:bg-slate-700/60"
174
+ >
175
+ <td
176
+ v-for="(col, colIdx) in columns"
177
+ :key="col.name"
178
+ class="py-2 px-4 align-middle max-w-80"
179
+ :class="{
180
+ 'font-medium text-slate-800 dark:text-white cursor-pointer': colIdx === 0,
181
+ 'text-slate-600 dark:text-slate-400 font-mono text-sm': colIdx !== 0,
182
+ }"
183
+ @click="colIdx === 0 ? handleRowClick(row) : undefined"
184
+ >
185
+ <template v-if="customRender && customRender[col.name]">
186
+ <component :is="customRender[col.name](row[col.name], row)" />
187
+ </template>
188
+ <template v-if="[
189
+ 'published_at',
190
+ 'publish_at',
191
+ 'created_at',
192
+ 'date',
193
+ ].includes(col.name) && row[col.name]
194
+ ">
195
+ <!-- <span class="whitespace-nowrap">
196
+ {{ formatDate(row[col.name]) || '--' }}
197
+ </span> -->
198
+ <span class="whitespace-nowrap">
199
+ {{ formatDateTime(row[col.name]) }}
200
+ </span>
201
+ </template>
202
+ <template v-else-if="[
203
+ 'updated_at',
204
+ 'lastModified',
205
+ 'last_edit',
206
+ ].includes(col.name)
207
+ ">
208
+ <span class="whitespace-nowrap">
209
+ {{ timeAgo(row[col.name]) || '--' }}
210
+ </span>
211
+ </template>
212
+ <template v-else-if="col.name === 'views'">
213
+ <div class="flex items-center">
214
+ <Eye class="w-4 h-4 mr-1" />
215
+ {{ row[col.name]?.toLocaleString?.() ?? row[col.name] }}
216
+ </div>
217
+ </template>
218
+ <template v-else-if="['entries', 'fields'].includes(col.name)">
219
+ <div class="inline-flex whitespace-nowrap items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300">
220
+ {{ row[col.name] || "-" }}
221
+ </div>
222
+ </template>
223
+ <template v-else-if="col.name === 'type'">
224
+ <div class="flex items-center">
225
+ <div
226
+ class="whitespace-nowrap inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors border-transparent hover:bg-secondary/80"
227
+ :class="row[col.name] === 'pages' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' : 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'"
228
+ >
229
+ {{ row[col.name] || "-" }}
230
+ </div>
231
+ </div>
232
+ </template>
233
+ <template v-else-if="col.name === 'name'">
234
+ <div
235
+ class="flex items-center gap-3"
236
+ >
237
+ <File v-if="row.type === 'single'" class="shrink-0 lucide lucide-page w-5 h-5 text-blue-600" />
238
+ <Layers v-else class="shrink-0 lucide lucide-layers w-5 h-5 text-blue-600" />
239
+ {{ row[col.name] }}
240
+ </div>
241
+ </template>
242
+ <template v-else-if="col.name === 'status'">
243
+ <div
244
+ v-if="getStatusBadge(row[col.name])"
245
+ class="whitespace-nowrap rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors hover:bg-secondary/80 flex items-center w-fit bg-green-100 text-green-800 border-green-200
246
+ dark:bg-green-900/30 dark:text-green-300 dark:border-green-700"
247
+ :class="getStatusBadge(row[col.name])?.classes"
248
+ >
249
+ <component
250
+ :is="getStatusBadge(row[col.name])?.icon"
251
+ class="w-3 h-3 mr-1"
252
+ />
253
+ {{ getStatusBadge(row[col.name])?.text }}
254
+ </div>
255
+ </template>
256
+ <template v-else-if="col.name === 'slug'">
257
+ <div class="flex items-center gap-3">
258
+ {{ row[col.name] ? '/' + row[col.name] : "-" }}
259
+ </div>
260
+ </template>
261
+ <template v-else-if="col.name === 'color'">
262
+ <span
263
+ class="w-4 h-4 rounded-full block ml-2"
264
+ :class="'bg-[' + row[col.name] + ']'"
265
+ >
266
+ </span>
267
+ </template>
268
+ <template v-else-if="col.name === 'enabled'">
269
+ <span class="whitespace-nowrap rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors hover:bg-secondary/80 flex items-center w-fit" :class="row[col.name] ? 'bg-green-100 text-green-800 border-green-200' : 'bg-red-100 text-red-800 border-red-200'">{{ row[col.name] ? 'Доступ' : 'Заборонено' }}</span>
270
+ </template>
271
+ <template v-else>
272
+ <div class="flex items-center gap-3">
273
+ <p class="line-clamp-2">
274
+ {{ row[col.name] || "-" }}
275
+ </p>
276
+ </div>
277
+ </template>
278
+ </td>
279
+ <td class="flex justify-end py-2 px-4 align-middle max-w-40">
280
+ <div class="flex items-center justify-center gap-1">
281
+ <button
282
+ @click.stop="handleAction('edit', row)"
283
+ class="inline-flex items-center justify-center whitespace-nowrap font-medium border rounded-md text-xs h-8 w-8 p-0 border-slate-300 bg-white transition-all duration-200 shadow-sm
284
+ dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600 dark:hover:text-slate-200
285
+ hover:border-blue-300 hover:bg-blue-50 hover:text-blue-600"
286
+ title="Edit"
287
+ >
288
+ <Pencil class="text-slate-800 dark:text-slate-200 w-4 h-4 group-hover:text-green-600 dark:group-hover:text-blue-50" />
289
+ </button>
290
+ <button
291
+ @click.stop="handleAction('delete', row)"
292
+ class="inline-flex items-center justify-center whitespace-nowrap font-medium border rounded-md text-xs h-8 w-8 p-0 border-slate-300 bg-white transition-all duration-200 shadow-sm
293
+ dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600 dark:hover:text-slate-200
294
+ hover:border-blue-300 hover:bg-blue-50 hover:text-blue-600"
295
+ title="Preview"
296
+ >
297
+ <Trash2 class="w-4 h-4 text-slate-800 dark:text-slate-200 group-hover:text-red-600 dark:group-hover:text-blue-50" />
298
+ </button>
299
+ <slot name="actions" :row="row" />
300
+ </div>
301
+ </td>
302
+ </tr>
303
+ </tbody>
304
+ </table>
305
+
306
+ </div>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </template>
File without changes
@@ -0,0 +1,118 @@
1
+ <template>
2
+ <div
3
+ class="flex items-center justify-between px-6 py-4 border-t border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800"
4
+ >
5
+ <div class="flex justify-between flex-1 sm:hidden">
6
+ <button
7
+ class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
8
+ >
9
+ {{ $t("common.pagination.previous") }}
10
+ </button>
11
+ <button
12
+ class="relative inline-flex items-center px-4 py-2 ml-3 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
13
+ >
14
+ {{ $t("common.pagination.next") }}
15
+ </button>
16
+ </div>
17
+ <div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
18
+ <div>
19
+ <p class="text-sm text-slate-600 dark:text-slate-400">
20
+ {{ $t("common.pagination.showing") }}
21
+ <span class="font-medium text-slate-800 dark:text-slate-100">{{ count }}</span>
22
+ {{ $t("common.pagination.of") }}
23
+ <span class="font-medium text-slate-800 dark:text-slate-100">{{ total }}</span>
24
+ {{ $t("common.pagination.results") }}
25
+ </p>
26
+ </div>
27
+ <div>
28
+ <nav
29
+ class="relative z-0 inline-flex space-x-2 rounded-md shadow-sm"
30
+ aria-label="Pagination"
31
+ >
32
+ <button
33
+ @click="page = page > 1 ? page - 1 : 1"
34
+ :disabled="page === 1"
35
+ 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
36
+ disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed
37
+ hover:text-accent-foreground hover:bg-slate-50
38
+ dark:bg-slate-700 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-600"
39
+ >
40
+ <span class="sr-only">{{ $t("common.pagination.previous") }}</span>
41
+ <ChevronLeft class="w-5 h-5" />
42
+ </button>
43
+ <div class="space-x-1">
44
+ <template v-for="(p, idx) in pagesToShow" :key="p + '-' + idx">
45
+ <span v-if="p === 'ellipsis'" class="px-2 py-2 text-gray-400"
46
+ >...</span
47
+ >
48
+ <button
49
+ v-else
50
+ @click="page = p"
51
+ class="inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors h-8 w-8 p-0 text-sm rounded-md"
52
+ :class="{
53
+ 'border shadow-sm hover:text-accent-foreground bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-300 hover:border-blue-600 hover:bg-slate-50 dark:hover:bg-slate-600': p !== page,
54
+ 'shadow bg-blue-600 hover:bg-blue-700 text-white border-blue-600': p === page
55
+ }"
56
+ >
57
+ {{ p }}
58
+ </button>
59
+ </template>
60
+ </div>
61
+ <button
62
+ @click="
63
+ page =
64
+ page < Math.ceil(total / limit)
65
+ ? page + 1
66
+ : Math.ceil(total / limit)
67
+ "
68
+ :disabled="page === Math.ceil(total / limit)"
69
+ 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
70
+ disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed
71
+ hover:text-accent-foreground hover:bg-slate-50
72
+ dark:bg-slate-700 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-600"
73
+ >
74
+ <span class="sr-only">{{ $t("common.pagination.next") }}</span>
75
+ <ChevronRight class="w-5 h-5" />
76
+ </button>
77
+ </nav>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ import { ChevronLeft, ChevronRight } from "lucide-vue-next";
85
+ import { computed } from "vue";
86
+
87
+ const {
88
+ limit,
89
+ count,
90
+ total,
91
+ } = defineProps<{
92
+ total: number;
93
+ count: number;
94
+ limit: number;
95
+ }>();
96
+
97
+ const page = defineModel<number>("page");
98
+
99
+ const totalPages = computed(() => Math.ceil(total / limit));
100
+
101
+ const pagesToShow = computed(() => {
102
+ const maxPages = 4;
103
+ const pages: (number | "ellipsis")[] = [];
104
+ if (totalPages.value <= maxPages) {
105
+ for (let i = 1; i <= totalPages.value; i++) pages.push(i);
106
+ return pages;
107
+ }
108
+ // Always show first and last
109
+ const left = Math.max(2, page.value - 2);
110
+ const right = Math.min(totalPages.value - 1, page.value + 2);
111
+ pages.push(1);
112
+ if (left > 2) pages.push("ellipsis");
113
+ for (let i = left; i <= right; i++) pages.push(i);
114
+ if (right < totalPages.value - 1) pages.push("ellipsis");
115
+ pages.push(totalPages.value);
116
+ return pages;
117
+ });
118
+ </script>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <div>
3
+ <div class="flex justify-end mb-4">
4
+ <a
5
+ :href="`${previewUrl}/uk/${preview_path}${slug}${draftKey ? `?draftKey=${draftKey}` : ''}`"
6
+ target="_blank"
7
+ class="inline-flex items-center gap-2 px-3 py-1.5 text-black-600 bg-white border border-blue-200 rounded hover:border-blue-300 hover:bg-blue-50 transition-colors duration-200 font-normal cursor-pointer"
8
+ >
9
+ Переглянути на сайті
10
+ <ExternalLink class="w-4 h-4" />
11
+ </a>
12
+ </div>
13
+ <iframe
14
+ v-if="previewUrl && preview_path"
15
+ :src="`${previewUrl}/uk/${preview_path}${slug}?hideHeader=true&hideFooter=true`"
16
+ class="w-full h-[80vh]"
17
+ ></iframe>
18
+ <iframe
19
+ v-else-if="previewUrl"
20
+ :src="`${previewUrl}/uk/${slug}?hideHeader=true&hideFooter=true`"
21
+ class="w-full h-[80vh]"
22
+ ></iframe>
23
+ <div
24
+ v-else
25
+ class="w-full h-[80vh] flex items-center justify-center text-gray-500"
26
+ >
27
+ Loading preview...
28
+ </div>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup>
33
+ import { ref, onMounted } from "vue";
34
+ import { ExternalLink } from "lucide-vue-next";
35
+
36
+ const props = defineProps({
37
+ preview_path: {
38
+ type: String,
39
+ default: "",
40
+ },
41
+ slug: {
42
+ type: String,
43
+ default: "",
44
+ },
45
+ });
46
+
47
+ const previewUrl = ref("");
48
+ const draftKey = ref("");
49
+
50
+ const fetchSettings = async () => {
51
+ try {
52
+ const response = await fetch("/api/settings");
53
+ const data = await response.json();
54
+ previewUrl.value =
55
+ data?.settings?.site_info?.previewUrl || "http://ip.local.softpro.ua";
56
+ } catch (error) {
57
+ console.error("Failed to fetch settings:", error);
58
+ previewUrl.value = "http://ip.local.softpro.ua"; // fallback
59
+ }
60
+ };
61
+ const fetchUser = async () => {
62
+ try {
63
+ const response = await fetch("/user");
64
+ const data = await response.json();
65
+ draftKey.value = data?.user?.draftKey;
66
+ } catch (error) {
67
+ console.error("Failed to fetch user:", error);
68
+ }
69
+ };
70
+
71
+ onMounted(() => {
72
+ fetchSettings();
73
+ fetchUser();
74
+ });
75
+ </script>
@@ -0,0 +1,21 @@
1
+ import { ref, watch } from 'vue';
2
+
3
+ export type CollectionView = 'list' | 'grid';
4
+
5
+ const saved = localStorage.getItem('collectionView');
6
+ const collectionView = ref<CollectionView>(saved === 'list' || saved === 'grid' ? saved as CollectionView : 'list');
7
+
8
+ watch(collectionView, (newView) => {
9
+ localStorage.setItem('collectionView', newView);
10
+ });
11
+
12
+ function toggleCollectionView() {
13
+ collectionView.value = collectionView.value === 'grid' ? 'list' : 'grid';
14
+ }
15
+
16
+ export function useCollectionView() {
17
+ return {
18
+ collectionView,
19
+ toggleCollectionView,
20
+ };
21
+ }
@@ -0,0 +1,26 @@
1
+ import { ref, watch, type Ref } from 'vue';
2
+
3
+ /**
4
+ * Debounce composable for Vue 3 Composition API
5
+ * @param value - The value to debounce
6
+ * @param delay - The debounce delay in ms (default: 300)
7
+ * @returns debouncedValue
8
+ */
9
+ export function useDebounce<T>(value: Ref<T>, delay = 300) {
10
+ const debouncedValue = ref(value.value) as Ref<T>;
11
+
12
+ let timeout: ReturnType<typeof setTimeout>;
13
+
14
+ watch(
15
+ value,
16
+ (newValue) => {
17
+ clearTimeout(timeout);
18
+ timeout = setTimeout(() => {
19
+ debouncedValue.value = newValue;
20
+ }, delay);
21
+ },
22
+ { immediate: true }
23
+ );
24
+
25
+ return debouncedValue;
26
+ }
@@ -0,0 +1,28 @@
1
+ export async function useMonaco() {
2
+ if (window.monaco) return window.monaco;
3
+
4
+ const CDN = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min';
5
+
6
+ if (!window.require) {
7
+ window.require = { paths: { vs: `${CDN}/vs` } };
8
+ await new Promise<void>((res, rej) => {
9
+ const s = document.createElement('script');
10
+ s.src = `${CDN}/vs/loader.min.js`;
11
+ s.onload = () => res();
12
+ s.onerror = rej;
13
+ document.head.appendChild(s);
14
+ });
15
+ }
16
+
17
+ return new Promise<typeof monaco>((resolve) => {
18
+ window.require(['vs/editor/editor.main'], () => resolve(window.monaco));
19
+ });
20
+ }
21
+
22
+ // Додаємо типи для TypeScript
23
+ declare global {
24
+ interface Window {
25
+ monaco: any;
26
+ require: any;
27
+ }
28
+ }
@@ -0,0 +1,40 @@
1
+ import { ref, watch } from 'vue';
2
+
3
+ type Theme = 'light' | 'dark';
4
+ type Color = 'blue' | 'green' | 'purple';
5
+
6
+ export function useTheme() {
7
+ const theme = ref<Theme>(
8
+ localStorage.getItem('theme') === 'light' ? 'light' : 'dark'
9
+ );
10
+
11
+ const color = ref<Color>(
12
+ localStorage.getItem('color') as Color || 'blue'
13
+ );
14
+
15
+ watch(theme, (newTheme) => {
16
+ document.documentElement.classList.toggle('dark');
17
+ localStorage.setItem('theme', newTheme);
18
+ });
19
+
20
+ watch(color, (newColor) => {
21
+ document.documentElement.classList.remove('theme-green', 'theme-purple', 'theme-blue');
22
+ document.documentElement.classList.add(`theme-${newColor}`);
23
+ localStorage.setItem('color', newColor);
24
+ });
25
+
26
+ const toggleTheme = () => {
27
+ theme.value = theme.value === 'light' ? 'dark' : 'light';
28
+ };
29
+
30
+ const toggleColor = (newColor: Color) => {
31
+ color.value = newColor;
32
+ };
33
+
34
+ return {
35
+ theme,
36
+ toggleTheme,
37
+ color,
38
+ toggleColor
39
+ };
40
+ }
@@ -0,0 +1 @@
1
+ {"slug":"test-slug","title":"test-slug123466578"}