news-cms-module 0.0.1 → 0.1.1
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/config/index.js +0 -1
- package/controllers/NewsController.js +42 -4
- package/controllers/StatController.js +3 -3
- package/index.js +0 -5
- package/package.json +1 -1
- package/routes/index.js +2 -12
- package/services/NewsService.js +30 -6
- package/services/StatService.js +4 -11
- package/services/index.js +0 -2
package/config/index.js
CHANGED
|
@@ -54,7 +54,7 @@ class NewsController {
|
|
|
54
54
|
|
|
55
55
|
// --- Route Admin (CRUD) ---
|
|
56
56
|
async adminList(req, res) {
|
|
57
|
-
//
|
|
57
|
+
//tambahkan ARCHIVE
|
|
58
58
|
const { rows: posts } = await this.newsService.getAllPosts(req.query.page, 10, ['DRAFT', 'PUBLISHED']);
|
|
59
59
|
|
|
60
60
|
res.status(200).json({
|
|
@@ -68,10 +68,8 @@ class NewsController {
|
|
|
68
68
|
|
|
69
69
|
async createPost(req, res) {
|
|
70
70
|
try {
|
|
71
|
-
// Asumsi req.body.content adalah array of blocks [{blockType, contentValue}, ...]
|
|
72
71
|
const { title, summary, authorId, status, contentBlocks } = req.body;
|
|
73
72
|
|
|
74
|
-
// Slug harus dibuat sebelum disimpan
|
|
75
73
|
const slug = title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
|
|
76
74
|
|
|
77
75
|
const newNews = await this.newsService.createPost(
|
|
@@ -96,7 +94,47 @@ class NewsController {
|
|
|
96
94
|
}
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
async updatePost(req, res) {
|
|
98
|
+
try {
|
|
99
|
+
const { id } = req.params;
|
|
100
|
+
const { title, summary, authorId, status, contentBlocks } = req.body;
|
|
101
|
+
|
|
102
|
+
if (!id || !title || !contentBlocks) {
|
|
103
|
+
return res.status(400).json({ success: false, message: 'Missing required fields or ID.' });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const slug = title.toLowerCase().trim().replace(/ /g, '-').replace(/[^\w-]+/g, '');
|
|
107
|
+
const isPublished = status === 'PUBLISHED';
|
|
108
|
+
|
|
109
|
+
const updatedPost = await this.newsService.updatePost(
|
|
110
|
+
id,
|
|
111
|
+
{
|
|
112
|
+
title,
|
|
113
|
+
slug,
|
|
114
|
+
summary,
|
|
115
|
+
authorId,
|
|
116
|
+
status: status || 'DRAFT',
|
|
117
|
+
publishedAt: isPublished ? new Date() : null
|
|
118
|
+
},
|
|
119
|
+
contentBlocks
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
if (!updatedPost) {
|
|
123
|
+
return res.status(404).json({ success: false, message: 'News post not found for update.' });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
res.status(200).json({
|
|
127
|
+
success: true,
|
|
128
|
+
message: 'Post updated successfully.',
|
|
129
|
+
data: updatedPost
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(error);
|
|
134
|
+
res.status(500).json({ success: false, message: 'Failed to update post.', error: error.message });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
100
138
|
}
|
|
101
139
|
|
|
102
140
|
module.exports = NewsController;
|
|
@@ -5,14 +5,14 @@ class StatController {
|
|
|
5
5
|
this.statService = statService;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
// Middleware untuk pelacakan kunjungan
|
|
8
|
+
// Middleware untuk pelacakan kunjungan blum selesai
|
|
9
9
|
async trackVisitMiddleware(req, res, next) {
|
|
10
10
|
const { slug } = req.params;
|
|
11
11
|
|
|
12
12
|
// Ambil post ID (idealnya dilakukan oleh service sebelum dipanggil)
|
|
13
13
|
// Untuk demo, kita asumsikan kita punya newsId dari service atau middleware sebelumnya.
|
|
14
14
|
// **REAL-WORLD:** Anda harus mencari newsId berdasarkan slug di sini atau di service.
|
|
15
|
-
const newsId = 1; // Contoh: Asumsi newsId ditemukan
|
|
15
|
+
// const newsId = 1; // Contoh: Asumsi newsId ditemukan
|
|
16
16
|
|
|
17
17
|
const sessionId = req.sessionID || req.ip; // Gunakan sesi atau IP untuk unique ID
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ class StatController {
|
|
|
20
20
|
await this.statService.trackVisit(newsId, sessionId);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
next();
|
|
23
|
+
next();
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
async getTrendingApi(req, res) {
|
package/index.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
const express = require('express');
|
|
2
|
-
const initModels = require('./models'); // Mengimpor inisialisasi model dan koneksi DB
|
|
3
|
-
const initServices = require('./services'); // Mengimpor semua Services (CRUD, Stat)
|
|
4
|
-
const setupRoutes = require('./routes'); // Mengimpor fungsi setup router
|
|
5
|
-
|
|
6
1
|
/**
|
|
7
2
|
* Fungsi utama package yang di-export.
|
|
8
3
|
* @param {object} dbConfig - Konfigurasi koneksi database dari aplikasi pengguna.
|
package/package.json
CHANGED
package/routes/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// routes/index.js
|
|
2
|
-
|
|
3
1
|
const express = require('express');
|
|
4
2
|
const NewsController = require('../controllers/NewsController');
|
|
5
3
|
const StatController = require('../controllers/StatController');
|
|
@@ -52,23 +50,15 @@ module.exports = (router, services, config) => {
|
|
|
52
50
|
adminRouter.post('/create', newsController.createPost.bind(newsController));
|
|
53
51
|
|
|
54
52
|
// PUT/PATCH /admin/update/:id (Memperbarui berita)
|
|
55
|
-
|
|
53
|
+
adminRouter.put('/update/:id', newsController.updatePost.bind(newsController));
|
|
56
54
|
|
|
57
55
|
// DELETE /admin/delete/:id (Menghapus berita)
|
|
58
|
-
|
|
56
|
+
adminRouter.delete('/delete/:id', newsController.deletePost.bind(newsController));
|
|
59
57
|
|
|
60
58
|
// Pasang router admin ke prefix yang ditentukan pengguna (default: '/admin')
|
|
61
59
|
router.use(config.adminRoutePrefix, adminRouter);
|
|
62
|
-
|
|
63
|
-
// adminRouter.get('/', newsController.adminList.bind(newsController));
|
|
64
|
-
// adminRouter.post('/create', newsController.createPost.bind(newsController));
|
|
65
|
-
// // ... Tambahkan route admin lain (/edit/:id, /delete/:id)
|
|
66
|
-
|
|
67
|
-
// router.use(config.adminRoutePrefix, adminRouter);
|
|
68
|
-
|
|
69
60
|
|
|
70
61
|
// 4. Route API (Untuk data trending)
|
|
71
62
|
router.get('/api/trending', statController.getTrendingApi.bind(statController));
|
|
72
63
|
|
|
73
|
-
// Router telah siap di file index.js
|
|
74
64
|
};
|
package/services/NewsService.js
CHANGED
|
@@ -6,7 +6,6 @@ class NewsService {
|
|
|
6
6
|
|
|
7
7
|
// --- Operasi READ (Membaca) ---
|
|
8
8
|
async getAllPosts(page = 1, limit = 10, status = 'PUBLISHED') {
|
|
9
|
-
// Logika untuk mengambil daftar berita dengan pagination
|
|
10
9
|
const offset = (page - 1) * limit;
|
|
11
10
|
return this.News.findAndCountAll({
|
|
12
11
|
where: { status },
|
|
@@ -17,13 +16,12 @@ class NewsService {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
async getPostBySlug(slug) {
|
|
20
|
-
// Mengambil berita beserta semua blok kontennya
|
|
21
19
|
return this.News.findOne({
|
|
22
20
|
where: { slug, status: 'PUBLISHED' },
|
|
23
21
|
include: [{
|
|
24
22
|
model: this.ContentNews,
|
|
25
23
|
as: 'blocks',
|
|
26
|
-
order: [['order', 'ASC']]
|
|
24
|
+
order: [['order', 'ASC']]
|
|
27
25
|
}]
|
|
28
26
|
});
|
|
29
27
|
}
|
|
@@ -44,10 +42,36 @@ class NewsService {
|
|
|
44
42
|
});
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
async updatePost(id, newsData, contentBlocks) {
|
|
46
|
+
const existingNews = await this.News.findByPk(id);
|
|
47
|
+
if (!existingNews) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return this.News.sequelize.transaction(async (t) => {
|
|
52
|
+
await existingNews.update(newsData, { transaction: t });
|
|
53
|
+
|
|
54
|
+
await this.ContentNews.destroy({
|
|
55
|
+
where: { newsId: id },
|
|
56
|
+
transaction: t
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const blocks = contentBlocks.map((block, index) => ({
|
|
60
|
+
...block,
|
|
61
|
+
newsId: id,
|
|
62
|
+
order: index + 1
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
await this.ContentNews.bulkCreate(blocks, { transaction: t });
|
|
66
|
+
|
|
67
|
+
return this.News.findByPk(id, {
|
|
68
|
+
include: [{ model: this.ContentNews, as: 'blocks', order: [['order', 'ASC']] }],
|
|
69
|
+
transaction: t
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
48
74
|
async deletePost(id) {
|
|
49
|
-
// Karena kita menggunakan onDelete: 'CASCADE' di Models,
|
|
50
|
-
// menghapus News akan otomatis menghapus ContentNews dan VisitorLog
|
|
51
75
|
return this.News.destroy({ where: { id } });
|
|
52
76
|
}
|
|
53
77
|
}
|
package/services/StatService.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// services/StatService.js
|
|
2
|
-
|
|
3
1
|
const { Op, literal } = require('sequelize');
|
|
4
2
|
|
|
5
3
|
class StatService {
|
|
@@ -9,7 +7,6 @@ class StatService {
|
|
|
9
7
|
}
|
|
10
8
|
|
|
11
9
|
async trackVisit(newsId, sessionId) {
|
|
12
|
-
// Mencoba membuat entri unik (newsId, sessionId). Jika sudah ada, Sequelize akan melempar error unik.
|
|
13
10
|
try {
|
|
14
11
|
await this.VisitorLog.create({
|
|
15
12
|
newsId: newsId,
|
|
@@ -18,8 +15,6 @@ class StatService {
|
|
|
18
15
|
});
|
|
19
16
|
return true;
|
|
20
17
|
} catch (error) {
|
|
21
|
-
// Ini biasanya terjadi karena pelanggaran unique constraint (pengunjung yang sama mengklik lagi)
|
|
22
|
-
// Di sini kita bisa mengabaikan error atau logging.
|
|
23
18
|
return false;
|
|
24
19
|
}
|
|
25
20
|
}
|
|
@@ -27,26 +22,24 @@ class StatService {
|
|
|
27
22
|
async getTrendingPosts(timeframeHours = 24, limit = 10) {
|
|
28
23
|
const cutOffTime = new Date(new Date() - timeframeHours * 60 * 60 * 1000);
|
|
29
24
|
|
|
30
|
-
// Menggunakan Sequelize untuk melakukan GROUP BY dan COUNT
|
|
31
25
|
const trendingLogs = await this.VisitorLog.findAll({
|
|
32
26
|
attributes: [
|
|
33
27
|
'newsId',
|
|
34
|
-
[literal('COUNT(DISTINCT sessionId)'), 'uniqueVisits']
|
|
28
|
+
[literal('COUNT(DISTINCT sessionId)'), 'uniqueVisits']
|
|
35
29
|
],
|
|
36
30
|
where: {
|
|
37
|
-
visitedAt: { [Op.gte]: cutOffTime }
|
|
31
|
+
visitedAt: { [Op.gte]: cutOffTime }
|
|
38
32
|
},
|
|
39
33
|
group: ['newsId'],
|
|
40
|
-
order: [[literal('uniqueVisits'), 'DESC']],
|
|
34
|
+
order: [[literal('uniqueVisits'), 'DESC']],
|
|
41
35
|
limit: limit
|
|
42
36
|
});
|
|
43
37
|
|
|
44
38
|
const newsIds = trendingLogs.map(log => log.newsId);
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
|
|
47
41
|
return this.News.findAll({
|
|
48
42
|
where: { id: newsIds, status: 'PUBLISHED' },
|
|
49
|
-
// Tambahkan order agar urutan trending tetap terjaga
|
|
50
43
|
order: [[literal(`FIELD(id, ${newsIds.join(',')})`)]], // MySQL specific ordering
|
|
51
44
|
});
|
|
52
45
|
}
|