news-cms-module 0.1.1 → 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 +299 -58
- 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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const multer = require('multer');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const storage = multer.diskStorage({
|
|
6
|
+
destination: function (req, file, cb) {
|
|
7
|
+
let dest = 'public/newspicture/';
|
|
8
|
+
|
|
9
|
+
if (file.fieldname === 'thumbnailImage') {
|
|
10
|
+
dest += 'thumbnail';
|
|
11
|
+
} else if (file.fieldname === 'contentImages') {
|
|
12
|
+
dest += 'contentnews';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(dest)) {
|
|
16
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
cb(null, dest);
|
|
20
|
+
},
|
|
21
|
+
filename: function (req, file, cb) {
|
|
22
|
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
23
|
+
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const fileFilter = (req, file, cb) => {
|
|
28
|
+
const allowedTypes = /jpeg|jpg|png|webp/;
|
|
29
|
+
const isExtensionValid = allowedTypes.test(path.extname(file.originalname).toLowerCase());
|
|
30
|
+
const isMimeValid = allowedTypes.test(file.mimetype);
|
|
31
|
+
|
|
32
|
+
if (isExtensionValid && isMimeValid) {
|
|
33
|
+
cb(null, true);
|
|
34
|
+
} else {
|
|
35
|
+
cb(new Error('Format file tidak didukung! Gunakan jpg/jpeg/png.'), false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const upload = multer({
|
|
40
|
+
storage: storage,
|
|
41
|
+
fileFilter: fileFilter,
|
|
42
|
+
limits: { fileSize: 3 * 1024 * 1024 }
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
module.exports = upload;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const parseContentBlocks = (req, res, next) => {
|
|
2
|
+
if (req.body.contentBlocks && typeof req.body.contentBlocks === 'string') {
|
|
3
|
+
try {
|
|
4
|
+
req.body.contentBlocks = JSON.parse(req.body.contentBlocks);
|
|
5
|
+
} catch (e) {
|
|
6
|
+
return res.status(400).json({ success: false, error: 'Format konten tidak valid' });
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
console.log("di parseform");
|
|
10
|
+
next();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
parseContentBlocks
|
|
15
|
+
}
|
package/models/News.js
CHANGED
package/models/index.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
// models/index.js
|
|
2
|
-
|
|
3
1
|
const { Sequelize } = require('sequelize');
|
|
4
2
|
const NewsModel = require('./News');
|
|
5
3
|
const ContentNewsModel = require('./ContentNews');
|
|
6
4
|
const VisitorLogModel = require('./VisitorLog');
|
|
7
5
|
|
|
8
6
|
module.exports = (config) => {
|
|
9
|
-
// Inisialisasi Sequelize dengan konfigurasi dari aplikasi pengguna
|
|
10
7
|
const sequelize = new Sequelize(config.database, config.username, config.password, {
|
|
11
8
|
host: config.host,
|
|
12
9
|
dialect: 'mysql',
|
|
13
|
-
logging: false,
|
|
10
|
+
logging: false,
|
|
14
11
|
});
|
|
15
12
|
|
|
16
13
|
const db = {};
|
|
@@ -22,7 +19,7 @@ module.exports = (config) => {
|
|
|
22
19
|
|
|
23
20
|
db.News.hasMany(db.ContentNews, {
|
|
24
21
|
foreignKey: 'newsId',
|
|
25
|
-
as: '
|
|
22
|
+
as: 'contentBlocks',
|
|
26
23
|
onDelete: 'CASCADE'
|
|
27
24
|
});
|
|
28
25
|
db.ContentNews.belongsTo(db.News, {
|
|
@@ -30,7 +27,6 @@ module.exports = (config) => {
|
|
|
30
27
|
as: 'newsItem'
|
|
31
28
|
});
|
|
32
29
|
|
|
33
|
-
|
|
34
30
|
db.News.hasMany(db.VisitorLog, {
|
|
35
31
|
foreignKey: 'newsId',
|
|
36
32
|
as: 'visits',
|
|
@@ -42,7 +38,6 @@ module.exports = (config) => {
|
|
|
42
38
|
});
|
|
43
39
|
|
|
44
40
|
|
|
45
|
-
// --- PENAMBAHAN KUNCI UNTUK SINKRONISASI ---
|
|
46
41
|
/**
|
|
47
42
|
* Metode untuk mensinkronisasi model dengan tabel database.
|
|
48
43
|
* @param {object} options - Opsi sinkronisasi Sequelize (e.g., { alter: true } atau { force: true }).
|
|
@@ -52,7 +47,7 @@ module.exports = (config) => {
|
|
|
52
47
|
// dan mencoba mengubah kolom yang ada agar sesuai dengan definisi model, tanpa menghapus data.
|
|
53
48
|
await sequelize.sync(options);
|
|
54
49
|
};
|
|
55
|
-
|
|
50
|
+
|
|
56
51
|
|
|
57
52
|
db.sequelize = sequelize;
|
|
58
53
|
db.Sequelize = Sequelize;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "news-cms-module",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"dotenv": "^17.2.3",
|
|
14
14
|
"ejs": "^3.1.10",
|
|
15
15
|
"express": "^5.1.0",
|
|
16
|
+
"express-session": "^1.18.2",
|
|
17
|
+
"express-validator": "^7.3.1",
|
|
18
|
+
"multer": "^2.0.2",
|
|
16
19
|
"mysql2": "^3.15.1",
|
|
17
20
|
"path": "^0.12.7",
|
|
18
21
|
"sequelize": "^6.37.7"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Search Bar Toggle Start
|
|
2
|
+
let navbar = document.querySelector(".navbar");
|
|
3
|
+
let searchBar = document.querySelector(".search-bar .fa-magnifying-glass");
|
|
4
|
+
|
|
5
|
+
if (searchBar) {
|
|
6
|
+
searchBar.addEventListener("click", () => {
|
|
7
|
+
navbar.classList.toggle("showInput");
|
|
8
|
+
|
|
9
|
+
if (navbar.classList.contains("showInput")) {
|
|
10
|
+
searchBar.classList.replace("fa-magnifying-glass", "fa-x");
|
|
11
|
+
} else {
|
|
12
|
+
searchBar.classList.replace("fa-x", "fa-magnifying-glass");
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
// Search Bar Toggle End
|
|
17
|
+
|
|
18
|
+
// Menu Toggle Start
|
|
19
|
+
let menuIcon = document.querySelector("#menu-icon");
|
|
20
|
+
let menu = document.querySelector(".menu");
|
|
21
|
+
|
|
22
|
+
if (menuIcon) {
|
|
23
|
+
menuIcon.addEventListener("click", () => {
|
|
24
|
+
menu.classList.toggle("active");
|
|
25
|
+
|
|
26
|
+
// Toggle icon between bars and x
|
|
27
|
+
if (menu.classList.contains("active")) {
|
|
28
|
+
menuIcon.classList.replace("fa-bars", "fa-x");
|
|
29
|
+
} else {
|
|
30
|
+
menuIcon.classList.replace("fa-x", "fa-bars");
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// Menu Toggle End
|
|
35
|
+
|
|
36
|
+
// Dropdown Toggle Start
|
|
37
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
38
|
+
const dropdowns = document.querySelectorAll(".dropdown");
|
|
39
|
+
|
|
40
|
+
dropdowns.forEach((dropdown) => {
|
|
41
|
+
const toggle = dropdown.querySelector(".dropdown-toggle");
|
|
42
|
+
const menu = dropdown.querySelector(".dropdown-menu");
|
|
43
|
+
|
|
44
|
+
if (toggle && menu) {
|
|
45
|
+
toggle.addEventListener("click", (e) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
|
|
49
|
+
// Close other dropdowns
|
|
50
|
+
dropdowns.forEach((other) => {
|
|
51
|
+
if (other !== dropdown) {
|
|
52
|
+
other.classList.remove("active");
|
|
53
|
+
other.querySelector(".dropdown-menu")?.classList.remove("show");
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Toggle current
|
|
58
|
+
dropdown.classList.toggle("active");
|
|
59
|
+
menu.classList.toggle("show");
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Close dropdown when clicking outside
|
|
65
|
+
document.addEventListener("click", (e) => {
|
|
66
|
+
dropdowns.forEach((dropdown) => {
|
|
67
|
+
if (!dropdown.contains(e.target)) {
|
|
68
|
+
dropdown.classList.remove("active");
|
|
69
|
+
dropdown.querySelector(".dropdown-menu")?.classList.remove("show");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
// Dropdown Toggle End
|
|
75
|
+
|
|
76
|
+
// Authentication Check Start
|
|
77
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
78
|
+
const loginBtn = document.querySelector(".btn-login");
|
|
79
|
+
const isLoggedIn = localStorage.getItem("isLoggedIn");
|
|
80
|
+
|
|
81
|
+
if (loginBtn && isLoggedIn === "true") {
|
|
82
|
+
loginBtn.textContent = "Logout";
|
|
83
|
+
loginBtn.href = "#"; // Prevent immediate redirect
|
|
84
|
+
|
|
85
|
+
loginBtn.addEventListener("click", (e) => {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
localStorage.removeItem("isLoggedIn");
|
|
88
|
+
window.location.href = "./login.html";
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// Authentication Check End
|