news-cms-module 1.0.0 → 1.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/README.md +55 -31
- package/config/index.js +2 -0
- package/controllers/NewsController.js +0 -23
- package/models/index.js +5 -2
- package/package.json +2 -2
- package/routes/index.js +18 -2
- package/services/NewsService.js +8 -11
- package/views/admin/create_news.ejs +7 -11
- package/views/admin/dashboard.ejs +1 -1
- package/views/admin/update_news.ejs +2 -10
package/README.md
CHANGED
|
@@ -21,48 +21,72 @@ npm install news-cms-module ejs express-session mysql2
|
|
|
21
21
|
|
|
22
22
|
```javascript
|
|
23
23
|
const express = require('express');
|
|
24
|
-
|
|
24
|
+
require('dotenv').config();
|
|
25
25
|
const newsModule = require('news-cms-module');
|
|
26
26
|
|
|
27
27
|
const app = express();
|
|
28
28
|
|
|
29
29
|
const dbConfig = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
host: process.env.DB_HOST,
|
|
31
|
+
port: process.env.DB_PORT || 3036, // Sesuaikan dengan port db anda
|
|
32
|
+
username: process.env.DB_USER_NAME,
|
|
33
|
+
password: process.env.DB_PASSWORD,
|
|
34
|
+
database: process.env.DB_NAME,
|
|
35
|
+
dialect: 'mysql',
|
|
36
|
+
dialectOptions: {
|
|
37
|
+
ssl: {
|
|
38
|
+
require: true,
|
|
39
|
+
rejectUnauthorized: false // Set ke false jika tidak pakai file sertifikat .pem
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
// Opsional: tambahkan ini untuk mencegah timeout di koneksi lambat
|
|
43
|
+
pool: {
|
|
44
|
+
max: 5,
|
|
45
|
+
min: 0,
|
|
46
|
+
acquire: 30000,
|
|
47
|
+
idle: 10000
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Konfigurasi URL Dinamis
|
|
52
|
+
const PORT = process.env.PORT || 3000;
|
|
53
|
+
const APP_URL = process.env.APP_URL || `http://localhost:${PORT}`;
|
|
54
|
+
const NEWS_PREFIX = '/berita'; // Anda bisa mengganti ini sesuka hati (misal: /news)
|
|
36
55
|
|
|
37
56
|
async function startServer() {
|
|
38
|
-
// 1. Middleware Global
|
|
57
|
+
// 1. Inisialisasi Middleware Global
|
|
39
58
|
app.use(express.json());
|
|
40
59
|
app.use(express.urlencoded({ extended: true }));
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})
|
|
60
|
+
app.use(express.static('public'));
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// 2. Inisialisasi Package (Dijalankan secara ASYNC)
|
|
64
|
+
const newsRouter = await newsModule(dbConfig, {
|
|
65
|
+
adminRoutePrefix: '/cms-admin',
|
|
66
|
+
newsPrefix: NEWS_PREFIX,
|
|
67
|
+
baseUrl: APP_URL + NEWS_PREFIX,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 3. Pasang Router ke Prefix URL
|
|
71
|
+
// Ini akan membuat rute: /berita/list, /berita/cms-admin, dll.
|
|
72
|
+
app.use(NEWS_PREFIX, newsRouter);
|
|
73
|
+
|
|
74
|
+
// 4. Jalankan Server
|
|
75
|
+
app.listen(PORT, () => {
|
|
76
|
+
console.log(`Server Berhasil Dijalankan!`);
|
|
77
|
+
console.log(`Base URL : ${APP_URL}`);
|
|
78
|
+
console.log(`API Berita : ${APP_URL}${NEWS_PREFIX}/list`);
|
|
79
|
+
console.log(`Admin CMS : ${APP_URL}${NEWS_PREFIX}/cms-admin/dashboard`);
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("Gagal menjalankan server:", error);
|
|
83
|
+
}
|
|
63
84
|
}
|
|
64
85
|
|
|
65
86
|
startServer();
|
|
66
87
|
```
|
|
88
|
+
pastikan untuk menyesuaikan bagian authAdminMiddleware pada module di bagian middlewares. Sesuaikan dengan preferensi tabel user masing-masing.
|
|
89
|
+
|
|
67
90
|
|
|
68
|
-
|
|
91
|
+
github news-cms-module: https://github.com/DewaAry-D/news-cms-module.git
|
|
92
|
+
github contoh implementasi: https://github.com/DewaAry-D/dummy-news.git
|
package/config/index.js
CHANGED
|
@@ -4,6 +4,8 @@ const defaultConfig = {
|
|
|
4
4
|
// Pengaturan Router
|
|
5
5
|
adminRoutePrefix: '/admin', // Prefix default untuk route CRUD admin
|
|
6
6
|
publicRoutePrefix: '/', // Prefix default untuk route publik
|
|
7
|
+
newsPrefix: '/berita', // Prefix defaul umum contohnya /berita/{..}
|
|
8
|
+
baseURL: '', // Prefix defaul untuk fetch API
|
|
7
9
|
|
|
8
10
|
// Pengaturan Views
|
|
9
11
|
viewEngine: 'ejs', // Template engine default
|
|
@@ -37,24 +37,6 @@ class NewsController {
|
|
|
37
37
|
const categories = await this.newsService.getUniqueCategories();
|
|
38
38
|
const trending = await this.newsService.getTrendingNews();
|
|
39
39
|
|
|
40
|
-
//ini mati klo dah ada view
|
|
41
|
-
// res.status(200).json({
|
|
42
|
-
// success: true,
|
|
43
|
-
// data: {
|
|
44
|
-
// posts,
|
|
45
|
-
// categories,
|
|
46
|
-
// trending,
|
|
47
|
-
// pagination: {
|
|
48
|
-
// totalItems,
|
|
49
|
-
// totalPages,
|
|
50
|
-
// currentPage,
|
|
51
|
-
// perPage,
|
|
52
|
-
// hasNextPage: currentPage < totalPages,
|
|
53
|
-
// hasPrevPage: currentPage > 1
|
|
54
|
-
// }
|
|
55
|
-
// }
|
|
56
|
-
// });
|
|
57
|
-
|
|
58
40
|
//render
|
|
59
41
|
res.render(path.join(__dirname, "../views/home.ejs"), {
|
|
60
42
|
posts,
|
|
@@ -144,11 +126,6 @@ class NewsController {
|
|
|
144
126
|
error: 'news tidak ditemukan'
|
|
145
127
|
});
|
|
146
128
|
}
|
|
147
|
-
|
|
148
|
-
// res.status(200).json({
|
|
149
|
-
// success: true,
|
|
150
|
-
// data: post
|
|
151
|
-
// })
|
|
152
129
|
|
|
153
130
|
res.render(path.join(__dirname, '../views/admin/update_news.ejs'), {
|
|
154
131
|
data: posts
|
package/models/index.js
CHANGED
|
@@ -6,8 +6,11 @@ const VisitorLogModel = require('./VisitorLog');
|
|
|
6
6
|
module.exports = (config) => {
|
|
7
7
|
const sequelize = new Sequelize(config.database, config.username, config.password, {
|
|
8
8
|
host: config.host,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
port: config.port,
|
|
10
|
+
dialect: config.dialect || 'mysql',
|
|
11
|
+
dialectOptions: config.dialectOptions,
|
|
12
|
+
pool: config.pool,
|
|
13
|
+
logging: config.logging !== undefined ? config.logging : false,
|
|
11
14
|
});
|
|
12
15
|
|
|
13
16
|
const db = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "news-cms-module",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"express-session": "^1.18.2",
|
|
17
17
|
"express-validator": "^7.3.1",
|
|
18
18
|
"multer": "^2.0.2",
|
|
19
|
-
"mysql2": "^3.
|
|
19
|
+
"mysql2": "^3.16.0",
|
|
20
20
|
"path": "^0.12.7",
|
|
21
21
|
"sequelize": "^6.37.7"
|
|
22
22
|
}
|
package/routes/index.js
CHANGED
|
@@ -42,8 +42,24 @@ module.exports = (router, services, config) => {
|
|
|
42
42
|
const adminRouter = express.Router();
|
|
43
43
|
adminRouter.use(isAdmin);
|
|
44
44
|
|
|
45
|
-
adminRouter.get('/create', (req, res) => {
|
|
46
|
-
|
|
45
|
+
// adminRouter.get('/create', (req, res) => {
|
|
46
|
+
// res.render(path.join(__dirname, '../views/admin/create_news.ejs'));
|
|
47
|
+
// });
|
|
48
|
+
|
|
49
|
+
router.get('/cms-admin/create', (req, res) => {
|
|
50
|
+
const appBaseUrl = config.baseUrl;
|
|
51
|
+
const newsPrefix = config.newsPrefix;
|
|
52
|
+
const adminPrefix = config.adminRoutePrefix;
|
|
53
|
+
|
|
54
|
+
const fullApiUrl = `${appBaseUrl}${adminPrefix}/create`;
|
|
55
|
+
const nextUrl = `${newsPrefix}${adminPrefix}/list`
|
|
56
|
+
|
|
57
|
+
res.render(path.join(__dirname, "../views/admin/create_news.ejs"), {
|
|
58
|
+
title: 'Buat Berita Baru',
|
|
59
|
+
|
|
60
|
+
apiBaseUrl: fullApiUrl,
|
|
61
|
+
nextUrl
|
|
62
|
+
});
|
|
47
63
|
});
|
|
48
64
|
adminRouter.post('/create', beritaUpload, parseContentBlocks, CreateNewsValidationRules, validate, newsController.createPost.bind(newsController));
|
|
49
65
|
|
package/services/NewsService.js
CHANGED
|
@@ -9,10 +9,8 @@ class NewsService {
|
|
|
9
9
|
|
|
10
10
|
async getTrendingNews() {
|
|
11
11
|
try {
|
|
12
|
-
// 1. Tentukan batas waktu (24 jam yang lalu dari sekarang)
|
|
13
12
|
const last24Hours = new Date(new Date() - 24 * 60 * 60 * 1000);
|
|
14
13
|
|
|
15
|
-
// 2. Query untuk menghitung views per berita
|
|
16
14
|
const trending = await this.News.findAll({
|
|
17
15
|
attributes: [
|
|
18
16
|
'id',
|
|
@@ -22,24 +20,23 @@ class NewsService {
|
|
|
22
20
|
'imagePath',
|
|
23
21
|
'authorName',
|
|
24
22
|
'createdAt',
|
|
25
|
-
// Membuat kolom virtual 'totalViews' dari hasil hitung (COUNT)
|
|
26
23
|
[fn('COUNT', col('visits.id')), 'totalViews']
|
|
27
24
|
],
|
|
28
25
|
include: [{
|
|
29
26
|
model: this.VisitorLog,
|
|
30
|
-
as: 'visits',
|
|
31
|
-
attributes: [],
|
|
27
|
+
as: 'visits',
|
|
28
|
+
attributes: [],
|
|
32
29
|
where: {
|
|
33
30
|
visitedAt: {
|
|
34
|
-
[Op.gt]: last24Hours
|
|
31
|
+
[Op.gt]: last24Hours
|
|
35
32
|
}
|
|
36
33
|
},
|
|
37
|
-
required: true
|
|
34
|
+
required: true
|
|
38
35
|
}],
|
|
39
|
-
group: ['News.id'],
|
|
40
|
-
order: [[fn('COUNT', col('visits.id')), 'DESC']],
|
|
41
|
-
limit: 10,
|
|
42
|
-
subQuery: false
|
|
36
|
+
group: ['News.id'],
|
|
37
|
+
order: [[fn('COUNT', col('visits.id')), 'DESC']],
|
|
38
|
+
limit: 10,
|
|
39
|
+
subQuery: false
|
|
43
40
|
});
|
|
44
41
|
|
|
45
42
|
return trending;
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
<!-- Sidebar -->
|
|
13
13
|
<nav class="sidebar">
|
|
14
14
|
<div class="logo">
|
|
15
|
-
<a href="
|
|
15
|
+
<a href="./dashboard" style="display: flex; justify-content: center;"><img src="/berita/img/logo.png"
|
|
16
16
|
alt="Logo DeNews" /></a>
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
19
|
<ul class="nav-menu">
|
|
20
20
|
<li class="nav-item">
|
|
21
|
-
<a href="
|
|
21
|
+
<a href="./dashboard" class="nav-link">
|
|
22
22
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
23
23
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
|
24
24
|
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
|
@@ -73,6 +73,9 @@
|
|
|
73
73
|
<option value="Hiburan">Hiburan</option>
|
|
74
74
|
<option value="Olahraga">Olahraga</option>
|
|
75
75
|
<option value="Nasional">Nasional</option>
|
|
76
|
+
<option value="Teknologi">Teknologi</option>
|
|
77
|
+
<option value="Game">Game</option>
|
|
78
|
+
<option value="Kesehatan">Kesehatan</option>
|
|
76
79
|
</select>
|
|
77
80
|
</div>
|
|
78
81
|
|
|
@@ -279,29 +282,22 @@
|
|
|
279
282
|
|
|
280
283
|
console.log(contentBlocksData);
|
|
281
284
|
|
|
282
|
-
console.log("step 6");
|
|
283
285
|
|
|
284
286
|
// Kirim metadata blok sebagai string JSON (Standar multipart/form-data untuk array of objects)
|
|
285
287
|
formData.append('contentBlocks', JSON.stringify(contentBlocksData));
|
|
286
|
-
|
|
287
|
-
console.log("step 7");
|
|
288
|
-
|
|
289
288
|
try {
|
|
290
|
-
const response = await fetch('
|
|
289
|
+
const response = await fetch('<%= apiBaseUrl %>', {
|
|
291
290
|
method: 'POST',
|
|
292
291
|
body: formData
|
|
293
292
|
// Jangan set Header Content-Type, browser akan otomatis mengurusnya untuk FormData
|
|
294
293
|
});
|
|
295
294
|
|
|
296
|
-
console.log("step 8");
|
|
297
|
-
|
|
298
295
|
const result = await response.json();
|
|
299
296
|
|
|
300
|
-
console.log("step 9");
|
|
301
297
|
|
|
302
298
|
if (response.ok && result.success) {
|
|
303
299
|
alert('Berita berhasil disimpan!');
|
|
304
|
-
window.location.href = '
|
|
300
|
+
window.location.href = '<%= nextUrl %>';
|
|
305
301
|
} else {
|
|
306
302
|
alert('Gagal membuat berita: ' + (result.error || 'Terjadi kesalahan server'));
|
|
307
303
|
}
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
%>
|
|
90
90
|
<div class="bar-wrapper">
|
|
91
91
|
<div class="bar" data-height="<%= Math.round(heightPercent) %>"
|
|
92
|
-
style="height: <%= Math.round(heightPercent)
|
|
92
|
+
style="height: <%= Math.round(heightPercent) %>%">
|
|
93
93
|
</div>
|
|
94
94
|
<span class="bar-label">
|
|
95
95
|
<%= monthName %>
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
<!-- Sidebar -->
|
|
13
13
|
<nav class="sidebar">
|
|
14
14
|
<div class="logo">
|
|
15
|
-
<a href="index.html" style="display: flex; justify-content: center;"><img src="
|
|
15
|
+
<a href="index.html" style="display: flex; justify-content: center;"><img src="/berita/img/logo.png"
|
|
16
16
|
alt="Logo DeNews" /></a>
|
|
17
17
|
</div>
|
|
18
18
|
|
|
19
19
|
<ul class="nav-menu">
|
|
20
20
|
<li class="nav-item">
|
|
21
|
-
<a href="
|
|
21
|
+
<a href="./dashboard" class="nav-link">
|
|
22
22
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
23
23
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
|
24
24
|
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
|
@@ -329,14 +329,10 @@
|
|
|
329
329
|
});
|
|
330
330
|
|
|
331
331
|
console.log(contentBlocksData);
|
|
332
|
-
|
|
333
|
-
console.log("step 6");
|
|
334
332
|
|
|
335
333
|
// Kirim metadata blok sebagai string JSON (Standar multipart/form-data untuk array of objects)
|
|
336
334
|
formData.append('contentBlocks', JSON.stringify(contentBlocksData));
|
|
337
335
|
|
|
338
|
-
console.log("step 7");
|
|
339
|
-
|
|
340
336
|
try {
|
|
341
337
|
let url = 'http://localhost:3000/berita/cms-admin/update/' + '<%= data.id %>';
|
|
342
338
|
const response = await fetch( url, {
|
|
@@ -345,12 +341,8 @@
|
|
|
345
341
|
// Jangan set Header Content-Type, browser akan otomatis mengurusnya untuk FormData
|
|
346
342
|
});
|
|
347
343
|
|
|
348
|
-
console.log("step 8");
|
|
349
|
-
|
|
350
344
|
const result = await response.json();
|
|
351
345
|
|
|
352
|
-
console.log("step 9");
|
|
353
|
-
|
|
354
346
|
if (response.ok && result.success) {
|
|
355
347
|
alert('Berita berhasil disimpan!');
|
|
356
348
|
window.location.href = '/berita/cms-admin/list';
|