@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,176 @@
1
+ <template>
2
+ <div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6 pb-6">
3
+ <div v-for="collection in collections" :key="collection.id" class="rounded-xl text-card-foreground shadow-lg border-0 bg-white dark:bg-slate-800 backdrop-blur-sm hover:shadow-xl transition-all duration-200 transform hover:bg-slate-50">
4
+ <div class="flex items-center gap-2 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">
5
+ <File v-if="collection.type === 'single'" class="shrink-0 lucide lucide-page w-5 h-5 text-blue-600" />
6
+ <Layers v-else class="shrink-0 lucide lucide-layers w-5 h-5 text-blue-600" />
7
+ <p class="line-clamp-1 tracking-tight text-lg font-semibold text-slate-800 dark:text-slate-100 vertical-center">
8
+ {{ collection.title }}
9
+ </p>
10
+ <div
11
+ v-if="getStatusBadge(collection.status)"
12
+ class="whitespace-nowrap rounded-md border ml-auto 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
13
+ dark:bg-green-900/30 dark:text-green-300 dark:border-green-700"
14
+ :class="getStatusBadge(collection.status)?.classes"
15
+ >
16
+ <component
17
+ :is="getStatusBadge(collection.status)?.icon"
18
+ class="w-3 h-3 mr-1"
19
+ />
20
+ {{ getStatusBadge(collection.status)?.text }}
21
+ </div>
22
+ </div>
23
+ <div class="w-full flex flex-col gap-2 p-6">
24
+ <p class="text-slate-600 dark:text-slate-300 mb-4 line-clamp-2" :title="collection.description">
25
+ {{ collection.description !== null ? collection.description : 'No description' }}
26
+ </p>
27
+ <div class="flex items-center justify-between gap-2">
28
+ <p class="text-sm text-slate-600 dark:text-slate-400">{{ $t("collections.entries") }}:</p>
29
+ <p class="font-semibold text-slate-800 dark:text-slate-100">
30
+ {{ collection.entries }}
31
+ </p>
32
+ </div>
33
+ <div class="flex items-center justify-between gap-2">
34
+ <p class="text-sm text-slate-500 dark:text-slate-400">{{ $t("collections.fields") }}:</p>
35
+ <p class="font-semibold text-slate-800 dark:text-slate-100">
36
+ {{ collection.fields }}
37
+ </p>
38
+ </div>
39
+ <div class="flex items-center justify-between gap-2">
40
+ <p class="text-sm text-slate-500 dark:text-slate-400">{{ $t("collections.lastModified") }}:</p>
41
+ <p class="text-sm text-slate-600 dark:text-slate-300">
42
+ {{ timeAgo(collection.last_edit) }}
43
+ </p>
44
+ </div>
45
+ <div class="flex items-center justify-between gap-2 mt-6">
46
+ <slot name="actions" :collection="collection" :handleAction="handleAction">
47
+ <!-- Default buttons -->
48
+ <button class="inline-flex gap-1 items-center justify-center whitespace-nowrap font-medium border rounded-md text-xs h-8 w-4/5 p-0 border-slate-300 bg-white transition-all duration-200 shadow-sm group
49
+ dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600 dark:hover:border-slate-500 dark:hover:text-slate-200
50
+ hover:bg-green-50 hover:border-green-300"
51
+ @click.stop="handleAction('edit', collection)"
52
+ >
53
+ <Edit class="text-slate-800 dark:text-slate-200 w-4 h-4 group-hover:text-green-600 dark:group-hover:text-blue-50" />
54
+ <span class="text-slate-800 dark:text-slate-200 group-hover:text-green-600 dark:group-hover:text-blue-50">{{ $t("common.actions.edit") }}</span>
55
+ </button>
56
+ <button :class="collection.id === 'pages' ? 'disabled' : ''" 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 group
57
+ dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600 dark:hover:border-slate-500 dark:hover:text-slate-200
58
+ hover:bg-red-50 hover:border-red-300"
59
+ @click.stop="collection.id === 'pages' ? null : handleAction('delete', collection)"
60
+ >
61
+ <Trash2 class="text-slate-800 dark:text-slate-200 w-4 h-4 group-hover:text-red-600 dark:group-hover:text-blue-50" />
62
+ </button>
63
+ <button :class="collection.type === 'single' ? 'disabled cursor-not-allowed' : ''" 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 group
64
+ dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600 dark:hover:border-slate-500 dark:hover:text-slate-200
65
+ hover:bg-blue-50 hover:border-blue-300"
66
+ @click.stop="collection.type === 'single' ? null : handleAction('view', collection)"
67
+ >
68
+ <List class="text-slate-800 dark:text-slate-200 w-4 h-4 group-hover:text-blue-600 dark:group-hover:text-blue-50" />
69
+ </button>
70
+ </slot>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </template>
76
+
77
+ <script setup lang="ts">
78
+ import {
79
+ Edit,
80
+ Trash2,
81
+ List,
82
+ Layers,
83
+ CheckCircle,
84
+ XCircle,
85
+ HelpCircle,
86
+ File,
87
+ } from "lucide-vue-next";
88
+ import { ref } from "vue";
89
+
90
+ const confirm = ref(false);
91
+ const selectedRow = ref<any>(null);
92
+
93
+ const props = defineProps<{
94
+ collections: any[];
95
+ onEdit: (row: any) => void;
96
+ onView: (row: any) => void;
97
+ onDelete: (row: any) => void;
98
+ }>();
99
+
100
+ const emit = defineEmits<{
101
+ (e: "edit", row: any): void;
102
+ (e: "view", row: any): void;
103
+ (e: "delete", row: any): void;
104
+ }>();
105
+
106
+ const handleAction = (action: string, row: any) => {
107
+ switch (action) {
108
+ case "edit":
109
+ emit("edit", row);
110
+ if (props.onEdit) props.onEdit(row);
111
+ break;
112
+ case "view":
113
+ emit("view", row);
114
+ if (props.onView) props.onView(row);
115
+ break;
116
+ case "delete":
117
+ emit("delete", row);
118
+ if (props.onDelete) props.onDelete(row);
119
+ break;
120
+ }
121
+ };
122
+
123
+ const timeAgo = (date: string) => {
124
+ const now = new Date();
125
+ const past = new Date(date);
126
+ const diffMs = now.getTime() - past.getTime();
127
+ const diffSec = Math.floor(diffMs / 1000);
128
+ const diffMin = Math.floor(diffSec / 60);
129
+ const diffHour = Math.floor(diffMin / 60);
130
+ const diffDay = Math.floor(diffHour / 24);
131
+ const diffWeek = Math.floor(diffDay / 7);
132
+
133
+ if (diffWeek >= 2) return `${diffWeek} weeks ago`;
134
+ if (diffWeek === 1) return `1 week ago`;
135
+ if (diffDay >= 2) return `${diffDay} days ago`;
136
+ if (diffDay === 1) return `1 day ago`;
137
+ if (diffHour >= 2) return `${diffHour} hours ago`;
138
+ if (diffHour === 1) return `1 hour ago`;
139
+ if (diffMin >= 2) return `${diffMin} minutes ago`;
140
+ if (diffMin === 1) return `1 minute ago`;
141
+ return `just now`;
142
+ }
143
+
144
+ const getStatusBadge = (status: string) => {
145
+ switch (status) {
146
+ case "published":
147
+ return {
148
+ icon: CheckCircle,
149
+ text: "Published",
150
+ classes:
151
+ "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-200 border-green-200 dark:border-green-700",
152
+ };
153
+ case "draft":
154
+ return {
155
+ icon: Edit,
156
+ text: "Draft",
157
+ classes:
158
+ "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-200 border-yellow-200 dark:border-yellow-700",
159
+ };
160
+ case "archived":
161
+ return {
162
+ icon: XCircle,
163
+ text: "Archived",
164
+ classes:
165
+ "bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300 border-gray-200 dark:border-gray-700",
166
+ };
167
+ default:
168
+ return {
169
+ icon: HelpCircle,
170
+ text: "--",
171
+ classes:
172
+ "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-200 border-yellow-200 dark:border-yellow-700",
173
+ };
174
+ }
175
+ };
176
+ </script>
@@ -0,0 +1,75 @@
1
+ ```vue
2
+ <template>
3
+ <div
4
+ ref="blockRef"
5
+ class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-4"
6
+ >
7
+ <div class="flex items-center justify-between mb-4">
8
+ <div class="flex items-center">
9
+ <button
10
+ class="handle p-1 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400"
11
+ >
12
+ <GripVertical class="h-5 w-5" />
13
+ </button>
14
+ <span class="ml-2 text-sm font-medium text-gray-700 dark:text-gray-300">
15
+ {{ block.type.charAt(0).toUpperCase() + block.type.slice(1) }}
16
+ </span>
17
+ </div>
18
+ <div class="flex items-center space-x-2">
19
+ <button
20
+ @click="$emit('edit', block.id)"
21
+ class="p-1 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-400"
22
+ >
23
+ <Settings class="h-5 w-5" />
24
+ </button>
25
+ <button
26
+ @click="$emit('remove', block.id)"
27
+ class="p-1 text-red-400 hover:text-red-600 dark:text-red-500 dark:hover:text-red-400"
28
+ >
29
+ <X class="h-5 w-5" />
30
+ </button>
31
+ </div>
32
+ </div>
33
+
34
+ <component :is="blockComponent" v-bind="blockProps" />
35
+ </div>
36
+ </template>
37
+
38
+ <script setup lang="ts">
39
+ import { computed } from 'vue';
40
+ import { GripVertical, Settings, X } from 'lucide-vue-next';
41
+
42
+ const props = defineProps<{
43
+ block: {
44
+ id: string;
45
+ type: string;
46
+ content: any;
47
+ };
48
+ }>();
49
+
50
+ defineEmits<{
51
+ (e: 'remove', id: string): void;
52
+ (e: 'edit', id: string): void;
53
+ }>();
54
+
55
+ // This would be expanded to include all block type components
56
+ const blockComponent = computed(() => {
57
+ switch (props.block.type) {
58
+ case 'heading':
59
+ return 'HeadingBlock';
60
+ case 'paragraph':
61
+ return 'ParagraphBlock';
62
+ // Add more cases for other block types
63
+ default:
64
+ return 'div';
65
+ }
66
+ });
67
+
68
+ const blockProps = computed(() => {
69
+ return {
70
+ content: props.block.content,
71
+ // Add any other props needed by specific block components
72
+ };
73
+ });
74
+ </script>
75
+ ```
@@ -0,0 +1,156 @@
1
+ <template>
2
+ <div class="max-w-4xl">
3
+ <div
4
+ @submit.prevent="handleSubmit"
5
+ class="p-6 space-y-6 dark:bg-gray-800 dark:border-gray-700"
6
+ >
7
+ <div class="space-y-6">
8
+ <div v-for="col in columns" :key="col.name">
9
+ <div class="mb-2 text-sm font-medium text-gray-900 dark:text-white">
10
+ {{ col.title }}
11
+ </div>
12
+ <component
13
+ :is="getComponent(col?.type || '')"
14
+ v-model="formData[col.name]"
15
+ :label="col?.title"
16
+ :id="col?.name"
17
+ v-bind="col"
18
+ />
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { ref, watch } from "vue";
27
+ // import { Save } from "lucide-vue-next";
28
+ import { useRouter, useRoute } from "vue-router";
29
+ //import v3core from "@opengis/v3-core";
30
+ // import v3core from "../../../../v3-core/src/misc/import-file.ts";
31
+ import MonacoEditor from "../form-components/MonacoEditor.vue";
32
+ import Slug from "../form-components/vs-form-slug.vue";
33
+
34
+ const formComponents = {
35
+ //...v3core.formComponents,
36
+ VsFormMonacoEditor: MonacoEditor,
37
+ VsFormSlug: Slug,
38
+ };
39
+
40
+ const route = useRoute();
41
+
42
+ interface Column {
43
+ name: string;
44
+ label: string;
45
+ title?: string;
46
+ type?: string;
47
+ required?: boolean;
48
+ placeholder?: string;
49
+ rows?: number;
50
+ options?: string[];
51
+ component: any;
52
+ }
53
+
54
+ const props = defineProps<{
55
+ columns: Column[];
56
+ initialData?: Record<string, any>;
57
+ }>();
58
+
59
+ const emit = defineEmits<{
60
+ (e: "submit", data: Record<string, any>): void;
61
+ (e: "cancel"): void;
62
+ (e: "update", data: Record<string, any>): void;
63
+ }>();
64
+
65
+ function getDefaultFormData(
66
+ columns: Column[],
67
+ initialData?: Record<string, any>
68
+ ) {
69
+ const data: Record<string, any> = {};
70
+ columns.forEach((col) => {
71
+ data[col.name] =
72
+ initialData && initialData[col.name] !== undefined
73
+ ? initialData[col.name]
74
+ : "";
75
+ });
76
+ return data;
77
+ }
78
+
79
+ const formData = ref(
80
+ getDefaultFormData(props.columns ?? [], props.initialData)
81
+ );
82
+ const previousFormData = ref({ ...formData.value });
83
+
84
+ const updateTimeout = ref<number | null>(null);
85
+ const handleFormUpdate = () => {
86
+ if (updateTimeout.value) {
87
+ clearTimeout(updateTimeout.value);
88
+ }
89
+
90
+ updateTimeout.value = window.setTimeout(() => {
91
+ const hasChanges = Object.keys(formData.value).some(
92
+ (key) => formData.value[key] !== previousFormData.value[key]
93
+ );
94
+
95
+ if (hasChanges) {
96
+ emit("update", { ...formData.value });
97
+ previousFormData.value = { ...formData.value };
98
+ }
99
+ }, 300);
100
+ };
101
+
102
+ watch(
103
+ formData,
104
+ () => {
105
+ handleFormUpdate();
106
+ },
107
+ { deep: true }
108
+ );
109
+
110
+ const router = useRouter();
111
+
112
+ watch(
113
+ () => [props.columns, props.initialData],
114
+ ([columns, initialData]) => {
115
+ if (columns && initialData) {
116
+ formData.value = getDefaultFormData(columns as Column[], initialData);
117
+ }
118
+ },
119
+ { immediate: true }
120
+ );
121
+
122
+ const getComponent = (type: string) => {
123
+ if (type === "yes_no") return formComponents["VsFormSwitcher"];
124
+ if (type === "date") return formComponents["VsFormDatepicker"];
125
+
126
+ if (!type) return null;
127
+ const capitalizedType = type.charAt(0).toUpperCase() + type.slice(1);
128
+ return formComponents["VsForm" + capitalizedType];
129
+ };
130
+
131
+ const handleCancel = () => {
132
+ router.back();
133
+ };
134
+ const handleSubmit = async () => {
135
+ if (props.initialData?.id) {
136
+ // Редагування існуючої статті
137
+ await fetch(`/api/cms/${route.meta.type}/${props.initialData.id}`, {
138
+ method: "PUT",
139
+ headers: {
140
+ "Content-Type": "application/json",
141
+ },
142
+ body: JSON.stringify({ ...formData.value }),
143
+ });
144
+ } else {
145
+ // Додавання нової статті
146
+ await fetch(`/api/cms/${route.meta.type}`, {
147
+ method: "POST",
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ },
151
+ body: JSON.stringify({ ...formData.value }),
152
+ });
153
+ }
154
+ router.back();
155
+ };
156
+ </script>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <div class="p-4 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
3
+ <div class="flex items-center justify-between">
4
+ <div class="flex-1">
5
+ <div class="flex items-center space-x-3 mb-2">
6
+ <div class="inline-flex items-center rounded-md border px-2.5 py-0.5 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-xs bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-300">
7
+ {{ item.type }}
8
+ </div>
9
+ <div :class="getStatusClasses(item.status)">
10
+ {{ item.status || '--' }}
11
+ </div>
12
+ </div>
13
+ <h3 class="font-semibold text-slate-800 dark:text-slate-100 mb-1">
14
+ {{ item.title || '--' }}
15
+ </h3>
16
+ <div class="flex items-center space-x-4 text-sm text-slate-500 dark:text-slate-400">
17
+ <span class="flex items-center">
18
+ <Users class="w-4 h-4 mr-1" />
19
+ {{ item.author || '--' }}
20
+ </span>
21
+ <span class="flex items-center">
22
+ <Calendar class="w-4 h-4 mr-1" />
23
+ {{ formatDate(item.updated_at) || '--.--.--' }}
24
+ </span>
25
+ <span class="flex items-center">
26
+ <Eye class="w-4 h-4 mr-1" />
27
+ {{ item.views || '--' }} {{ $t("dashboard.views") }}
28
+ </span>
29
+ </div>
30
+ </div>
31
+ <div class="flex items-center space-x-2">
32
+ <router-link
33
+ :to="`/collections/${item.object_id}/${item.content_id}`"
34
+ 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
35
+ hover:text-accent-foreground hover:bg-slate-50
36
+ dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600"
37
+ >
38
+ <SquarePen class="w-4 h-4 dark:text-blue-50" />
39
+ </router-link>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import { Users, Calendar, Eye, SquarePen } from 'lucide-vue-next';
47
+
48
+ interface ContentItem {
49
+ id: number;
50
+ type: 'Article' | 'Post';
51
+ status: 'published' | 'draft' | 'review';
52
+ title: string;
53
+ author: string;
54
+ updated_at: string;
55
+ views: number;
56
+ }
57
+
58
+ interface Props {
59
+ item: ContentItem;
60
+ }
61
+
62
+ defineProps<Props>();
63
+
64
+ const getStatusClasses = (status: string) => {
65
+ const statusClasses = {
66
+ published: 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent hover:bg-secondary/80 bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300',
67
+ draft: 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent hover:bg-secondary/80 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300',
68
+ review: 'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent hover:bg-secondary/80 bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300'
69
+ };
70
+ return statusClasses[status as keyof typeof statusClasses] || statusClasses.draft;
71
+ };
72
+
73
+ const formatDate = (dateStr: string) => {
74
+ if (!dateStr) return '--.--.--';
75
+ const date = new Date(dateStr);
76
+ return new Intl.DateTimeFormat('en-US', {
77
+ year: 'numeric',
78
+ month: 'short',
79
+ day: 'numeric'
80
+ }).format(date);
81
+ };
82
+ </script>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <div class="flex items-center justify-between">
3
+ <div>
4
+ <div class="flex items-center gap-2">
5
+ <h1 class="text-4xl font-bold text-slate-800 dark:text-slate-100 mb-2">
6
+ {{ $t('dashboard.title') }}
7
+ </h1>
8
+ <a :href="`https://cms.opengis.info/${locale}/get-started/app-overview`" target="_blank" :title="$t('guide.appOverview')">
9
+ <HelpCircle class="w-5 h-5" />
10
+ </a>
11
+ </div>
12
+ <div class="text-lg text-slate-600 dark:text-slate-300">
13
+ {{ $t('dashboard.description') }}
14
+ </div>
15
+ </div>
16
+ <div>
17
+ <router-link
18
+ to="/settings/collections"
19
+ class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 shadow h-9 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white"
20
+ >
21
+ <Plus class="w-4 h-4 mr-2" />
22
+ {{ $t('dashboard.newCollection') }}
23
+ </router-link>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { Plus, HelpCircle } from 'lucide-vue-next';
30
+ import { useI18n } from 'vue-i18n';
31
+
32
+ const { locale } = useI18n();
33
+ </script>
@@ -0,0 +1,84 @@
1
+ <template>
2
+ <div class="rounded-xl text-card-foreground shadow-lg border-0 bg-white dark:bg-slate-800 backdrop-blur-sm">
3
+ <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">
4
+ <h3 class="tracking-tight text-lg font-semibold text-slate-800 dark:text-slate-100 flex items-center">
5
+ <Zap class="w-5 h-5 mr-2" />
6
+ {{ $t("dashboard.quickActions") }}
7
+ </h3>
8
+ </div>
9
+ <div class="p-4">
10
+ <div class="grid grid-cols-2 gap-3">
11
+ <button
12
+ v-for="action in actions"
13
+ :key="action.id"
14
+ class="p-3 rounded-lg border border-slate-200 transition-all duration-200 text-left group
15
+ hover:bg-slate-50 hover:shadow-md
16
+ dark:border-slate-600 bg-white dark:bg-slate-700 dark:hover:bg-slate-600"
17
+ @click="handleAction(action?.link)"
18
+ >
19
+ <div class="p-2 rounded-lg bg-blue-50 dark:bg-blue-900/20 mb-2 w-fit">
20
+ <component :is="action?.icon" :class="`w-4 h-4 ${action?.iconColor}`" />
21
+ </div>
22
+ <h3 class="font-medium text-slate-800 dark:text-slate-100 text-sm mb-1 group-hover:text-slate-900 dark:group-hover:text-slate-50">
23
+ {{ action?.title }}
24
+ </h3>
25
+ <p class="text-xs text-slate-500 dark:text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300">
26
+ {{ action?.description }}
27
+ </p>
28
+ </button>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import { FileText, Users, Image as ImageIcon, MessageCircle, Zap } from 'lucide-vue-next';
36
+ import { useRouter } from 'vue-router';
37
+ import { useI18n } from 'vue-i18n';
38
+
39
+ const router = useRouter();
40
+ const { t } = useI18n();
41
+
42
+ interface Action {
43
+ id: string;
44
+ title: string;
45
+ description: string;
46
+ icon: any;
47
+ iconColor: string;
48
+ link?: string;
49
+ }
50
+
51
+ const actions: Action[] = [
52
+ {
53
+ id: 'create-article',
54
+ title: t('dashboard.createArticle'),
55
+ description: t('dashboard.createArticleDescription'),
56
+ icon: FileText,
57
+ iconColor: 'text-blue-500',
58
+ link: '/collections/pages/create'
59
+ },
60
+ {
61
+ id: 'upload-media',
62
+ title: t('dashboard.uploadMedia'),
63
+ description: t('dashboard.uploadMediaDescription'),
64
+ icon: ImageIcon,
65
+ iconColor: 'text-purple-500',
66
+ link: '/media'
67
+ },
68
+ {
69
+ id: 'manage-users',
70
+ title: t('dashboard.manageUsers'),
71
+ description: t('dashboard.manageUsersDescription'),
72
+ icon: Users,
73
+ iconColor: 'text-green-500',
74
+ link: '/settings/users'
75
+ },
76
+
77
+ ];
78
+
79
+ const handleAction = (actionLink: string) => {
80
+ if (actionLink) {
81
+ router.push(actionLink);
82
+ }
83
+ };
84
+ </script>
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <div class="lg:col-span-2">
3
+ <div class="rounded-xl text-card-foreground shadow-lg border-0 bg-white dark:bg-slate-800 backdrop-blur-sm">
4
+ <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">
5
+ <div class="flex items-center justify-between">
6
+ <h3 class="tracking-tight text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
7
+ <Activity class="w-5 h-5 mr-2" />
8
+ {{ $t('dashboard.recentContent') }}
9
+ </h3>
10
+ <button @click="goTo('/settings/collections')" class="inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border shadow-sm hover:text-accent-foreground h-8 rounded-md px-3 text-xs bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-600">
11
+ {{ $t('dashboard.viewAll') }}
12
+ </button>
13
+ </div>
14
+ </div>
15
+ <div class="p-0">
16
+ <div class="divide-y divide-slate-100 dark:divide-slate-700">
17
+ <ContentItem
18
+ v-for="item in content"
19
+ :key="item.id"
20
+ :item="item"
21
+ />
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { Activity } from 'lucide-vue-next';
30
+ import ContentItem from './ContentItem.vue';
31
+ import { useRouter } from 'vue-router';
32
+
33
+ const router = useRouter();
34
+
35
+ const goTo = (path: string) => {
36
+ router.push(path);
37
+ };
38
+
39
+ interface ContentItem {
40
+ id: number;
41
+ type: 'Article' | 'Post';
42
+ status: 'published' | 'draft' | 'review';
43
+ title: string;
44
+ author: string;
45
+ updated_at: string;
46
+ views: number;
47
+ }
48
+
49
+ interface Props {
50
+ content: ContentItem[];
51
+ }
52
+
53
+ defineProps<Props>();
54
+ </script>