@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
package/src/main.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createApp } from "vue";
|
|
2
|
+
/*import formComponents from "./components/form-components";
|
|
3
|
+
import referenceComponents from "./components/referencec";
|
|
4
|
+
import iconsTypes from "./components/icons-types";*/
|
|
5
|
+
import App from "./App.vue";
|
|
6
|
+
|
|
7
|
+
import '@opengis/core/dist/index.css'
|
|
8
|
+
import '@opengis/form/dist/index.css'
|
|
9
|
+
// import v3core from '@opengis/v3-core';
|
|
10
|
+
// import v3core from '../../v3-core/src/misc/import-file.ts';
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
import richtext from '@opengis/richtext'
|
|
14
|
+
//import richtext from '../../richtext/src/misc/import-file.js'
|
|
15
|
+
|
|
16
|
+
import router from "./router";
|
|
17
|
+
import { i18n, initI18n } from "./i18n.ts";
|
|
18
|
+
// import "./assets/main.css";
|
|
19
|
+
// import '@opengis/v3-core/dist/style.css';
|
|
20
|
+
|
|
21
|
+
const app = createApp(App);
|
|
22
|
+
/*v3core.install(app, createApp, {
|
|
23
|
+
i18nConfig: i18n,
|
|
24
|
+
});*/
|
|
25
|
+
|
|
26
|
+
/*Object.entries({ ...formComponents, ...iconsTypes, ...referenceComponents }).forEach(([key, value]) => {
|
|
27
|
+
app.component(key, value);
|
|
28
|
+
});*/
|
|
29
|
+
|
|
30
|
+
app.use(router);
|
|
31
|
+
app.use(i18n);
|
|
32
|
+
app.use(richtext, { i18nConfig: i18n });
|
|
33
|
+
app.config.globalProperties.$settings = {
|
|
34
|
+
locale: 'uk',
|
|
35
|
+
locales: ['en', 'uk']
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ініціалізуємо i18n перед монтуванням
|
|
39
|
+
initI18n().then(() => {
|
|
40
|
+
app.mount("#app");
|
|
41
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full max-w-7xl mx-auto space-y-8">
|
|
3
|
+
<!-- Header Section -->
|
|
4
|
+
<DashboardHeader />
|
|
5
|
+
|
|
6
|
+
<!-- Stats Cards -->
|
|
7
|
+
<StatsGrid :stats="statsData" />
|
|
8
|
+
|
|
9
|
+
<!-- Main Content Grid -->
|
|
10
|
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
11
|
+
<!-- Recent Content -->
|
|
12
|
+
<RecentContent
|
|
13
|
+
:content="recentContent"
|
|
14
|
+
class="lg:col-span-2"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<!-- Quick Actions -->
|
|
18
|
+
<div>
|
|
19
|
+
<QuickActions />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { ref, onMounted } from 'vue';
|
|
27
|
+
import DashboardHeader from '../components/dashboard/DashboardHeader.vue';
|
|
28
|
+
import StatsGrid from '../components/dashboard/StatsGrid.vue';
|
|
29
|
+
import RecentContent from '../components/dashboard/RecentContent.vue';
|
|
30
|
+
import QuickActions from '../components/dashboard/QuickActions.vue';
|
|
31
|
+
import { useI18n } from 'vue-i18n';
|
|
32
|
+
|
|
33
|
+
const { t } = useI18n();
|
|
34
|
+
|
|
35
|
+
// Types
|
|
36
|
+
interface StatItem {
|
|
37
|
+
id: string;
|
|
38
|
+
title: string;
|
|
39
|
+
value: string;
|
|
40
|
+
description: string;
|
|
41
|
+
change: {
|
|
42
|
+
value: string;
|
|
43
|
+
isPositive: boolean;
|
|
44
|
+
};
|
|
45
|
+
icon: string;
|
|
46
|
+
color: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ContentItem {
|
|
50
|
+
id: number;
|
|
51
|
+
type: 'Article' | 'Post';
|
|
52
|
+
status: 'published' | 'draft' | 'review';
|
|
53
|
+
title: string;
|
|
54
|
+
author: string;
|
|
55
|
+
updated_at: string;
|
|
56
|
+
views: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
// Data
|
|
62
|
+
const statsData = ref<StatItem[]>([
|
|
63
|
+
{
|
|
64
|
+
id: 'total-content',
|
|
65
|
+
title: t('dashboard.totalContent'),
|
|
66
|
+
value: '2,847',
|
|
67
|
+
description: t('dashboard.articlesPostsPages'),
|
|
68
|
+
change: { value: '+12.5%', isPositive: true },
|
|
69
|
+
icon: 'file-text',
|
|
70
|
+
color: 'blue'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'active-users',
|
|
74
|
+
title: t('dashboard.activeUsers'),
|
|
75
|
+
value: '1,234',
|
|
76
|
+
description: t('dashboard.monthlyActiveUsers'),
|
|
77
|
+
change: { value: '+8.2%', isPositive: true },
|
|
78
|
+
icon: 'users',
|
|
79
|
+
color: 'emerald'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'media-files',
|
|
83
|
+
title: t('dashboard.mediaFiles'),
|
|
84
|
+
value: '5,692',
|
|
85
|
+
description: t('dashboard.imagesVideosDocs'),
|
|
86
|
+
change: { value: '+23.1%', isPositive: true },
|
|
87
|
+
icon: 'image',
|
|
88
|
+
color: 'purple'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'new-media',
|
|
92
|
+
title: t('dashboard.newMediaFiles'),
|
|
93
|
+
value: '89.2K',
|
|
94
|
+
description: t('dashboard.thisMonth'),
|
|
95
|
+
change: { value: '-2.4%', isPositive: false },
|
|
96
|
+
icon: 'eye',
|
|
97
|
+
color: 'orange'
|
|
98
|
+
}
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const recentContent = ref<ContentItem[]>([
|
|
102
|
+
{
|
|
103
|
+
id: 1,
|
|
104
|
+
type: 'Article',
|
|
105
|
+
status: 'published',
|
|
106
|
+
title: t('dashboard.gettingStartedWithNextJs14'),
|
|
107
|
+
author: t('dashboard.sarahJohnson'),
|
|
108
|
+
updated_at: '2024-01-15',
|
|
109
|
+
views: 1247
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 2,
|
|
113
|
+
type: 'Post',
|
|
114
|
+
status: 'draft',
|
|
115
|
+
title: t('dashboard.advancedReactPatterns'),
|
|
116
|
+
author: t('dashboard.mikeChen'),
|
|
117
|
+
updated_at: '2024-01-14',
|
|
118
|
+
views: 0
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 3,
|
|
122
|
+
type: 'Article',
|
|
123
|
+
status: 'published',
|
|
124
|
+
title: t('dashboard.uiUxDesignPrinciples'),
|
|
125
|
+
author: t('dashboard.emmaWilson'),
|
|
126
|
+
updated_at: '2024-01-13',
|
|
127
|
+
views: 892
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 4,
|
|
131
|
+
type: 'Post',
|
|
132
|
+
status: 'review',
|
|
133
|
+
title: t('dashboard.databaseOptimizationTips'),
|
|
134
|
+
author: t('dashboard.alexRodriguez'),
|
|
135
|
+
updated_at: '2024-01-12',
|
|
136
|
+
views: 0
|
|
137
|
+
}
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const rewriteStatsData = (data: any) => {
|
|
141
|
+
statsData.value = statsData.value.map(stat => {
|
|
142
|
+
switch (stat.id) {
|
|
143
|
+
case 'total-content':
|
|
144
|
+
return { ...stat, value: data.totalContent?.toLocaleString() ?? stat.value };
|
|
145
|
+
case 'active-users':
|
|
146
|
+
return { ...stat, value: data.totalUsers?.toLocaleString() ?? stat.value };
|
|
147
|
+
case 'media-files':
|
|
148
|
+
return { ...stat, value: data.totalMediaFiles?.toLocaleString() ?? stat.value };
|
|
149
|
+
case 'new-media':
|
|
150
|
+
return { ...stat, value: data.newMediaFiles?.toLocaleString() ?? stat.value };
|
|
151
|
+
default:
|
|
152
|
+
return stat;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const fetchDashboardData = async () => {
|
|
158
|
+
const res = await fetch('api/cms-stat');
|
|
159
|
+
const data = await res.json();
|
|
160
|
+
|
|
161
|
+
recentContent.value = data.recentContent;
|
|
162
|
+
rewriteStatsData(data.stat);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
onMounted(() => {
|
|
166
|
+
fetchDashboardData();
|
|
167
|
+
});
|
|
168
|
+
</script>
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<div class="w-full max-w-7xl mx-auto space-y-6">
|
|
4
|
+
<SettingsTitle :title="$t('email.title')" :description="$t('email.description')" />
|
|
5
|
+
<div class="rounded-xl text-card-foreground shadow-lg border-0 bg-white dark:bg-slate-800 backdrop-blur-sm">
|
|
6
|
+
<div class="flex flex-col space-y-1.5 p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-slate-50 dark:from-slate-800 to-white dark:to-slate-800">
|
|
7
|
+
<h3 class="tracking-tight text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
|
|
8
|
+
<TableCellsMerge class="text-emerald-500 mr-2"/>
|
|
9
|
+
{{ $t('email.title') }}
|
|
10
|
+
</h3>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="p-6 space-y-4">
|
|
13
|
+
<div class="flex justify-between">
|
|
14
|
+
<VsFilter
|
|
15
|
+
:schema="filterScheme"
|
|
16
|
+
view="inline"
|
|
17
|
+
@change="handleChange"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="overflow-x-auto" v-if="email?.length">
|
|
21
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
22
|
+
<thead class="bg-gray-100">
|
|
23
|
+
<tr>
|
|
24
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">№</th>
|
|
25
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.email') }}</th>
|
|
26
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.cdate') }}</th>
|
|
27
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700"></th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody class="divide-y divide-gray-200" v-for="(email, index) in email" :key="email?.message_id">
|
|
31
|
+
|
|
32
|
+
<tr class="cursor-pointer dark:text-white">
|
|
33
|
+
<td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ email?.num }}</td>
|
|
34
|
+
<td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ email?.email }}</td>
|
|
35
|
+
<td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ formatDate(email?.created_at) }}</td>
|
|
36
|
+
</tr>
|
|
37
|
+
</tbody>
|
|
38
|
+
</table>
|
|
39
|
+
</div>
|
|
40
|
+
<div class='bg-white dark:bg-slate-800 backdrop-blur-sm0 text-center p-6 rounded-xl' v-else>
|
|
41
|
+
<h3 class='text-lg font-semibold'>{{ $t('feedback.table.noFeedback') }}</h3>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="my-5 pr-2">
|
|
45
|
+
<div class="paginationWrapper relative flex justify-center">
|
|
46
|
+
<VsPagination
|
|
47
|
+
:total
|
|
48
|
+
:maxPages="6"
|
|
49
|
+
:defaultPage="page"
|
|
50
|
+
:pageSize="limit"
|
|
51
|
+
:goTo="false"
|
|
52
|
+
size="medium"
|
|
53
|
+
backgroundColor="#e5e7eb"
|
|
54
|
+
:borderedCellSelected="true"
|
|
55
|
+
:pageSizes="[16, 32, 48]"
|
|
56
|
+
@pageChange="handlePageChange"
|
|
57
|
+
@pageSizeChange="changeLimit"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
<script>
|
|
67
|
+
import SettingsTitle from '@/components/settings/SettingsTitle.vue';
|
|
68
|
+
import VsFilter from '@opengis/filter';
|
|
69
|
+
import {
|
|
70
|
+
TableCellsMerge,
|
|
71
|
+
ArrowDown,
|
|
72
|
+
ArrowUp,
|
|
73
|
+
ArrowRightFromLine,
|
|
74
|
+
} from "lucide-vue-next";
|
|
75
|
+
|
|
76
|
+
export default {
|
|
77
|
+
components: {
|
|
78
|
+
SettingsTitle,
|
|
79
|
+
VsFilter,
|
|
80
|
+
TableCellsMerge,
|
|
81
|
+
ArrowDown,
|
|
82
|
+
ArrowUp,
|
|
83
|
+
ArrowRightFromLine
|
|
84
|
+
},
|
|
85
|
+
data() {
|
|
86
|
+
return {
|
|
87
|
+
email: null,
|
|
88
|
+
total: 0,
|
|
89
|
+
openIndex: null,
|
|
90
|
+
limit: 16,
|
|
91
|
+
page: 1,
|
|
92
|
+
filterValue: '',
|
|
93
|
+
filterScheme: [],
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
async created() {
|
|
97
|
+
this.$router.replace({
|
|
98
|
+
query: { page: 1 }
|
|
99
|
+
})
|
|
100
|
+
await this.getData();
|
|
101
|
+
},
|
|
102
|
+
methods: {
|
|
103
|
+
async getData() {
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(`/api/email-list?limit=${this.limit}&page=${this.page}`);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
this.email = data?.rows;
|
|
108
|
+
this.total = data?.total;
|
|
109
|
+
this.filterScheme = data?.filters;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(err.toString())
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
async handleChange(filters) {
|
|
115
|
+
const filterStr = Object.entries(filters?.data)
|
|
116
|
+
.filter(([, v]) => v != null)
|
|
117
|
+
.map(([key, val]) => `${key}=${val}`)
|
|
118
|
+
.join('|');
|
|
119
|
+
|
|
120
|
+
this.filterValue = filterStr;
|
|
121
|
+
|
|
122
|
+
if (this.filterValue ==='no-url') {
|
|
123
|
+
this.$router.replace({
|
|
124
|
+
query: { page: 1 }
|
|
125
|
+
})
|
|
126
|
+
} else {
|
|
127
|
+
this.$router.replace({
|
|
128
|
+
query: { filter:this.filterValue, page: 1 }
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
};
|
|
132
|
+
this.page = 1;
|
|
133
|
+
await this.getData();
|
|
134
|
+
},
|
|
135
|
+
async handlePageChange(page) {
|
|
136
|
+
this.page = page;
|
|
137
|
+
if (this.filterValue==='no-url' || !this.filterValue) {
|
|
138
|
+
this.$router.replace({
|
|
139
|
+
query: { page }
|
|
140
|
+
})
|
|
141
|
+
} else {
|
|
142
|
+
this.$router.replace({
|
|
143
|
+
query: { filter:this.filterValue, page}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
}
|
|
147
|
+
await this.getData();
|
|
148
|
+
},
|
|
149
|
+
async changeLimit(limit) {
|
|
150
|
+
this.limit = limit;
|
|
151
|
+
this.page = 1;
|
|
152
|
+
await this.getData();
|
|
153
|
+
},
|
|
154
|
+
toggle(index) {
|
|
155
|
+
this.openIndex = this.openIndex === index ? null : index
|
|
156
|
+
},
|
|
157
|
+
onEnter(el) {
|
|
158
|
+
el.style.height = '0'
|
|
159
|
+
el.style.opacity = '0'
|
|
160
|
+
requestAnimationFrame(() => {
|
|
161
|
+
el.style.transition = 'all 0.3s ease'
|
|
162
|
+
el.style.height = el.scrollHeight + 'px'
|
|
163
|
+
el.style.opacity = '1'
|
|
164
|
+
})
|
|
165
|
+
},
|
|
166
|
+
onLeave(el) {
|
|
167
|
+
el.style.height = el.scrollHeight + 'px'
|
|
168
|
+
el.style.opacity = '1'
|
|
169
|
+
requestAnimationFrame(() => {
|
|
170
|
+
el.style.transition = 'all 0.3s ease'
|
|
171
|
+
el.style.height = '0'
|
|
172
|
+
el.style.opacity = '0'
|
|
173
|
+
})
|
|
174
|
+
},
|
|
175
|
+
formatDate(date) {
|
|
176
|
+
if (!date) return null;
|
|
177
|
+
const options = { year: "numeric", month: "2-digit", day: "2-digit" };
|
|
178
|
+
return new Date(date).toLocaleDateString("uk-UA", options);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
}
|
|
183
|
+
</script>
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<div class="w-full max-w-7xl mx-auto space-y-6">
|
|
4
|
+
<SettingsTitle :title="$t('feedback.title')" :description="$t('feedback.description')" />
|
|
5
|
+
<div class="rounded-xl text-card-foreground shadow-lg border-0 bg-white dark:bg-slate-800 backdrop-blur-sm">
|
|
6
|
+
<div class="flex flex-col space-y-1.5 p-6 border-b border-slate-200 dark:border-slate-700 bg-gradient-to-r from-slate-50 dark:from-slate-800 to-white dark:to-slate-800">
|
|
7
|
+
<h3 class="tracking-tight text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
|
|
8
|
+
<TableCellsMerge class="text-emerald-500 mr-2"/>
|
|
9
|
+
{{ $t('feedback.title') }}
|
|
10
|
+
</h3>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="p-6 space-y-4">
|
|
13
|
+
<div class="flex justify-between">
|
|
14
|
+
<VsFilter
|
|
15
|
+
:schema="filterScheme"
|
|
16
|
+
view="inline"
|
|
17
|
+
@change="handleChange"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="overflow-x-auto" v-if="feedback?.length">
|
|
21
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
22
|
+
<thead class="bg-gray-100">
|
|
23
|
+
<tr>
|
|
24
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">№</th>
|
|
25
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.userName') }}</th>
|
|
26
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.email') }}</th>
|
|
27
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.cdate') }}</th>
|
|
28
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700">{{ $t('feedback.table.phone') }}</th>
|
|
29
|
+
<th class="px-4 py-2 text-left text-sm font-semibold text-gray-700"></th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody class="divide-y divide-gray-200" v-for="(feedback, index) in feedback" :key="feedback?.message_id">
|
|
33
|
+
|
|
34
|
+
<tr class="cursor-pointer dark:text-white" @click="toggle(feedback?.message_id)">
|
|
35
|
+
<td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ feedback?.num }}</td>
|
|
36
|
+
<td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{feedback?.user_name}}</td>
|
|
37
|
+
<td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ feedback?.email }}</td>
|
|
38
|
+
<td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{ formatDate(feedback?.created_at) }}</td>
|
|
39
|
+
<td class="px-4 py-2 text-sm text-gray-800 dark:text-white">{{feedback?.phone}}</td>
|
|
40
|
+
<td class="px-4 py-2 text-sm text-gray-800 dark:text-white">
|
|
41
|
+
<div>
|
|
42
|
+
<ArrowUp v-if="openIndex === feedback?.message_id"/>
|
|
43
|
+
<ArrowDown v-else/>
|
|
44
|
+
</div>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
47
|
+
<Transition name="collapse" @enter="onEnter" @leave="onLeave">
|
|
48
|
+
<tr v-show="openIndex === feedback?.message_id">
|
|
49
|
+
<td colspan="6" class="bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:text-slate-300">
|
|
50
|
+
<div ref="content" class="transition-all duration-300 ease-in-out space-y-2">
|
|
51
|
+
|
|
52
|
+
<div v-if="feedback?.message_text">
|
|
53
|
+
<b>{{ $t('feedback.table.messageText') }}:</b>
|
|
54
|
+
<p class="whitespace-pre-line mt-1">{{ feedback.message_text }}</p>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div v-if="feedback?.feedback_type">
|
|
58
|
+
<b>{{ $t('feedback.table.feedbackType') }}:</b>
|
|
59
|
+
<span>{{ feedback.feedback_type }}</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div v-if="feedback?.telegram_link">
|
|
63
|
+
<b>Telegram:</b>
|
|
64
|
+
<a :href="feedback.telegram_link" class="text-blue-500 underline " target="_blank">{{ feedback.telegram_link }}</a>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div v-if="feedback?.linkedin_link">
|
|
68
|
+
<b>LinkedIn:</b>
|
|
69
|
+
<a :href="feedback.linkedin_link" class="text-blue-500 underline " target="_blank">{{ feedback.linkedin_link }}</a>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div v-if="feedback?.portfolio_link">
|
|
73
|
+
<b>{{ $t('feedback.table.portfolioLink') }}:</b>
|
|
74
|
+
<a :href="feedback.portfolio_link" class="text-blue-500 underline " target="_blank">{{ feedback.portfolio_link }}</a>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</td>
|
|
78
|
+
</tr>
|
|
79
|
+
</Transition>
|
|
80
|
+
</tbody>
|
|
81
|
+
</table>
|
|
82
|
+
</div>
|
|
83
|
+
<div class='bg-white dark:bg-slate-800 backdrop-blur-sm0 text-center p-6 rounded-xl' v-else>
|
|
84
|
+
<h3 class='text-lg font-semibold'>{{ $t('feedback.table.noFeedback') }}</h3>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="my-5 pr-2">
|
|
88
|
+
<div class="paginationWrapper relative flex justify-center">
|
|
89
|
+
<VsPagination
|
|
90
|
+
:total
|
|
91
|
+
:maxPages="6"
|
|
92
|
+
:defaultPage="page"
|
|
93
|
+
:pageSize="limit"
|
|
94
|
+
:goTo="false"
|
|
95
|
+
size="medium"
|
|
96
|
+
backgroundColor="#e5e7eb"
|
|
97
|
+
:borderedCellSelected="true"
|
|
98
|
+
:pageSizes="[16, 32, 48]"
|
|
99
|
+
@pageChange="handlePageChange"
|
|
100
|
+
@pageSizeChange="changeLimit"
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</template>
|
|
109
|
+
<script>
|
|
110
|
+
import SettingsTitle from '@/components/settings/SettingsTitle.vue';
|
|
111
|
+
import {
|
|
112
|
+
TableCellsMerge,
|
|
113
|
+
ArrowDown,
|
|
114
|
+
ArrowUp,
|
|
115
|
+
ArrowRightFromLine,
|
|
116
|
+
} from "lucide-vue-next";
|
|
117
|
+
import VsFilter from '@opengis/filter';
|
|
118
|
+
|
|
119
|
+
export default {
|
|
120
|
+
components: {
|
|
121
|
+
SettingsTitle,
|
|
122
|
+
TableCellsMerge,
|
|
123
|
+
ArrowDown,
|
|
124
|
+
ArrowUp,
|
|
125
|
+
ArrowRightFromLine,
|
|
126
|
+
VsFilter
|
|
127
|
+
},
|
|
128
|
+
data() {
|
|
129
|
+
return {
|
|
130
|
+
feedback: null,
|
|
131
|
+
total: 0,
|
|
132
|
+
openIndex: null,
|
|
133
|
+
limit: 16,
|
|
134
|
+
page: 1,
|
|
135
|
+
filterValue: '',
|
|
136
|
+
filterScheme: [],
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async created() {
|
|
140
|
+
this.$router.replace({
|
|
141
|
+
query: { page: 1 }
|
|
142
|
+
})
|
|
143
|
+
await this.getData();
|
|
144
|
+
},
|
|
145
|
+
methods: {
|
|
146
|
+
async getData() {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch(`/api/feedback?limit=${this.limit}&page=${this.page}${(this.filterValue ==='no-url') ? '':`&filter=${this.filterValue}`}`);
|
|
149
|
+
const data = await response.json();
|
|
150
|
+
this.feedback = data?.rows;
|
|
151
|
+
this.total = data?.total;
|
|
152
|
+
|
|
153
|
+
// Clean filter schema to only include valid IFilterItem properties
|
|
154
|
+
const cleanFilters = (data?.filters || []).map((filter) => {
|
|
155
|
+
const { extra, title, ...cleanFilter } = filter;
|
|
156
|
+
return cleanFilter;
|
|
157
|
+
});
|
|
158
|
+
this.filterScheme = cleanFilters;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error(err.toString())
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
async handleChange(filters) {
|
|
164
|
+
const filterStr = Object.entries(filters?.data)
|
|
165
|
+
.filter(([, v]) => v != null)
|
|
166
|
+
.map(([key, val]) => `${key}=${val}`)
|
|
167
|
+
.join('|');
|
|
168
|
+
|
|
169
|
+
this.filterValue = filterStr;
|
|
170
|
+
|
|
171
|
+
if (this.filterValue ==='no-url') {
|
|
172
|
+
this.$router.replace({
|
|
173
|
+
query: { page: 1 }
|
|
174
|
+
})
|
|
175
|
+
} else {
|
|
176
|
+
this.$router.replace({
|
|
177
|
+
query: { filter:this.filterValue, page: 1 }
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
};
|
|
181
|
+
this.page = 1;
|
|
182
|
+
await this.getData();
|
|
183
|
+
},
|
|
184
|
+
async handlePageChange(page) {
|
|
185
|
+
this.page = page;
|
|
186
|
+
if (this.filterValue==='no-url' || !this.filterValue) {
|
|
187
|
+
this.$router.replace({
|
|
188
|
+
query: { page }
|
|
189
|
+
})
|
|
190
|
+
} else {
|
|
191
|
+
this.$router.replace({
|
|
192
|
+
query: { filter:this.filterValue, page}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
}
|
|
196
|
+
await this.getData();
|
|
197
|
+
},
|
|
198
|
+
async changeLimit(limit) {
|
|
199
|
+
this.limit = limit;
|
|
200
|
+
this.page = 1;
|
|
201
|
+
await this.getData();
|
|
202
|
+
},
|
|
203
|
+
toggle(index) {
|
|
204
|
+
this.openIndex = this.openIndex === index ? null : index
|
|
205
|
+
},
|
|
206
|
+
onEnter(el) {
|
|
207
|
+
el.style.height = '0'
|
|
208
|
+
el.style.opacity = '0'
|
|
209
|
+
requestAnimationFrame(() => {
|
|
210
|
+
el.style.transition = 'all 0.3s ease'
|
|
211
|
+
el.style.height = el.scrollHeight + 'px'
|
|
212
|
+
el.style.opacity = '1'
|
|
213
|
+
})
|
|
214
|
+
},
|
|
215
|
+
onLeave(el) {
|
|
216
|
+
el.style.height = el.scrollHeight + 'px'
|
|
217
|
+
el.style.opacity = '1'
|
|
218
|
+
requestAnimationFrame(() => {
|
|
219
|
+
el.style.transition = 'all 0.3s ease'
|
|
220
|
+
el.style.height = '0'
|
|
221
|
+
el.style.opacity = '0'
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
formatDate(date) {
|
|
225
|
+
if (!date) return null;
|
|
226
|
+
const options = { year: "numeric", month: "2-digit", day: "2-digit" };
|
|
227
|
+
return new Date(date).toLocaleDateString("uk-UA", options);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
}
|
|
232
|
+
</script>
|