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
package/views/admin/list.ejs
CHANGED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="id">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Manajemen Berita - DeNews</title>
|
|
8
|
+
<link rel="stylesheet" href="/berita/style.css" />
|
|
9
|
+
</head>
|
|
10
|
+
|
|
11
|
+
<body>
|
|
12
|
+
<!-- Sidebar -->
|
|
13
|
+
<nav class="sidebar">
|
|
14
|
+
<div class="logo">
|
|
15
|
+
<a href="./dashboard" style="display: flex; justify-content: center;">
|
|
16
|
+
<img src="/berita/img/logo.png" alt="Logo DeNews" />
|
|
17
|
+
</a>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<ul class="nav-menu">
|
|
21
|
+
<li class="nav-item">
|
|
22
|
+
<a href="./dashboard" class="nav-link">
|
|
23
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
24
|
+
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
|
25
|
+
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
|
26
|
+
</svg>
|
|
27
|
+
Dashboard
|
|
28
|
+
</a>
|
|
29
|
+
</li>
|
|
30
|
+
<li class="nav-item">
|
|
31
|
+
<a href="./list" class="nav-link active">
|
|
32
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
33
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
34
|
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
35
|
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
36
|
+
</svg>
|
|
37
|
+
Manajemen Berita
|
|
38
|
+
</a>
|
|
39
|
+
</li>
|
|
40
|
+
</ul>
|
|
41
|
+
</nav>
|
|
42
|
+
|
|
43
|
+
<!-- Main Content -->
|
|
44
|
+
<main class="main-content">
|
|
45
|
+
<div class="toolbar">
|
|
46
|
+
<div class="filter-group filter-dropdown">
|
|
47
|
+
<button class="btn btn-primary btn-filter" onclick="toggleFilterMenu(event)">
|
|
48
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
49
|
+
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
|
|
50
|
+
</svg>
|
|
51
|
+
Filter
|
|
52
|
+
</button>
|
|
53
|
+
<div class="filter-menu" id="filterMenu" onclick="event.stopPropagation()">
|
|
54
|
+
<form action="" method="GET" id="filterForm">
|
|
55
|
+
<% if (query.title) { %>
|
|
56
|
+
<input type="hidden" name="title" value="<%= query.title %>">
|
|
57
|
+
<% } %>
|
|
58
|
+
<div class="filter-section">
|
|
59
|
+
<div class="filter-header">Kategori</div>
|
|
60
|
+
<div class="filter-options">
|
|
61
|
+
<% categories.forEach(cat=> { %>
|
|
62
|
+
<label class="filter-label">
|
|
63
|
+
<input type="radio" name="category" value="<%= cat %>"
|
|
64
|
+
<%=query.category===cat ? 'checked' : '' %>>
|
|
65
|
+
<%= cat.charAt(0).toUpperCase() + cat.slice(1) %>
|
|
66
|
+
</label>
|
|
67
|
+
<% }); %>
|
|
68
|
+
<label class="filter-label">
|
|
69
|
+
<input type="radio" name="category" value="" <%=!query.category
|
|
70
|
+
? 'checked' : '' %>> Semua
|
|
71
|
+
</label>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="filter-section">
|
|
75
|
+
<div class="filter-header">Status</div>
|
|
76
|
+
<div class="filter-options">
|
|
77
|
+
<label class="filter-label"><input type="radio" name="status" value="PUBLISHED"
|
|
78
|
+
<%=query.status==='PUBLISHED' ? 'checked' : '' %>> Published</label>
|
|
79
|
+
<label class="filter-label"><input type="radio" name="status" value="DRAFT"
|
|
80
|
+
<%=query.status==='DRAFT' ? 'checked' : '' %>> Draft</label>
|
|
81
|
+
<label class="filter-label"><input type="radio" name="status" value="ARCHIVED"
|
|
82
|
+
<%=query.status==='ARCHIVED' ? 'checked' : '' %>> Archived</label>
|
|
83
|
+
<label class="filter-label"><input type="radio" name="status" value=""
|
|
84
|
+
<%=!query.status ? 'checked' : '' %>> Semua</label>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<div style="margin-top: 15px; display: flex; justify-content: flex-end; gap: 8px;">
|
|
88
|
+
<a href="?" class="btn btn-outline"
|
|
89
|
+
style="padding: 5px 10px; font-size: 12px; text-decoration: none; color: #666; border: 1px solid #ccc; border-radius: 4px;">Reset</a>
|
|
90
|
+
<button type="submit" class="btn btn-primary"
|
|
91
|
+
style="padding: 5px 10px; font-size: 12px;">Terapkan</button>
|
|
92
|
+
</div>
|
|
93
|
+
</form>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<form class="search-box" action="" method="GET">
|
|
98
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#ccc" stroke-width="2">
|
|
99
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
100
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
|
101
|
+
</svg>
|
|
102
|
+
<input type="text" class="search-input" name="title" placeholder="Telusuri Judul..."
|
|
103
|
+
value="<%= query.title %>" />
|
|
104
|
+
<% if (query.category) { %><input type="hidden" name="category" value="<%= query.category %>">
|
|
105
|
+
<% } %>
|
|
106
|
+
<% if (query.status) { %><input type="hidden" name="status" value="<%= query.status %>">
|
|
107
|
+
<% } %>
|
|
108
|
+
<button type="submit" class="btn btn-primary"
|
|
109
|
+
style="padding: 5px 15px; margin-right: 5px">
|
|
110
|
+
Cari
|
|
111
|
+
</button>
|
|
112
|
+
</form>
|
|
113
|
+
|
|
114
|
+
<a href="create" class="btn btn-primary"
|
|
115
|
+
style="text-decoration: none; display: flex; align-items: center; gap: 8px;">
|
|
116
|
+
<span style="font-size: 18px; font-weight: bold;">+</span> Tambah Berita
|
|
117
|
+
</a>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="table-container">
|
|
121
|
+
<table class="table">
|
|
122
|
+
<thead>
|
|
123
|
+
<tr>
|
|
124
|
+
<th class="col-title">Judul Berita</th>
|
|
125
|
+
<th class="col-author">Nama Author</th>
|
|
126
|
+
<th class="col-category">Kategori</th>
|
|
127
|
+
<th class="col-status">Status</th>
|
|
128
|
+
<th class="col-actions">Aksi</th>
|
|
129
|
+
</tr>
|
|
130
|
+
</thead>
|
|
131
|
+
<tbody>
|
|
132
|
+
<% if (posts && posts.length> 0) { %>
|
|
133
|
+
<% posts.forEach(post=> { %>
|
|
134
|
+
<tr>
|
|
135
|
+
<td class="col-title">
|
|
136
|
+
<span class="truncate" title="<%= post.title %>">
|
|
137
|
+
<%= post.title %>
|
|
138
|
+
</span>
|
|
139
|
+
</td>
|
|
140
|
+
<td class="col-author">
|
|
141
|
+
<%= post.authorName %>
|
|
142
|
+
</td>
|
|
143
|
+
<td class="col-category" style="text-transform: capitalize;">
|
|
144
|
+
<%= post.category %>
|
|
145
|
+
</td>
|
|
146
|
+
<td class="col-status">
|
|
147
|
+
<div class="dropdown">
|
|
148
|
+
<button class="badge badge-<%= post.status.toLowerCase() %> dropdown-toggle"
|
|
149
|
+
type="button" onclick="toggleDropdown(this)">
|
|
150
|
+
<%= post.status %>
|
|
151
|
+
</button>
|
|
152
|
+
<div class="dropdown-menu">
|
|
153
|
+
<div class="dropdown-item"
|
|
154
|
+
onclick="updateNewsStatus('<%= post.id %>', 'PUBLISHED')">
|
|
155
|
+
<span class="status-dot published"></span> Publish
|
|
156
|
+
</div>
|
|
157
|
+
<div class="dropdown-item"
|
|
158
|
+
onclick="updateNewsStatus('<%= post.id %>', 'DRAFT')">
|
|
159
|
+
<span class="status-dot draft"></span> Draft
|
|
160
|
+
</div>
|
|
161
|
+
<div class="dropdown-item"
|
|
162
|
+
onclick="updateNewsStatus('<%= post.id %>', 'ARCHIVED')">
|
|
163
|
+
<span class="status-dot archived"></span> Archived
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</td>
|
|
168
|
+
<td>
|
|
169
|
+
<div class="action-buttons">
|
|
170
|
+
<a href="<%= post.slug %>" class="btn-icon text-blue" title="Lihat">👁️</a>
|
|
171
|
+
<a href="update/<%= post.slug %>" class="btn-icon text-orange" title="Edit">✏️</a>
|
|
172
|
+
<button class="btn-icon text-red" title="Hapus"
|
|
173
|
+
onclick="deletePost('<%= post.id %>')">🗑️</button>
|
|
174
|
+
</div>
|
|
175
|
+
</td>
|
|
176
|
+
</tr>
|
|
177
|
+
<% }); %>
|
|
178
|
+
<% } else { %>
|
|
179
|
+
<tr>
|
|
180
|
+
<td colspan="5" style="text-align: center; padding: 30px; color: #888;">Belum
|
|
181
|
+
ada berita yang tersedia.</td>
|
|
182
|
+
</tr>
|
|
183
|
+
<% } %>
|
|
184
|
+
</tbody>
|
|
185
|
+
</table>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="pagination">
|
|
189
|
+
<% if (pagination.hasPrevPage) { %>
|
|
190
|
+
<a href="?page=<%= pagination.currentPage - 1 %>&title=<%= query.title %>&category=<%= query.category %>&status=<%= query.status %>"
|
|
191
|
+
class="page-item" style="text-decoration: none; color: inherit;"><</a>
|
|
192
|
+
<% } %>
|
|
193
|
+
|
|
194
|
+
<% for (let i=1; i <=pagination.totalPages; i++) { %>
|
|
195
|
+
<a href="?page=<%= i %>&title=<%= query.title %>&category=<%= query.category %>&status=<%= query.status %>"
|
|
196
|
+
class="page-item <%= pagination.currentPage === i ? 'active' : '' %>"
|
|
197
|
+
style="text-decoration: none;">
|
|
198
|
+
<%= i %>
|
|
199
|
+
</a>
|
|
200
|
+
<% } %>
|
|
201
|
+
|
|
202
|
+
<% if (pagination.hasNextPage) { %>
|
|
203
|
+
<a href="?page=<%= pagination.currentPage + 1 %>&title=<%= query.title %>&category=<%= query.category %>&status=<%= query.status %>"
|
|
204
|
+
class="page-item" style="text-decoration: none; color: inherit;">></a>
|
|
205
|
+
<% } %>
|
|
206
|
+
</div>
|
|
207
|
+
</main>
|
|
208
|
+
|
|
209
|
+
<script>
|
|
210
|
+
|
|
211
|
+
// Close dropdowns when clicking outside
|
|
212
|
+
window.onclick = function (event) {
|
|
213
|
+
if (!event.target.closest('.dropdown')) {
|
|
214
|
+
var dropdowns = document.getElementsByClassName("dropdown-menu");
|
|
215
|
+
for (var i = 0; i < dropdowns.length; i++) {
|
|
216
|
+
var openDropdown = dropdowns[i];
|
|
217
|
+
if (openDropdown.classList.contains('show')) {
|
|
218
|
+
openDropdown.classList.remove('show');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function toggleDropdown(button) {
|
|
225
|
+
// Close all other dropdowns first
|
|
226
|
+
var dropdowns = document.getElementsByClassName("dropdown-menu");
|
|
227
|
+
for (var i = 0; i < dropdowns.length; i++) {
|
|
228
|
+
if (dropdowns[i] !== button.nextElementSibling) {
|
|
229
|
+
dropdowns[i].classList.remove('show');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Toggle the clicked one
|
|
233
|
+
button.nextElementSibling.classList.toggle("show");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function updateNewsStatus(id, newStatus) {
|
|
237
|
+
if (!confirm(`Ubah status menjadi ${newStatus}?`)) return;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const response = await fetch(`update/${id}`, {
|
|
241
|
+
method: 'PATCH',
|
|
242
|
+
headers: { 'Content-Type': 'application/json' },
|
|
243
|
+
body: JSON.stringify({ status: newStatus })
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const result = await response.json();
|
|
247
|
+
if (result.success) {
|
|
248
|
+
location.reload();
|
|
249
|
+
} else {
|
|
250
|
+
alert('Gagal memperbarui status: ' + result.error);
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error(error);
|
|
254
|
+
alert('Terjadi kesalahan saat memperbarui status.');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// async function deleteNews(id) {
|
|
259
|
+
// if (!confirm('Apakah Anda yakin ingin menghapus berita ini? Tindakan ini tidak dapat dibatalkan.')) return;
|
|
260
|
+
|
|
261
|
+
// try {
|
|
262
|
+
// const response = await fetch(`/berita/admin/delete/${id}`, {
|
|
263
|
+
// method: 'DELETE'
|
|
264
|
+
// });
|
|
265
|
+
|
|
266
|
+
// const result = await response.json();
|
|
267
|
+
// if (result.success) {
|
|
268
|
+
// location.reload();
|
|
269
|
+
// } else {
|
|
270
|
+
// alert('Gagal menghapus berita: ' + result.message || result.error);
|
|
271
|
+
// }
|
|
272
|
+
// } catch (error) {
|
|
273
|
+
// console.error(error);
|
|
274
|
+
// alert('Terjadi kesalahan saat menghapus berita.');
|
|
275
|
+
// }
|
|
276
|
+
// }
|
|
277
|
+
|
|
278
|
+
// Filter Logic
|
|
279
|
+
function toggleFilterMenu(e) {
|
|
280
|
+
const menu = document.getElementById('filterMenu');
|
|
281
|
+
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
|
282
|
+
e.stopPropagation();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Close Filter Menu when clicking outside
|
|
286
|
+
window.addEventListener('click', function (e) {
|
|
287
|
+
const filterMenu = document.getElementById('filterMenu');
|
|
288
|
+
if (filterMenu && !e.target.closest('.filter-dropdown')) {
|
|
289
|
+
filterMenu.style.display = 'none';
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
async function deletePost(id) {
|
|
294
|
+
// 1. Konfirmasi ke user
|
|
295
|
+
if (!confirm('Apakah Anda yakin ingin menghapus berita ini?')) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
// 2. Kirim permintaan hapus ke backend
|
|
301
|
+
const response = await fetch(`delete/${id}`, {
|
|
302
|
+
method: 'DELETE',
|
|
303
|
+
headers: {
|
|
304
|
+
'Content-Type': 'application/json'
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Cek jika response bukan JSON (menghindari error Unexpected Token <)
|
|
309
|
+
const contentType = response.headers.get("content-type");
|
|
310
|
+
if (!contentType || !contentType.includes("application/json")) {
|
|
311
|
+
throw new Error("Server mengirim respon yang salah (bukan JSON)");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const result = await response.json();
|
|
315
|
+
|
|
316
|
+
if (result.success) {
|
|
317
|
+
alert('Berhasil: ' + result.message);
|
|
318
|
+
// 3. Refresh halaman agar data yang dihapus hilang dari tabel
|
|
319
|
+
window.location.reload();
|
|
320
|
+
} else {
|
|
321
|
+
alert('Gagal: ' + result.message);
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('Error:', error);
|
|
325
|
+
alert('Terjadi kesalahan: ' + error.message);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
</script>
|
|
329
|
+
</body>
|
|
330
|
+
|
|
331
|
+
</html>
|