@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.
- package/package.json +2 -2
- package/src/App.css +52 -0
- package/src/App.vue +62 -0
- package/src/assets/image.png +0 -0
- package/src/assets/main.css +3 -0
- package/src/assets/tailwind-3.4.17.js +113 -0
- package/src/components/LanguageSwitcher.vue +73 -0
- package/src/components/SettingsCard.vue +40 -0
- package/src/components/builder/CreateForm.vue +128 -0
- package/src/components/builder/formTypeSchema.js +145 -0
- package/src/components/builder/tabs/index.ts +9 -0
- package/src/components/builder/tabs/vs-builder-edit.vue +133 -0
- package/src/components/builder/tabs/vs-builder-monaco.vue +29 -0
- package/src/components/builder/tabs/vs-builder-preview.vue +39 -0
- package/src/components/builder/vs-builder-datatable-controls.vue +138 -0
- package/src/components/builder/vs-builder-datatable-form.vue +80 -0
- package/src/components/builder/vs-builder-datatable.vue +191 -0
- package/src/components/builder/vs-builder-list-item.vue +110 -0
- package/src/components/collections/CollectionsBreadcrumb.vue +52 -0
- package/src/components/collections/CollectionsGrid.vue +176 -0
- package/src/components/collections/ContentBlock.vue +75 -0
- package/src/components/collections/formWrapper.vue +156 -0
- package/src/components/dashboard/ContentItem.vue +82 -0
- package/src/components/dashboard/DashboardHeader.vue +33 -0
- package/src/components/dashboard/QuickActions.vue +84 -0
- package/src/components/dashboard/RecentContent.vue +54 -0
- package/src/components/dashboard/StatCard.vue +63 -0
- package/src/components/dashboard/StatsGrid.vue +28 -0
- package/src/components/form-components/MonacoEditor.vue +104 -0
- package/src/components/form-components/VsFormMeta.vue +40 -0
- package/src/components/form-components/VsFormTags.vue +150 -0
- package/src/components/form-components/custom-datatable/vs-form-custom-datatable-add.vue +84 -0
- package/src/components/form-components/custom-datatable/vs-form-custom-datatable-controls.vue +106 -0
- package/src/components/form-components/index.js +23 -0
- package/src/components/form-components/reference/vs-form-reference-add.vue +92 -0
- package/src/components/form-components/reference/vs-form-reference-controls.vue +101 -0
- package/src/components/form-components/reference-list/referenceOptionList.js +78 -0
- package/src/components/form-components/reference-list/vs-form-reference-add.vue +145 -0
- package/src/components/form-components/reference-list/vs-form-reference-choce.vue +39 -0
- package/src/components/form-components/reference-list/vs-form-reference-controls.vue +110 -0
- package/src/components/form-components/reference-skeleton/about-skeleton.vue +37 -0
- package/src/components/form-components/reference-skeleton/banner-skeleton.vue +29 -0
- package/src/components/form-components/reference-skeleton/body-skeleton.vue +56 -0
- package/src/components/form-components/reference-skeleton/cards-skeleton.vue +47 -0
- package/src/components/form-components/reference-skeleton/documents-skeleton.vue +64 -0
- package/src/components/form-components/reference-skeleton/faq-skeleton.vue +64 -0
- package/src/components/form-components/reference-skeleton/form-skeleton.vue +41 -0
- package/src/components/form-components/reference-skeleton/index.js +36 -0
- package/src/components/form-components/reference-skeleton/infoLine-skeleton.vue +37 -0
- package/src/components/form-components/reference-skeleton/news-skeleton.vue +54 -0
- package/src/components/form-components/reference-skeleton/slider-skeleton.vue +41 -0
- package/src/components/form-components/reference-skeleton/tabs-skeleton.vue +40 -0
- package/src/components/form-components/reference-skeleton/team-skeleton.vue +103 -0
- package/src/components/form-components/reference-skeleton/usefulLinks-skeleton.vue +52 -0
- package/src/components/form-components/reference-skeleton/video-skeleton.vue +36 -0
- package/src/components/form-components/testReferenceTypes.js +773 -0
- package/src/components/form-components/vs-form-color-picker.vue +29 -0
- package/src/components/form-components/vs-form-custom-datatable.vue +214 -0
- package/src/components/form-components/vs-form-integer.vue +86 -0
- package/src/components/form-components/vs-form-key-value.vue +201 -0
- package/src/components/form-components/vs-form-marcdown-md.vue +3 -0
- package/src/components/form-components/vs-form-media-select.vue +780 -0
- package/src/components/form-components/vs-form-reference-list.vue +97 -0
- package/src/components/form-components/vs-form-reference.vue +59 -0
- package/src/components/form-components/vs-form-relation.vue +30 -0
- package/src/components/form-components/vs-form-reletion-link.vue +34 -0
- package/src/components/form-components/vs-form-select-collection.vue +0 -0
- package/src/components/form-components/vs-form-slug.vue +72 -0
- package/src/components/form-components/vs-form-tiptap.vue +7 -0
- package/src/components/form-components/vs-richtext-md.vue +3 -0
- package/src/components/icons/BellIcon.vue +17 -0
- package/src/components/icons/GlobeIcon.vue +18 -0
- package/src/components/icons/KeyIcon.vue +20 -0
- package/src/components/icons/PaletteIcon.vue +22 -0
- package/src/components/icons/SettingsIcon.vue +19 -0
- package/src/components/icons/ShieldIcon.vue +18 -0
- package/src/components/icons/UsersIcon.vue +19 -0
- package/src/components/icons/icon-chevron-right.vue +16 -0
- package/src/components/icons/icon-drag.vue +20 -0
- package/src/components/icons/icon-file-text.vue +21 -0
- package/src/components/icons/icon-folder.vue +18 -0
- package/src/components/icons/icon-grid.vue +17 -0
- package/src/components/icons/icon-group.vue +19 -0
- package/src/components/icons/icon-home.vue +16 -0
- package/src/components/icons/icon-image.vue +18 -0
- package/src/components/icons/icon-list.vue +20 -0
- package/src/components/icons/icon-more.vue +17 -0
- package/src/components/icons/icon-plus.vue +17 -0
- package/src/components/icons-types/icon-array.vue +22 -0
- package/src/components/icons-types/icon-boolean.vue +18 -0
- package/src/components/icons-types/icon-datalist.vue +22 -0
- package/src/components/icons-types/icon-date.vue +20 -0
- package/src/components/icons-types/icon-datetime.vue +20 -0
- package/src/components/icons-types/icon-file.vue +21 -0
- package/src/components/icons-types/icon-gallery.vue +18 -0
- package/src/components/icons-types/icon-image.vue +19 -0
- package/src/components/icons-types/icon-integer.vue +20 -0
- package/src/components/icons-types/icon-merkdown.vue +18 -0
- package/src/components/icons-types/icon-multiselect.vue +22 -0
- package/src/components/icons-types/icon-number.vue +20 -0
- package/src/components/icons-types/icon-radio.vue +22 -0
- package/src/components/icons-types/icon-reference-list.vue +22 -0
- package/src/components/icons-types/icon-reference.vue +20 -0
- package/src/components/icons-types/icon-relation.vue +22 -0
- package/src/components/icons-types/icon-richtext.vue +18 -0
- package/src/components/icons-types/icon-select.vue +22 -0
- package/src/components/icons-types/icon-slug.vue +19 -0
- package/src/components/icons-types/icon-text.vue +19 -0
- package/src/components/icons-types/index.js +43 -0
- package/src/components/layout/Layout.vue +67 -0
- package/src/components/layout/Sidebar.vue +128 -0
- package/src/components/media/FileUploadProgress.vue +29 -0
- package/src/components/media/MediaBreadcrumb.vue +42 -0
- package/src/components/media/MediaCreateFolder.vue +59 -0
- package/src/components/media/MediaFileInfo.vue +148 -0
- package/src/components/media/MediaGrid.vue +148 -0
- package/src/components/media/MediaList.vue +148 -0
- package/src/components/media/MediaViewControls.vue +38 -0
- package/src/components/media/TypeTag.vue +23 -0
- package/src/components/menu/AddNewItemInTree.vue +75 -0
- package/src/components/menu/MenuBody.vue +149 -0
- package/src/components/menu/MenuItem.vue +73 -0
- package/src/components/menu/MenuList.vue +101 -0
- package/src/components/referencec/index.ts +7 -0
- package/src/components/referencec/vs-reference-faq.vue +61 -0
- package/src/components/referencec/vs-reference-user-card.vue +40 -0
- package/src/components/settings/NotificationSettings.vue +32 -0
- package/src/components/settings/SettingsTable.vue +50 -0
- package/src/components/settings/SettingsTitle.vue +33 -0
- package/src/components/settings/SettingsToggleItem.vue +25 -0
- package/src/components/sidebar/DropdownMenu.vue +34 -0
- package/src/components/sidebar/SettingsSidebar.vue +121 -0
- package/src/components/sidebar/SidebarFooter.vue +52 -0
- package/src/components/sidebar/SidebarHeader.vue +57 -0
- package/src/components/sidebar/SidebarMenu.vue +78 -0
- package/src/components/ui/EmptyData.vue +76 -0
- package/src/components/ui/UniversalTable.vue +310 -0
- package/src/components/ui/UniversalTableFilters.vue +0 -0
- package/src/components/ui/UniversalTablePagination.vue +118 -0
- package/src/components/ui/VsPreview.vue +75 -0
- package/src/composables/useCollectionView.ts +21 -0
- package/src/composables/useDebounce.ts +26 -0
- package/src/composables/useMonaco.ts +28 -0
- package/src/composables/useTheme.ts +40 -0
- package/src/content/test-slug/metadata.json +1 -0
- package/src/i18n.ts +75 -0
- package/src/index.css +3 -0
- package/src/locales/en.json +778 -0
- package/src/locales/uk.json +797 -0
- package/src/main.ts +41 -0
- package/src/pages/Dashboard.vue +168 -0
- package/src/pages/EmailPage.vue +183 -0
- package/src/pages/FeedbackPage.vue +232 -0
- package/src/pages/MediaPage.vue +372 -0
- package/src/pages/TagsPage.vue +207 -0
- package/src/pages/builder/BuilderPage.vue +195 -0
- package/src/pages/builder/EditCollectionPage.vue +163 -0
- package/src/pages/collections/ArticlesPage.vue +385 -0
- package/src/pages/collections/CollectionsPage.vue +146 -0
- package/src/pages/collections/SingletonsPage.vue +119 -0
- package/src/pages/collections/contentForm.vue +484 -0
- package/src/pages/collections/schema/seo.ts +27 -0
- package/src/pages/menu/MenuAddPage.vue +123 -0
- package/src/pages/menu/MenuItemPage.vue +183 -0
- package/src/pages/menu/MenuPage.vue +133 -0
- package/src/pages/settings/ApiKeys.vue +75 -0
- package/src/pages/settings/Appearance.vue +80 -0
- package/src/pages/settings/Logs.vue +260 -0
- package/src/pages/settings/PermissionsPage.vue +237 -0
- package/src/pages/settings/Settings.vue +186 -0
- package/src/pages/settings/Users.vue +109 -0
- package/src/pages/settings/general.vue +154 -0
- package/src/pages/settings/generalScheme.js +132 -0
- package/src/pages/users/AddUser.vue +106 -0
- package/src/pages/users/UsersPage.vue +98 -0
- package/src/props/builder.ts +67 -0
- package/src/props/content.ts +56 -0
- package/src/props/media.ts +63 -0
- package/src/router/index.ts +181 -0
- package/src/types/fastify-auth.d.ts +4 -0
- package/src/utils/getField.js +270 -0
- package/src/utils/translit.js +19 -0
- 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"}
|