news-cms-module 0.1.2 → 1.0.0
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/README.md +68 -0
- package/config/index.js +1 -3
- package/controllers/NewsController.js +280 -60
- package/controllers/StatController.js +22 -15
- package/index.js +18 -32
- package/middlewares/authAdminMiddleware.js +22 -0
- package/middlewares/multerMiddleware.js +45 -0
- package/middlewares/parseForm.js +15 -0
- package/models/News.js +1 -0
- package/models/index.js +3 -8
- package/package.json +4 -1
- package/public/img/Main.png +0 -0
- package/public/img/berita-terkait.png +0 -0
- package/public/img/berita.png +0 -0
- package/public/img/berita2.png +0 -0
- package/public/img/detail1.png +0 -0
- package/public/img/detail2.png +0 -0
- package/public/img/logo.png +0 -0
- package/public/js/script.js +92 -0
- package/public/style.css +1620 -0
- package/routes/index.js +25 -28
- package/services/NewsService.js +202 -34
- package/services/StatService.js +14 -5
- package/services/index.js +3 -3
- package/validations/mainValidation.js +25 -0
- package/validations/newsValidations.js +84 -0
- package/views/admin/create_news.ejs +316 -0
- package/views/admin/dashboard.ejs +104 -0
- package/views/admin/detailadmin.ejs +202 -0
- package/views/admin/list.ejs +331 -0
- package/views/admin/update_news.ejs +368 -0
- package/views/detail.ejs +235 -0
- package/views/home.ejs +220 -0
- package/views/list.ejs +0 -0
package/routes/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
+
const multer = require('multer');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const upload = require('../middlewares/multerMiddleware');
|
|
5
|
+
const {CreateNewsValidationRules, UpdateNewsValidationRules} = require('../validations/newsValidations');
|
|
6
|
+
const {validate} = require('../validations/mainValidation');
|
|
7
|
+
const {parseContentBlocks} = require('../middlewares/parseForm');
|
|
8
|
+
const { isAdmin } = require('../middlewares/authAdminMiddleware');
|
|
9
|
+
|
|
2
10
|
const NewsController = require('../controllers/NewsController');
|
|
3
11
|
const StatController = require('../controllers/StatController');
|
|
4
|
-
const path = require('path');
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* Setup semua route untuk package.
|
|
@@ -12,7 +19,11 @@ const path = require('path');
|
|
|
12
19
|
module.exports = (router, services, config) => {
|
|
13
20
|
// Inisialisasi Controllers dengan services dan config yang dibutuhkan
|
|
14
21
|
const newsController = new NewsController(services.news, services.stat, config);
|
|
15
|
-
const statController = new StatController(services.stat);
|
|
22
|
+
const statController = new StatController(services.news, services.stat);
|
|
23
|
+
const beritaUpload = upload.fields([
|
|
24
|
+
{ name: 'thumbnailImage', maxCount: 1 },
|
|
25
|
+
{ name: 'contentImages', maxCount: 10 }
|
|
26
|
+
]);
|
|
16
27
|
|
|
17
28
|
// 1. Ekspos Aset Statis (CSS)
|
|
18
29
|
// Misalnya, package diakses di /berita-kami, maka aset diakses di /berita-kami/nc-assets
|
|
@@ -22,43 +33,29 @@ module.exports = (router, services, config) => {
|
|
|
22
33
|
// Route ini menggunakan prefix yang ditentukan oleh pengguna (default: '/')
|
|
23
34
|
// router.get(config.publicRoutePrefix, newsController.listPublic.bind(newsController));
|
|
24
35
|
|
|
25
|
-
// 2. Route Publik (READ-ONLY API)
|
|
26
|
-
// Gunakan endpoint yang jelas: /list, /trending, /:slug
|
|
27
36
|
router.get(config.publicRoutePrefix + 'list', newsController.listPublic.bind(newsController));
|
|
28
37
|
|
|
29
|
-
// Route detail berita, termasuk tracking middleware
|
|
30
38
|
router.get(`${config.publicRoutePrefix}post/:slug`,
|
|
31
39
|
statController.trackVisitMiddleware.bind(statController),
|
|
32
40
|
newsController.getDetail.bind(newsController));
|
|
33
41
|
|
|
34
|
-
// Route detail berita (termasuk tracking middleware)
|
|
35
|
-
// PERHATIAN: Di controller, Anda harus memastikan baseUrl dikirim ke view
|
|
36
|
-
// router.get(`${config.publicRoutePrefix}:slug`, statController.trackVisitMiddleware.bind(statController), newsController.getDetail.bind(newsController));
|
|
37
|
-
|
|
38
|
-
// 3. Route Admin (CRUD)
|
|
39
|
-
// Route ini menggunakan prefix admin yang ditentukan oleh pengguna (default: '/admin')
|
|
40
42
|
const adminRouter = express.Router();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// GET /admin/list (Membaca semua post, termasuk DRAFT)
|
|
44
|
-
adminRouter.get('/list', newsController.adminList.bind(newsController));
|
|
43
|
+
adminRouter.use(isAdmin);
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
adminRouter.get('/create', (req, res) => {
|
|
46
|
+
res.render(path.join(__dirname, '../views/admin/create_news.ejs'));
|
|
47
|
+
});
|
|
48
|
+
adminRouter.post('/create', beritaUpload, parseContentBlocks, CreateNewsValidationRules, validate, newsController.createPost.bind(newsController));
|
|
49
|
+
|
|
50
|
+
adminRouter.get('/update/:slug', newsController.getEditForAdmin.bind(newsController));
|
|
51
|
+
adminRouter.patch('/update/:id', newsController.updateStatusNews.bind(newsController));
|
|
52
|
+
adminRouter.put('/update/:id', beritaUpload, parseContentBlocks, UpdateNewsValidationRules, validate, newsController.updatePost.bind(newsController));
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
adminRouter.
|
|
51
|
-
|
|
52
|
-
// PUT/PATCH /admin/update/:id (Memperbarui berita)
|
|
53
|
-
adminRouter.put('/update/:id', newsController.updatePost.bind(newsController));
|
|
54
|
+
adminRouter.get('/dashboard', newsController.dashboardAdmin.bind(newsController));
|
|
55
|
+
adminRouter.get('/list', newsController.adminList.bind(newsController));
|
|
56
|
+
adminRouter.get('/:slug', newsController.getDetailForAdmin.bind(newsController));
|
|
54
57
|
|
|
55
|
-
// DELETE /admin/delete/:id (Menghapus berita)
|
|
56
58
|
adminRouter.delete('/delete/:id', newsController.deletePost.bind(newsController));
|
|
57
|
-
|
|
58
|
-
// Pasang router admin ke prefix yang ditentukan pengguna (default: '/admin')
|
|
59
59
|
router.use(config.adminRoutePrefix, adminRouter);
|
|
60
|
-
|
|
61
|
-
// 4. Route API (Untuk data trending)
|
|
62
60
|
router.get('/api/trending', statController.getTrendingApi.bind(statController));
|
|
63
|
-
|
|
64
61
|
};
|
package/services/NewsService.js
CHANGED
|
@@ -1,79 +1,247 @@
|
|
|
1
|
+
const { Op, fn, col, where } = require("sequelize");
|
|
2
|
+
|
|
1
3
|
class NewsService {
|
|
2
|
-
constructor(NewsModel, ContentNewsModel) {
|
|
4
|
+
constructor(NewsModel, ContentNewsModel, VisitorLogModel) {
|
|
3
5
|
this.News = NewsModel;
|
|
4
6
|
this.ContentNews = ContentNewsModel;
|
|
7
|
+
this.VisitorLog = VisitorLogModel
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async getTrendingNews() {
|
|
11
|
+
try {
|
|
12
|
+
// 1. Tentukan batas waktu (24 jam yang lalu dari sekarang)
|
|
13
|
+
const last24Hours = new Date(new Date() - 24 * 60 * 60 * 1000);
|
|
14
|
+
|
|
15
|
+
// 2. Query untuk menghitung views per berita
|
|
16
|
+
const trending = await this.News.findAll({
|
|
17
|
+
attributes: [
|
|
18
|
+
'id',
|
|
19
|
+
'title',
|
|
20
|
+
'slug',
|
|
21
|
+
'category',
|
|
22
|
+
'imagePath',
|
|
23
|
+
'authorName',
|
|
24
|
+
'createdAt',
|
|
25
|
+
// Membuat kolom virtual 'totalViews' dari hasil hitung (COUNT)
|
|
26
|
+
[fn('COUNT', col('visits.id')), 'totalViews']
|
|
27
|
+
],
|
|
28
|
+
include: [{
|
|
29
|
+
model: this.VisitorLog,
|
|
30
|
+
as: 'visits', // SESUAI dengan alias relasi Anda
|
|
31
|
+
attributes: [], // Kita tidak butuh kolom detail dari VisitorLog
|
|
32
|
+
where: {
|
|
33
|
+
visitedAt: {
|
|
34
|
+
[Op.gt]: last24Hours // Hanya log dalam 24 jam terakhir
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
required: true // Menggunakan INNER JOIN agar hanya berita yang ada view-nya yang muncul
|
|
38
|
+
}],
|
|
39
|
+
group: ['News.id'], // Kelompokkan berdasarkan ID berita
|
|
40
|
+
order: [[fn('COUNT', col('visits.id')), 'DESC']], // Urutkan terbanyak ke terendah
|
|
41
|
+
limit: 10, // Ambil 10 teratas
|
|
42
|
+
subQuery: false // WAJIB false agar LIMIT dan GROUP BY bekerja benar dengan JOIN
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return trending;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("Error fetching trending news:", error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
5
50
|
}
|
|
6
51
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
52
|
+
async getUniqueCategories() {
|
|
53
|
+
const categories = await this.News.findAll({
|
|
54
|
+
attributes: [
|
|
55
|
+
[fn('DISTINCT', col('category')), 'category']
|
|
56
|
+
],
|
|
57
|
+
raw: true
|
|
58
|
+
});
|
|
59
|
+
return categories.map(item => item.category).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getRecommendationNews(category) {
|
|
63
|
+
return this.News.findAll({
|
|
64
|
+
where: {
|
|
65
|
+
category
|
|
66
|
+
},
|
|
67
|
+
limit: 5,
|
|
68
|
+
order: [['createdAt', 'DESC']]
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//for all
|
|
73
|
+
async getAllPosts({ offset = 0, limit = 10, title = '', category = '', status = '' }) {
|
|
74
|
+
const validStatuses = ['PUBLISHED', 'ARCHIVED', 'DRAFT'];
|
|
75
|
+
const where = {};
|
|
76
|
+
|
|
77
|
+
if (status && validStatuses.includes(status)) {
|
|
78
|
+
where.status = status;
|
|
79
|
+
}
|
|
80
|
+
if (title) {
|
|
81
|
+
where.title = { [Op.like]: `%${title}%` };
|
|
82
|
+
}
|
|
83
|
+
if (category) {
|
|
84
|
+
where.category = category;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return await this.News.findAndCountAll({
|
|
88
|
+
where: where,
|
|
12
89
|
limit: limit,
|
|
13
90
|
offset: offset,
|
|
14
|
-
order: [['
|
|
91
|
+
order: [['createdAt', 'DESC']]
|
|
15
92
|
});
|
|
16
93
|
}
|
|
17
94
|
|
|
95
|
+
//forUser
|
|
18
96
|
async getPostBySlug(slug) {
|
|
19
97
|
return this.News.findOne({
|
|
20
98
|
where: { slug, status: 'PUBLISHED' },
|
|
21
99
|
include: [{
|
|
22
100
|
model: this.ContentNews,
|
|
23
|
-
as: '
|
|
101
|
+
as: 'contentBlocks',
|
|
24
102
|
order: [['order', 'ASC']]
|
|
25
103
|
}]
|
|
26
104
|
});
|
|
27
105
|
}
|
|
28
106
|
|
|
29
|
-
//
|
|
30
|
-
|
|
107
|
+
//forAdmin
|
|
108
|
+
|
|
109
|
+
async getPostBySlugForAdmin(slug) {
|
|
110
|
+
return this.News.findOne({
|
|
111
|
+
where: { slug },
|
|
112
|
+
include: [{
|
|
113
|
+
model: this.ContentNews,
|
|
114
|
+
as: 'contentBlocks',
|
|
115
|
+
order: [['order', 'ASC']]
|
|
116
|
+
}]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async createPost(newsData, contentBlocks, files) {
|
|
31
121
|
return this.News.sequelize.transaction(async (t) => {
|
|
122
|
+
const rawPath = files['thumbnailImage']?.[0]?.path;
|
|
123
|
+
newsData.imagePath = rawPath
|
|
124
|
+
? rawPath.replace(/\\/g, '/').replace(/^public/, '')
|
|
125
|
+
: null;
|
|
32
126
|
const newsItem = await this.News.create(newsData, { transaction: t });
|
|
33
127
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
128
|
+
let blocks = [];
|
|
129
|
+
let count = 0;
|
|
130
|
+
contentBlocks.forEach((element, index) => {
|
|
131
|
+
console.log(element);
|
|
132
|
+
if (element.blockType == "IMAGE") {
|
|
133
|
+
const rawPathE = files['contentImages']?.[count]?.path;
|
|
134
|
+
element.contentValue = rawPathE.replace(/\\/g, '/').replace(/^public/, '');
|
|
135
|
+
count++;
|
|
136
|
+
}
|
|
137
|
+
element.newsId = newsItem.id;
|
|
138
|
+
element.order = index + 1;
|
|
139
|
+
blocks.push(element);
|
|
140
|
+
});
|
|
141
|
+
|
|
39
142
|
|
|
40
143
|
await this.ContentNews.bulkCreate(blocks, { transaction: t });
|
|
41
144
|
return newsItem;
|
|
42
145
|
});
|
|
43
146
|
}
|
|
44
147
|
|
|
45
|
-
async updatePost(id, newsData, contentBlocks) {
|
|
46
|
-
const existingNews = await this.News.findByPk(id);
|
|
47
|
-
if (!existingNews) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
148
|
+
async updatePost(id, newsData, contentBlocks, files) {
|
|
51
149
|
return this.News.sequelize.transaction(async (t) => {
|
|
52
|
-
await
|
|
150
|
+
const oldNews = await this.News.findByPk(id, {
|
|
151
|
+
include: [{ model: this.ContentNews, as: 'contentBlocks' }],
|
|
152
|
+
transaction: t
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!oldNews) throw new Error("Berita tidak ditemukan");
|
|
156
|
+
|
|
157
|
+
let filesToDelete = [];
|
|
53
158
|
|
|
54
|
-
|
|
159
|
+
if (files['thumbnailImage']?.[0]) {
|
|
160
|
+
if (oldNews.imagePath) filesToDelete.push(oldNews.imagePath);
|
|
161
|
+
const rawPath = files['thumbnailImage'][0].path;
|
|
162
|
+
// newsData.imagePath = rawPath.replace(/\\/g, '/');
|
|
163
|
+
newsData.imagePath = rawPath.replace(/\\/g, '/').replace(/^public/, '');
|
|
164
|
+
} else {
|
|
165
|
+
newsData.imagePath = oldNews.imagePath;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await oldNews.update(newsData, { transaction: t });
|
|
169
|
+
|
|
170
|
+
const oldBlocks = oldNews.contentBlocks || [];
|
|
171
|
+
|
|
172
|
+
await this.ContentNews.destroy({
|
|
55
173
|
where: { newsId: id },
|
|
56
|
-
transaction: t
|
|
174
|
+
transaction: t
|
|
57
175
|
});
|
|
58
176
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
newsId: id,
|
|
62
|
-
order: index + 1
|
|
63
|
-
}));
|
|
177
|
+
let blocks = [];
|
|
178
|
+
let imageCount = 0;
|
|
64
179
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
180
|
+
contentBlocks.forEach((element, index) => {
|
|
181
|
+
if (element.blockType === "IMAGE") {
|
|
182
|
+
const newFile = files['contentImages']?.[imageCount];
|
|
183
|
+
|
|
184
|
+
if (newFile) {
|
|
185
|
+
const rawPathE = newFile.path;
|
|
186
|
+
// element.contentValue = rawPathE.replace(/\\/g, '/');
|
|
187
|
+
element.contentValue = rawPathE.replace(/\\/g, '/').replace(/^public/, '');
|
|
188
|
+
imageCount++;
|
|
189
|
+
} else {
|
|
190
|
+
element.contentValue = element.contentValue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
element.newsId = id;
|
|
195
|
+
element.order = index + 1;
|
|
196
|
+
blocks.push(element);
|
|
70
197
|
});
|
|
198
|
+
|
|
199
|
+
await this.ContentNews.bulkCreate(blocks, { transaction: t });
|
|
200
|
+
|
|
201
|
+
return { newsItem: oldNews, filesToDelete };
|
|
71
202
|
});
|
|
72
203
|
}
|
|
73
204
|
|
|
205
|
+
async updateStatusNews(id, status) {
|
|
206
|
+
const news = await this.News.findByPk(id);
|
|
207
|
+
if (!news) throw new Error("Berita tidak ditemukan");
|
|
208
|
+
|
|
209
|
+
await news.update({
|
|
210
|
+
status: status
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return news;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
//belum selesai
|
|
74
217
|
async deletePost(id) {
|
|
75
218
|
return this.News.destroy({ where: { id } });
|
|
76
219
|
}
|
|
220
|
+
|
|
221
|
+
async dashboardAdmin(currentYear) {
|
|
222
|
+
const newsCount = await this.News.count();
|
|
223
|
+
|
|
224
|
+
const monthlyVisitors = await this.VisitorLog.findAll({
|
|
225
|
+
attributes: [
|
|
226
|
+
[fn('MONTH', col('visitedAt')), 'month'],
|
|
227
|
+
[fn('COUNT', col('id')), 'total']
|
|
228
|
+
],
|
|
229
|
+
where: where(fn('YEAR', col('visitedAt')), currentYear),
|
|
230
|
+
group: [fn('MONTH', col('visitedAt'))],
|
|
231
|
+
raw: true
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const totalYearlyVisitors = await this.VisitorLog.count({
|
|
235
|
+
where: where(fn('YEAR', col('visitedAt')), currentYear)
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const categoryCount = await this.News.count({
|
|
239
|
+
distinct: true,
|
|
240
|
+
col: 'category'
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return { newsCount, categoryCount, monthlyVisitors, totalYearlyVisitors };
|
|
244
|
+
}
|
|
77
245
|
}
|
|
78
246
|
|
|
79
247
|
module.exports = NewsService;
|
package/services/StatService.js
CHANGED
|
@@ -7,15 +7,24 @@ class StatService {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
async trackVisit(newsId, sessionId) {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const TWENTY_FOUR_HOURS_AGO = new Date(new Date() - 24 * 60 * 60 * 1000);
|
|
11
|
+
|
|
12
|
+
const existingVisit = await this.VisitorLog.findOne({
|
|
13
|
+
where: {
|
|
12
14
|
newsId: newsId,
|
|
13
15
|
sessionId: sessionId,
|
|
16
|
+
visitedAt: {
|
|
17
|
+
[Op.gt]: TWENTY_FOUR_HOURS_AGO
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!existingVisit) {
|
|
23
|
+
return await this.VisitorLog.create({
|
|
24
|
+
newsId,
|
|
25
|
+
sessionId,
|
|
14
26
|
visitedAt: new Date()
|
|
15
27
|
});
|
|
16
|
-
return true;
|
|
17
|
-
} catch (error) {
|
|
18
|
-
return false;
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
|
package/services/index.js
CHANGED
|
@@ -8,8 +8,8 @@ const StatService = require('./StatService');
|
|
|
8
8
|
*/
|
|
9
9
|
module.exports = (db) => {
|
|
10
10
|
return {
|
|
11
|
-
news: new NewsService(db.News, db.ContentNews),
|
|
12
|
-
stat: new StatService(db.VisitorLog, db.News),
|
|
13
|
-
db: db
|
|
11
|
+
news: new NewsService(db.News, db.ContentNews, db.VisitorLog),
|
|
12
|
+
stat: new StatService(db.VisitorLog, db.News),
|
|
13
|
+
db: db
|
|
14
14
|
};
|
|
15
15
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { body, validationResult } = require('express-validator');
|
|
2
|
+
|
|
3
|
+
const validate = (req, res, next) => {
|
|
4
|
+
console.log("di validate");
|
|
5
|
+
const errors = validationResult(req);
|
|
6
|
+
if (errors.isEmpty()) {
|
|
7
|
+
console.log("validate berhasil");
|
|
8
|
+
return next();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
console.log("=== DETAIL ERROR VALIDASI ===");
|
|
12
|
+
console.log(JSON.stringify(errors.array(), null, 2));
|
|
13
|
+
|
|
14
|
+
return res.status(400).json({
|
|
15
|
+
status: 'error',
|
|
16
|
+
errors: errors.array().map(err => ({
|
|
17
|
+
field: err.path,
|
|
18
|
+
message: err.msg
|
|
19
|
+
}))
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
validate
|
|
25
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const { body, validationResult } = require('express-validator');
|
|
2
|
+
|
|
3
|
+
const CreateNewsValidationRules = [
|
|
4
|
+
body('title')
|
|
5
|
+
.notEmpty().withMessage('title wajib diisi')
|
|
6
|
+
.isLength({ min: 3 }).withMessage('title minimal 3 karakter')
|
|
7
|
+
.trim(),
|
|
8
|
+
body('authorName')
|
|
9
|
+
.notEmpty().withMessage('authorName wajib diisi')
|
|
10
|
+
.isLength({ min: 3 }).withMessage('authorName minimal 3 karakter')
|
|
11
|
+
.trim(),
|
|
12
|
+
body('category')
|
|
13
|
+
.notEmpty().withMessage('category wajib diisi')
|
|
14
|
+
.isLength({ min: 3 }).withMessage('category minimal 3 karakter')
|
|
15
|
+
.trim(),
|
|
16
|
+
body('status')
|
|
17
|
+
.notEmpty().withMessage('status wajib dipilih antara DRAFT, PUBLISHED')
|
|
18
|
+
.isLength({ min: 3 }).withMessage('status minimal 3 karakter')
|
|
19
|
+
.isIn(['DRAFT', 'PUBLISHED']).withMessage('status tidak sesuai')
|
|
20
|
+
.trim(),
|
|
21
|
+
|
|
22
|
+
body('contentBlocks')
|
|
23
|
+
.isArray({ min: 1 }).withMessage('contentBlocks harus berupa array dan minimal berisi 1 block'),
|
|
24
|
+
body('contentBlocks.*.blockType')
|
|
25
|
+
.notEmpty().withMessage('blockType wajib diisi')
|
|
26
|
+
.isIn(['PARAGRAPH', 'IMAGE', 'VIDEO', 'SUBHEADING']).withMessage('Tipe block tidak valid'),
|
|
27
|
+
body('contentBlocks.*.contentValue')
|
|
28
|
+
.custom((value, { req, path }) => {
|
|
29
|
+
const index = path.match(/\d+/)[0];
|
|
30
|
+
const blockType = req.body.contentBlocks[index].blockType;
|
|
31
|
+
|
|
32
|
+
if (blockType !== 'IMAGE') {
|
|
33
|
+
if (!value || value.trim() === '') {
|
|
34
|
+
throw new Error(`contentValue untuk ${blockType} wajib diisi`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (( blockType === 'VIDEO') && !value.startsWith('http')) {
|
|
39
|
+
throw new Error(`contentValue untuk ${blockType} harus berupa URL yang valid`);
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}),
|
|
43
|
+
body('contentBlocks.*.caption')
|
|
44
|
+
.optional({ nullable: true })
|
|
45
|
+
.isString().withMessage('caption harus berupa string')
|
|
46
|
+
.isLength({ max: 255 }).withMessage('caption maksimal 255 karakter')
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
const UpdateNewsValidationRules = [
|
|
50
|
+
body('title').optional().isLength({ min: 3 }).trim(),
|
|
51
|
+
body('authorName').optional().isLength({ min: 3 }).trim(),
|
|
52
|
+
body('category').optional().trim(),
|
|
53
|
+
body('status').optional().isIn(['DRAFT', 'PUBLISHED']),
|
|
54
|
+
|
|
55
|
+
// body('contentBlocks')
|
|
56
|
+
// .isArray().withMessage('contentBlocks harus berupa array'),
|
|
57
|
+
body('contentBlocks')
|
|
58
|
+
.isArray({ min: 1 }).withMessage('contentBlocks harus berupa array dan minimal berisi 1 block'),
|
|
59
|
+
|
|
60
|
+
body('contentBlocks.*.blockType')
|
|
61
|
+
.notEmpty().withMessage('blockType wajib diisi'),
|
|
62
|
+
|
|
63
|
+
body('contentBlocks.*.contentValue')
|
|
64
|
+
.custom((value, { req, path }) => {
|
|
65
|
+
const index = path.match(/\d+/)[0];
|
|
66
|
+
const block = req.body.contentBlocks[index];
|
|
67
|
+
|
|
68
|
+
// Jika IMAGE, cek apakah ada file baru ATAU path lama
|
|
69
|
+
if (block.blockType === 'IMAGE') {
|
|
70
|
+
const hasNewFile = req.files && req.files['contentImages'] && req.files['contentImages'].length > 0;
|
|
71
|
+
if (!hasNewFile && (!value || value.trim() === '')) {
|
|
72
|
+
throw new Error(`Blok gambar index ${index} wajib memiliki file baru atau path lama`);
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
if (!value || value.trim() === '') throw new Error('Konten wajib diisi');
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
})
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
CreateNewsValidationRules,
|
|
83
|
+
UpdateNewsValidationRules
|
|
84
|
+
};
|