@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,29 @@
1
+ <template>
2
+ <div v-if="uploads?.length" class="fixed bottom-4 right-4 w-96 bg-white shadow-lg rounded-lg p-4 z-50">
3
+ <div class="font-bold mb-2">{{ $t("media.uploadingFiles") }}</div>
4
+ <div v-for="upload in uploads" :key="upload.id" class="mb-3">
5
+ <div class="flex justify-between items-center mb-1">
6
+ <span class="truncate w-2/3">{{ upload?.name }}</span>
7
+ <span v-if="upload.status === 'success'" class="text-green-600">✔</span>
8
+ <span v-else-if="upload.status === 'error'" class="text-red-600">✖</span>
9
+ <span v-else class="text-blue-600">{{ upload?.progress }}%</span>
10
+ </div>
11
+ <div class="w-full bg-gray-200 rounded h-2">
12
+ <div
13
+ class="bg-blue-500 h-2 rounded"
14
+ :style="{ width: upload?.progress + '%' }"
15
+ ></div>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup>
22
+
23
+ const props = defineProps({
24
+ uploads: {
25
+ type: Array,
26
+ required: true,
27
+ },
28
+ });
29
+ </script>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <div class="flex items-center space-x-1">
3
+ <button
4
+ class="flex gap-x-1 items-center px-2 py-1 rounded text-sm font-medium transition-all duration-200 text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20"
5
+ @click="$emit('navigate', [])"
6
+ >
7
+ <IconHome class="w-3 h-3" />
8
+ {{ $t("media.mediaLibrary") }}
9
+ </button>
10
+ <ChevronRight
11
+ v-if="currentPath.length > 0"
12
+ class="w-3 h-3 text-slate-400 dark:text-slate-500"
13
+ />
14
+ <button
15
+ v-for="(segment, index) in currentPath"
16
+ :key="index"
17
+ class="flex gap-x-1 items-center px-2 py-1 rounded text-sm font-medium transition-all duration-200 text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/20"
18
+ @click="$emit('navigate', currentPath.slice(0, index + 1))"
19
+ >
20
+ <Folder class="w-3 h-3" />
21
+ {{ segment }}
22
+ <ChevronRight
23
+ v-if="index < currentPath.length - 1"
24
+ class="w-3 h-3 text-slate-400 dark:text-slate-500"
25
+ />
26
+ </button>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup>
31
+ import { ChevronRight, Folder } from "lucide-vue-next";
32
+ import IconHome from "../icons/icon-home.vue";
33
+
34
+ defineProps({
35
+ currentPath: {
36
+ type: Array,
37
+ required: true,
38
+ },
39
+ });
40
+
41
+ defineEmits(["navigate"]);
42
+ </script>
@@ -0,0 +1,59 @@
1
+ <template>
2
+ <VsModal teleport="#modal" :visible="modelValue" :title="$t('media.createFolder')" @close="$emit('update:modelValue', false)">
3
+ <VsForm :schema="scheme" v-model="formValues" v-model:form="form" />
4
+ <template #footer>
5
+ <div class="flex justify-end p-[20px] gap-[10px] border-t w-full">
6
+ <button
7
+ @click="$emit('update:modelValue', false)"
8
+ class="inline-flex items-center px-3 py-2 text-sm text-black duration-300 border border-gray-200 rounded-lg gap-x-2 whitespace-nowrap hover:bg-gray-100"
9
+ >
10
+ {{ $t("common.actions.cancel") }}
11
+ </button>
12
+ <button
13
+ @click="handleCreate"
14
+ class="py-2 px-3 inline-flex items-center gap-x-2 text-sm whitespace-nowrap text-white bg-blue-500 rounded-lg !border-gray-200 hover:bg-blue-700 duration-300"
15
+ >
16
+ {{ $t("common.actions.create") }}
17
+ </button>
18
+ </div>
19
+ </template>
20
+ </VsModal>
21
+ </template>
22
+
23
+ <script setup>
24
+ import { ref } from "vue";
25
+ import { useI18n } from "vue-i18n";
26
+ import VsForm from "@opengis/form";
27
+ import { VsModal } from "@opengis/core";
28
+ import { notify } from '@opengis/core';
29
+ const { t } = useI18n();
30
+
31
+ const modelValue = defineModel();
32
+
33
+ const emit = defineEmits(["update:modelValue", "create"]);
34
+
35
+ const formValues = ref({});
36
+ const form = ref({});
37
+
38
+ const scheme = {
39
+ name: {
40
+ name: "name",
41
+ label: t("media.newFolder"),
42
+ type: "Text",
43
+ },
44
+ };
45
+
46
+ const handleCreate = async () => {
47
+ let result = await form.value.validate();
48
+ if (result) {
49
+ notify({
50
+ type: "warning",
51
+ title: t("common.actions.warning"),
52
+ message: t("media.createFolderFailed"),
53
+ });
54
+ return;
55
+ }
56
+ emit("create", { ...formValues.value });
57
+ formValues.value = {};
58
+ };
59
+ </script>
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <div class="h-[fit-content] w-80 flex flex-col flex-shrink-0 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-lg">
3
+ <div
4
+ v-if="!file"
5
+ class="flex flex-col items-center justify-center h-64 text-muted-foreground"
6
+ >
7
+ <p>{{ $t("media.selectFile") }}</p>
8
+ </div>
9
+ <template v-else>
10
+ <div class="p-4 border-b border-slate-200 dark:border-slate-700">
11
+ <h3 class="font-semibold text-slate-800 dark:text-slate-100">{{ $t("media.fileDetails") }}</h3>
12
+ </div>
13
+ <div class="p-4 space-y-4">
14
+ <div class="aspect-video flex items-center justify-center bg-slate-100 dark:bg-slate-700 rounded-lg overflow-hidden relative">
15
+ <img
16
+ v-if="file?.filetype === 'image' && file?.filepath"
17
+ :src="file?.preview"
18
+ :alt="file?.alt || file?.filename"
19
+ class="w-full h-full object-cover"
20
+ >
21
+ <FileText v-else class="w-full h-4/5 object-cover" />
22
+ <div class="absolute top-3 right-3">
23
+ <TypeTag :type="file?.filetype" />
24
+ </div>
25
+ </div>
26
+ <div class="space-y-3">
27
+ <div>
28
+ <label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.name") }}</label>
29
+ <p class="text-slate-800 dark:text-slate-100 break-all">{{ file?.filename }}</p>
30
+ </div>
31
+ <div>
32
+ <label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.size") }}</label>
33
+ <p class="text-slate-800 dark:text-slate-100">{{ formatFileSize(file?.filesize) }}</p>
34
+ </div>
35
+ <div v-if="file?.dimensions">
36
+ <label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.dimensions") }}</label>
37
+ <p class="text-slate-800 dark:text-slate-100">1920x1080</p>
38
+ </div>
39
+ <div>
40
+ <label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.uploadDate") }}</label>
41
+ <p class="text-slate-800 dark:text-slate-100 flex items-center">
42
+ <Calendar class="w-4 h-4 mr-2" />
43
+ {{ file?.created_at ? formatDate(file?.created_at) : "--" }}
44
+ </p>
45
+ </div>
46
+ <div v-if="file?.author">
47
+ <label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.author") }}</label>
48
+ <p class="text-slate-800 dark:text-slate-100">{{ file?.author }}</p>
49
+ </div>
50
+ <div v-if="file?.description">
51
+ <label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.description") }}</label>
52
+ <p class="text-slate-800 dark:text-slate-100">{{ file?.description }}</p>
53
+ </div>
54
+ <div v-if="file?.tags">
55
+ <label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.tags") }}</label>
56
+ <div class="flex flex-wrap gap-1 mt-1">
57
+ <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-foreground text-xs">
58
+ banner
59
+ </div>
60
+ <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-foreground text-xs">
61
+ hero
62
+ </div>
63
+ <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-foreground text-xs">
64
+ homepage
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ <div class="space-y-2 pt-4 border-t border-slate-200 dark:border-slate-700">
70
+ <a :href="file?.filepath" download class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 shadow h-9 px-4 py-2 w-full bg-blue-600 hover:bg-blue-700 text-white">
71
+ <Download class="w-4 h-4 mr-2" />
72
+ {{ $t("media.download") }}
73
+ </a>
74
+ <button
75
+ class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 border shadow-sm hover:text-accent-foreground h-9 px-4 py-2 w-full bg-slate-600 hover:bg-slate-700 text-white"
76
+ @click="copyFilePath"
77
+ >
78
+ <Copy class="w-4 h-4 mr-2" />
79
+ {{ $t("media.copyPath") }}
80
+ </button>
81
+ <button
82
+ class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 border shadow-sm hover:text-accent-foreground h-9 px-4 py-2 w-full bg-red-500 hover:bg-red-600 text-white"
83
+ @click="openConfirm"
84
+ >
85
+ <Trash2 class="w-4 h-4 mr-2" />
86
+ {{ $t("media.delete") }}
87
+ </button>
88
+
89
+ </div>
90
+ </div>
91
+ </template>
92
+ </div>
93
+ </template>
94
+
95
+ <script setup>
96
+ import IconMore from "../icons/icon-more.vue";
97
+ import { Download, FileText, Share2, Pencil, Calendar, Trash2, Copy } from "lucide-vue-next";
98
+ import { formatFileSize, formatDate } from "../../props/media";
99
+ import TypeTag from "./TypeTag.vue";
100
+ import { confirm, notify } from '@opengis/core';
101
+ import { useI18n } from "vue-i18n";
102
+ const { t } = useI18n();
103
+
104
+ const emit = defineEmits(["delete-file"]);
105
+
106
+
107
+ const props = defineProps({
108
+ file: {
109
+ type: Object,
110
+ default: null,
111
+ },
112
+ });
113
+
114
+ const openConfirm = () => {
115
+ confirm({
116
+ title: t("builder.deleteTitle"),
117
+ message: t("builder.deleteObject"),
118
+ type: 'error',
119
+ onConfirm: () => {
120
+ handleDeleteFile();
121
+ },
122
+ })
123
+ };
124
+
125
+ const handleDeleteFile = () => {
126
+ emit("delete-file", props.file);
127
+ };
128
+
129
+ const copyFilePath = async () => {
130
+ if (props.file?.filepath) {
131
+ try {
132
+ await navigator.clipboard.writeText(props.file.filepath);
133
+ notify({
134
+ type: "success",
135
+ title: t("common.actions.success"),
136
+ message: t("toast.copied"),
137
+ });
138
+ } catch (err) {
139
+ console.error('Failed to copy file path:', err);
140
+ notify({
141
+ type: "error",
142
+ title: t("common.actions.error"),
143
+ message: t("media.copyPathFailed"),
144
+ });
145
+ }
146
+ }
147
+ };
148
+ </script>
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <div
3
+ class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 max-h-[calc(100%-130px)] overflow-y-auto px-2 pt-2 pb-5"
4
+ >
5
+ <div
6
+ v-if="files?.length === 0"
7
+ class="flex flex-col items-center justify-center h-64 text-muted-foreground col-span-full"
8
+ >
9
+ <File class="w-12 h-12 mb-2" />
10
+ <p>{{ $t('noFilesInFolder') }}</p>
11
+ </div>
12
+ <template v-for="file in sortedFiles" :key="file?.url">
13
+ <div
14
+ v-if="file?.type === 'dir'"
15
+ class="p-6 text-center rounded-xl h-[fit-content] relative 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:scale-105 cursor-pointer"
16
+ @click="$emit('file-click', file)"
17
+ >
18
+ <div
19
+ v-if="isDeleteButton"
20
+ @click.stop="selectedFolder = file; openConfirm"
21
+ class="absolute top-4 right-4 p-1 border border-gray-300 dark:border-slate-700 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-slate-700 group"
22
+ >
23
+ <Trash2 class="w-4 h-4 text-gray-500 group-hover:text-red-500" />
24
+ </div>
25
+ <Folder class="w-16 h-16 mx-auto mb-4 text-blue-600" />
26
+ <h3 class="truncate font-medium text-slate-800 dark:text-slate-100 mb-2">{{ file?.name}}</h3>
27
+ <p class="text-sm text-slate-500 dark:text-slate-400">{{ file?.count }} {{ $t('media.files') }}</p>
28
+ </div>
29
+ <div
30
+ v-else-if="file?.type === 'file'"
31
+ class="h-[fit-content] 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:scale-105 group cursor-pointer"
32
+ :class="{
33
+ 'bg-accent text-accent-foreground ring-2 ring-primary':
34
+ selectedFile?.url === file?.url || (selectedFiles && selectedFiles.some(f => f.id === file.id)),
35
+ }"
36
+ @click="$emit('file-click', file)"
37
+ >
38
+
39
+ <div class="p-0">
40
+ <div class="aspect-video bg-slate-100 dark:bg-slate-700 rounded-t-lg overflow-hidden relative">
41
+ <div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-700 dark:to-slate-800">
42
+ <div
43
+ v-if="file?.filetype === 'image' && (file?.filepath || file?.url)"
44
+ class="relative w-full"
45
+ >
46
+ <img
47
+ :src="file?.preview"
48
+ :alt="file?.alt || file?.filename"
49
+ class="object-cover w-full h-full rounded text-xs"
50
+ />
51
+ </div>
52
+ <FileText v-else class="w-12 h-12 text-gray-500" />
53
+ </div>
54
+ </div>
55
+ <div class="p-4">
56
+ <div class="flex items-center justify-between mb-2">
57
+ <h3 class="font-medium text-slate-800 dark:text-slate-100 truncate flex-1 mr-2">{{ file.filename }}</h3>
58
+ <TypeTag :type="file?.filetype" />
59
+ </div>
60
+ <div class="space-y-1 text-sm text-slate-500 dark:text-slate-400">
61
+ <div class="flex justify-between">
62
+ <span>{{ $t('media.size') }}:</span>
63
+ <span>{{ formatFileSize(file.filesize) }}</span>
64
+ </div>
65
+ <div class="flex justify-between">
66
+ <span>{{ $t('media.uploaded') }}:</span>
67
+ <span>{{ formatDate(file.created_at) }}</span>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </template>
74
+ </div>
75
+ </template>
76
+
77
+ <script setup>
78
+ import { File, Folder, FileText, Trash2 } from "lucide-vue-next";
79
+ import { formatFileSize } from "../../props/media";
80
+ import TypeTag from "./TypeTag.vue";
81
+ import { ref, onMounted, onUpdated, defineEmits } from "vue";
82
+ import { confirm } from '@opengis/core';
83
+ import { useI18n } from "vue-i18n";
84
+ const { t } = useI18n();
85
+
86
+ const emit = defineEmits(["delete-file", "file-click", "delete-folder"]);
87
+
88
+ const props = defineProps({
89
+ files: {
90
+ type: Array,
91
+ required: true,
92
+ },
93
+ selectedFile: {
94
+ type: Object,
95
+ default: null,
96
+ },
97
+ selectedFiles: {
98
+ type: Array,
99
+ default: () => [],
100
+ },
101
+ isDeleteButton: {
102
+ type: Boolean,
103
+ default: false,
104
+ },
105
+ });
106
+
107
+ const sortedFiles = ref([]);
108
+ const selectedFolder = ref(null);
109
+
110
+ const openConfirm = () => {
111
+ confirm({
112
+ title: t("builder.deleteTitle"),
113
+ message: t("builder.deleteObject"),
114
+ type: 'error',
115
+ onConfirm: () => {
116
+ handleDeleteFile();
117
+ },
118
+ })
119
+ };
120
+
121
+ const handleDeleteFile = () => {
122
+ if (selectedFolder.value.type === 'file') {
123
+ emit("delete-file", selectedFile.value);
124
+ } else {
125
+ emit("delete-folder", selectedFolder.value);
126
+ }
127
+ };
128
+
129
+
130
+ const formatDate = (dateStr) => {
131
+ if (!dateStr) return "";
132
+ const date = new Date(dateStr);
133
+ if (isNaN(date.getTime())) return '';
134
+ return date.toISOString().slice(0, 10);
135
+ };
136
+
137
+ const sortFiles = () => {
138
+ sortedFiles.value = props.files.sort((a, b) => {
139
+ if (a.type === 'dir' && b.type !== 'dir') return -1;
140
+ if (a.type !== 'dir' && b.type === 'dir') return 1;
141
+ return 0;
142
+ });
143
+ }
144
+
145
+ onMounted(() => sortFiles());
146
+
147
+ onUpdated(() => sortFiles());
148
+ </script>
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <div>
3
+ <div
4
+ v-if="files.length === 0"
5
+ class="flex flex-col items-center justify-center h-64 text-muted-foreground"
6
+ >
7
+ <File class="w-12 h-12 mb-2" />
8
+ <p>{{ $t("media.noFilesInFolder") }}</p>
9
+ </div>
10
+
11
+ <table v-else class="w-full text-sm caption-bottom">
12
+ <thead class="[&_tr]:border-b">
13
+ <tr
14
+ class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
15
+ >
16
+ <th
17
+ class="h-12 px-4 text-left align-middle font-medium text-muted-foreground w-[300px]"
18
+ >
19
+ {{ $t("media.name") }}
20
+ </th>
21
+ <th
22
+ class="h-12 px-4 font-medium text-left align-middle text-muted-foreground"
23
+ >
24
+ {{ $t("media.type") }}
25
+ </th>
26
+ <th
27
+ class="h-12 px-4 font-medium text-left align-middle text-muted-foreground"
28
+ >
29
+ {{ $t("media.size") }}
30
+ </th>
31
+ <th
32
+ class="hidden h-12 px-4 font-medium text-left align-middle text-muted-foreground md:table-cell"
33
+ >
34
+ {{ $t("media.updatedAt") }}
35
+ </th>
36
+ <th></th>
37
+ </tr>
38
+ </thead>
39
+ <tbody class="[&_tr:last-child]:border-0">
40
+ <tr
41
+ v-for="file in files"
42
+ :key="file.id"
43
+ class="border-b transition-colors hover:bg-blue-200/50 data-[state=selected]:bg-muted cursor-pointer"
44
+ :class="{
45
+ 'bg-blue-200': selectedFile?.id === file.id && selectedFile?.type === file.type ||
46
+ (selectedFiles && selectedFiles.some(f => f.id === file.id && f.type === file.type))
47
+ }"
48
+ @click="$emit('file-click', file)"
49
+ >
50
+ <td class="p-4 font-medium align-middle">
51
+ <div class="flex items-center gap-2">
52
+ <IconFolder
53
+ v-if="file.type === 'dir'"
54
+ class="w-5 h-5 text-blue-500"
55
+ />
56
+ <div
57
+ v-else-if="file?.filetype === 'image' && file?.filepath"
58
+ class="relative w-8 h-8"
59
+ >
60
+ <img
61
+ :src="file?.preview"
62
+ :alt="file?.alt || file?.filename"
63
+ class="object-cover w-full h-full rounded"
64
+ />
65
+ </div>
66
+ <IconImage
67
+ v-else-if="file?.filetype === 'image'"
68
+ class="w-5 h-5 text-green-500"
69
+ />
70
+ <File v-else class="w-5 h-5 text-gray-500" />
71
+ <span class="truncate max-w-[200px]">{{
72
+ file?.filename || file?.name
73
+ }}</span>
74
+ </div>
75
+ </td>
76
+ <td class="p-4 align-middle">
77
+ {{
78
+ file?.type === "dir"
79
+ ? $t("media.folder")
80
+ : file?.filetype?.charAt(0)?.toUpperCase() +
81
+ file?.filetype?.slice(1)
82
+ }}
83
+ </td>
84
+ <td class="p-4 align-middle">
85
+ {{ file?.type === "dir" ? "--" : formatFileSize(file?.filesize) }}
86
+ </td>
87
+ <td class="hidden p-4 align-middle md:table-cell">
88
+ {{ file?.updated_at ? formatDate(file?.updated_at) : "--" }}
89
+ </td>
90
+ <td v-if="file.type === 'dir'">
91
+ <Trash2 class="w-4 h-4 text-red-500 cursor-pointer" @click.stop=" openConfirm" />
92
+ </td>
93
+ </tr>
94
+ </tbody>
95
+ </table>
96
+ </div>
97
+ </template>
98
+
99
+ <script setup>
100
+ import IconFolder from "../icons/icon-folder.vue";
101
+ import IconImage from "../icons/icon-image.vue";
102
+ import { formatFileSize, formatDate } from "../../props/media";
103
+ import { Trash2 } from "lucide-vue-next";
104
+ import { ref } from "vue";
105
+ import { confirm } from '@opengis/core';
106
+ import { useI18n } from "vue-i18n";
107
+ const { t } = useI18n();
108
+
109
+ const props = defineProps({
110
+ files: {
111
+ type: Array,
112
+ required: true,
113
+ },
114
+ selectedFile: {
115
+ type: Object,
116
+ default: null,
117
+ },
118
+ selectedFiles: {
119
+ type: Array,
120
+ default: () => [],
121
+ },
122
+ });
123
+
124
+ const selectedFile = ref(null);
125
+
126
+ const emit = defineEmits(["file-click", "delete-folder"]);
127
+
128
+ const openConfirm = () => {
129
+
130
+ confirm({
131
+ title: t("builder.deleteTitle"),
132
+ message: t("builder.deleteObject"),
133
+ type: 'error',
134
+ onConfirm: () => {
135
+ handleDeleteFile();
136
+ },
137
+ })
138
+ };
139
+
140
+ const handleDeleteFile = () => {
141
+ emit("delete-folder", selectedFile.value);
142
+ };
143
+
144
+ const handleSelectedFile = (file) => {
145
+ selectedFile.value = file;
146
+ };
147
+
148
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <div class="flex items-center gap-3">
3
+ <button
4
+ class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 border shadow-sm hover:text-accent-foreground h-9 px-4 py-2 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"
5
+ @click="$emit('create-folder')"
6
+ >
7
+ <FolderPlus class="w-4 h-4 mr-2" />
8
+ {{ $t("media.newFolder") }}
9
+ </button>
10
+ <button
11
+ class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium disabled:pointer-events-none disabled:opacity-50 h-9 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white shadow-md hover:shadow-lg transition-all duration-200 transform hover:scale-105"
12
+ @click="$refs.fileInputRef.click()"
13
+ >
14
+ <input
15
+ type="file"
16
+ @change="$emit('file-change', $event)"
17
+ ref="fileInputRef"
18
+ class="hidden"
19
+ multiple
20
+ />
21
+ <Plus class="w-4 h-4 mr-2" />
22
+ {{ $t("media.uploadMedia") }}
23
+ </button>
24
+ </div>
25
+ </template>
26
+
27
+ <script setup>
28
+ import { Plus, FolderPlus } from "lucide-vue-next";
29
+
30
+ defineProps({
31
+ modelValue: {
32
+ type: String,
33
+ required: true,
34
+ },
35
+ });
36
+
37
+ defineEmits(["update:modelValue", "create-folder", "file-change"]);
38
+ </script>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <div class="inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors shadow-lg backdrop-blur-sm"
3
+ :class="typeStyles.get(type)"
4
+ >
5
+ {{ type }}
6
+ </div>
7
+ </template>
8
+
9
+ <script setup>
10
+ const typeStyles = new Map([
11
+ ['document', 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700'],
12
+ ['image', 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700'],
13
+ ['video', 'bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700'],
14
+ ])
15
+
16
+
17
+ const props = defineProps({
18
+ type: {
19
+ type: String,
20
+ required: true,
21
+ },
22
+ })
23
+ </script>