news-cms-module 0.1.2 → 1.1.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.
@@ -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;">&lt;</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;">&gt;</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>