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,368 @@
|
|
|
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>Tambah 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="index.html" style="display: flex; justify-content: center;"><img src="../../img/logo.png"
|
|
16
|
+
alt="Logo DeNews" /></a>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<ul class="nav-menu">
|
|
20
|
+
<li class="nav-item">
|
|
21
|
+
<a href="index.html" class="nav-link">
|
|
22
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
23
|
+
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
|
24
|
+
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
|
25
|
+
</svg>
|
|
26
|
+
Dashboard
|
|
27
|
+
</a>
|
|
28
|
+
</li>
|
|
29
|
+
<li class="nav-item">
|
|
30
|
+
<a href="management.html" class="nav-link active">
|
|
31
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
32
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
33
|
+
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
34
|
+
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
35
|
+
</svg>
|
|
36
|
+
Manajemen Berita
|
|
37
|
+
</a>
|
|
38
|
+
</li>
|
|
39
|
+
</ul>
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
</nav>
|
|
43
|
+
|
|
44
|
+
<!-- Main Content -->
|
|
45
|
+
<main class="main-content">
|
|
46
|
+
<a href="/berita/cms-admin/list" class="back-link">
|
|
47
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
|
|
48
|
+
<polyline points="15 18 9 12 15 6"></polyline>
|
|
49
|
+
</svg>
|
|
50
|
+
Kembali
|
|
51
|
+
</a>
|
|
52
|
+
|
|
53
|
+
<div class="form-card" style="max-width: 100%;">
|
|
54
|
+
<form id="newsForm">
|
|
55
|
+
<h3>Detail Berita</h3>
|
|
56
|
+
<br />
|
|
57
|
+
|
|
58
|
+
<div class="form-group">
|
|
59
|
+
<label class="form-label">Judul <span class="text-red">*</span></label>
|
|
60
|
+
<input type="text" class="form-control" id="newsTitle" value="<%= data.title %>" required />
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="form-group">
|
|
64
|
+
<label class="form-label">Nama Author <span class="text-red">*</span></label>
|
|
65
|
+
<input type="text" class="form-control" id="newsAuthor" value="<%= data.authorName %>" required />
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="form-group">
|
|
69
|
+
<label class="form-label">Kategori <span class="text-red">*</span></label>
|
|
70
|
+
<select class="form-control" id="newsCategory" required>
|
|
71
|
+
<option value="<%= data.category %>"><%= data.category %></option>
|
|
72
|
+
|
|
73
|
+
<% const categories = ["Ekonomi", "Hiburan", "Olahraga", "Nasional"]; %>
|
|
74
|
+
|
|
75
|
+
<% categories.forEach(cat => { %>
|
|
76
|
+
<option value="<%= cat %>" <%= data.category === cat ? 'selected' : '' %>>
|
|
77
|
+
<%= cat %>
|
|
78
|
+
</option>
|
|
79
|
+
<% }); %>
|
|
80
|
+
|
|
81
|
+
</select>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="form-group">
|
|
85
|
+
<label class="form-label">Gambar Thumbnail (Kosongkan jika tidak ingin ganti)</label>
|
|
86
|
+
<div style="margin-bottom: 10px;">
|
|
87
|
+
<small>Thumbnail saat ini:</small><br>
|
|
88
|
+
<img src="<%= data.imagePath %>" alt="Thumbnail" style="height: 100px; border-radius: 8px;">
|
|
89
|
+
</div>
|
|
90
|
+
<div class="input-group">
|
|
91
|
+
<input type="file" class="form-control" id="newsThumbnail" accept="image/*" />
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<h4>Konten Berita</h4>
|
|
96
|
+
<div id="contentContainer">
|
|
97
|
+
<% data.contentBlocks.forEach((block, index) => { %>
|
|
98
|
+
<div class="content-block" data-type="<%= block.blockType.toLowerCase() %>">
|
|
99
|
+
<button type="button" class="remove-block-btn" onclick="this.parentElement.remove()">🗑️</button>
|
|
100
|
+
<label class="form-label">
|
|
101
|
+
<%= block.blockType === 'PARAGRAPH' ? '📝 Paragraf' :
|
|
102
|
+
block.blockType === 'VIDEO' ? '🎥 Video (URL)' :
|
|
103
|
+
block.blockType === 'IMAGE' ? '🖼️ Gambar Konten' : 'H2 Sub Heading' %>
|
|
104
|
+
</label>
|
|
105
|
+
|
|
106
|
+
<div>
|
|
107
|
+
<% if (block.blockType === 'PARAGRAPH') { %>
|
|
108
|
+
<textarea class="form-control" name="contentValue" rows="4"><%= block.contentValue %></textarea>
|
|
109
|
+
<% } else if (block.blockType === 'VIDEO') { %>
|
|
110
|
+
<input type="text" class="form-control mb-2" name="contentValue" value="<%= block.contentValue %>">
|
|
111
|
+
<input type="text" class="form-control" name="contentCaption" value="<%= block.caption %>">
|
|
112
|
+
<% } else if (block.blockType === 'IMAGE') { %>
|
|
113
|
+
<div class="mb-2">
|
|
114
|
+
<small>Gambar saat ini:</small><br>
|
|
115
|
+
<img src="<%= block.contentValue %>" style="height: 80px; margin-bottom: 5px;">
|
|
116
|
+
<input type="file" class="form-control mb-2" name="contentFile" accept="image/*">
|
|
117
|
+
<input type="hidden" name="existingImage" value="<%= block.contentValue %>">
|
|
118
|
+
</div>
|
|
119
|
+
<input type="text" class="form-control" name="contentCaption" value="<%= block.caption %>">
|
|
120
|
+
<% } else if (block.blockType === 'SUBHEADING') { %>
|
|
121
|
+
<input type="text" class="form-control" name="contentValue" value="<%= block.contentValue %>">
|
|
122
|
+
<% } %>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<% }); %>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Add Content Trigger -->
|
|
129
|
+
<div id="addContentTrigger" class="dashed-area" onclick="toggleOptions(true)">
|
|
130
|
+
+ Tambah Detail Berita
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<!-- Content Type Options -->
|
|
134
|
+
<div id="contentTypeOptions" class="content-type-selector" style="display: none;">
|
|
135
|
+
<button type="button" class="btn btn-primary" onclick="addBlock('paragraph')">📝 Paragraf</button>
|
|
136
|
+
<button type="button" class="btn btn-primary" onclick="addBlock('video')">🎥 Video</button>
|
|
137
|
+
<button type="button" class="btn btn-primary" onclick="addBlock('image')">🖼️ Gambar</button>
|
|
138
|
+
<button type="button" class="btn btn-primary" onclick="addBlock('subheading')">H2 Sub Heading</button>
|
|
139
|
+
<button type="button" class="btn btn-outline text-red" onclick="toggleOptions(false)">❌ Batal</button>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div class="form-actions">
|
|
143
|
+
<a href="management.html" class="btn btn-outline"
|
|
144
|
+
style="border-color: #007bff; color: #333; text-align: center; text-decoration: none;">
|
|
145
|
+
Batal
|
|
146
|
+
</a>
|
|
147
|
+
<button type="submit" class="btn btn-primary">Simpan</button>
|
|
148
|
+
</div>
|
|
149
|
+
</form>
|
|
150
|
+
</div>
|
|
151
|
+
</main>
|
|
152
|
+
|
|
153
|
+
<script>
|
|
154
|
+
const contentContainer = document.getElementById('contentContainer');
|
|
155
|
+
const addTrigger = document.getElementById('addContentTrigger');
|
|
156
|
+
const optionsDiv = document.getElementById('contentTypeOptions');
|
|
157
|
+
|
|
158
|
+
function toggleOptions(show) {
|
|
159
|
+
if (show) {
|
|
160
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
161
|
+
addTrigger.style.display = 'none';
|
|
162
|
+
optionsDiv.style.display = 'flex';
|
|
163
|
+
} else {
|
|
164
|
+
addTrigger.style.display = 'block';
|
|
165
|
+
optionsDiv.style.display = 'none';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createRemoveBtn(elementToRemove) {
|
|
170
|
+
const btn = document.createElement('button');
|
|
171
|
+
btn.className = 'remove-block-btn';
|
|
172
|
+
btn.innerHTML = '🗑️';
|
|
173
|
+
btn.type = 'button';
|
|
174
|
+
btn.onclick = () => elementToRemove.remove();
|
|
175
|
+
return btn;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function addBlock(type) {
|
|
179
|
+
const blockDiv = document.createElement('div');
|
|
180
|
+
blockDiv.className = 'content-block';
|
|
181
|
+
blockDiv.dataset.type = type;
|
|
182
|
+
|
|
183
|
+
blockDiv.appendChild(createRemoveBtn(blockDiv));
|
|
184
|
+
const label = document.createElement('label');
|
|
185
|
+
label.className = 'form-label';
|
|
186
|
+
|
|
187
|
+
// Labeling
|
|
188
|
+
const labels = {
|
|
189
|
+
'paragraph': '📝 Paragraf',
|
|
190
|
+
'video': '🎥 Video (URL)',
|
|
191
|
+
'image': '🖼️ Gambar Konten',
|
|
192
|
+
'subheading': 'H2 Sub Heading'
|
|
193
|
+
};
|
|
194
|
+
label.textContent = labels[type];
|
|
195
|
+
|
|
196
|
+
let inputContainer = document.createElement('div');
|
|
197
|
+
|
|
198
|
+
if (type === 'paragraph') {
|
|
199
|
+
const input = document.createElement('textarea');
|
|
200
|
+
input.className = 'form-control';
|
|
201
|
+
input.name = 'contentValue';
|
|
202
|
+
input.rows = 4;
|
|
203
|
+
input.placeholder = 'Tulis paragraf disini...';
|
|
204
|
+
inputContainer.appendChild(input);
|
|
205
|
+
|
|
206
|
+
} else if (type === 'video') {
|
|
207
|
+
const urlInput = document.createElement('input');
|
|
208
|
+
urlInput.type = 'text';
|
|
209
|
+
urlInput.className = 'form-control mb-2';
|
|
210
|
+
urlInput.name = 'contentValue';
|
|
211
|
+
urlInput.placeholder = 'Masukkan URL Video YouTube...';
|
|
212
|
+
|
|
213
|
+
const capInput = document.createElement('input');
|
|
214
|
+
capInput.type = 'text';
|
|
215
|
+
capInput.className = 'form-control';
|
|
216
|
+
capInput.name = 'contentCaption';
|
|
217
|
+
capInput.placeholder = 'Keterangan video (opsional)...';
|
|
218
|
+
|
|
219
|
+
inputContainer.appendChild(urlInput);
|
|
220
|
+
inputContainer.appendChild(capInput);
|
|
221
|
+
|
|
222
|
+
} else if (type === 'image') {
|
|
223
|
+
const fileInput = document.createElement('input');
|
|
224
|
+
fileInput.type = 'file';
|
|
225
|
+
fileInput.className = 'form-control mb-2';
|
|
226
|
+
fileInput.name = 'contentFile'; // Hanya sementara di FE
|
|
227
|
+
fileInput.accept = 'image/*';
|
|
228
|
+
|
|
229
|
+
const capInput = document.createElement('input');
|
|
230
|
+
capInput.type = 'text';
|
|
231
|
+
capInput.className = 'form-control';
|
|
232
|
+
capInput.name = 'contentCaption';
|
|
233
|
+
capInput.placeholder = 'Keterangan gambar (opsional)...';
|
|
234
|
+
|
|
235
|
+
inputContainer.appendChild(fileInput);
|
|
236
|
+
inputContainer.appendChild(capInput);
|
|
237
|
+
|
|
238
|
+
} else if (type === 'subheading') {
|
|
239
|
+
const input = document.createElement('input');
|
|
240
|
+
input.type = 'text';
|
|
241
|
+
input.className = 'form-control';
|
|
242
|
+
input.name = 'contentValue';
|
|
243
|
+
input.placeholder = 'Judul Sub-heading...';
|
|
244
|
+
inputContainer.appendChild(input);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
blockDiv.appendChild(label);
|
|
248
|
+
blockDiv.appendChild(inputContainer);
|
|
249
|
+
contentContainer.appendChild(blockDiv);
|
|
250
|
+
toggleOptions(false);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Handler Submit Form
|
|
254
|
+
document.getElementById('newsForm').addEventListener('submit', async function (e) {
|
|
255
|
+
e.preventDefault();
|
|
256
|
+
|
|
257
|
+
console.log("step 1");
|
|
258
|
+
|
|
259
|
+
const formData = new FormData();
|
|
260
|
+
|
|
261
|
+
// 1. Ambil data utama sesuai kunci di Controller BE
|
|
262
|
+
const title = document.getElementById('newsTitle').value;
|
|
263
|
+
const authorName = document.getElementById('newsAuthor').value;
|
|
264
|
+
const category = document.getElementById('newsCategory').value;
|
|
265
|
+
const status = 'PUBLISHED'; // Atau sesuaikan jika ada inputnya
|
|
266
|
+
const thumbnailFile = document.getElementById('newsThumbnail').files[0];
|
|
267
|
+
|
|
268
|
+
console.log("step 2");
|
|
269
|
+
|
|
270
|
+
formData.append('title', title);
|
|
271
|
+
formData.append('authorName', authorName);
|
|
272
|
+
formData.append('category', category);
|
|
273
|
+
formData.append('status', status);
|
|
274
|
+
|
|
275
|
+
console.table(title, authorName, category, status);
|
|
276
|
+
|
|
277
|
+
console.log("step 3");
|
|
278
|
+
|
|
279
|
+
if (thumbnailFile) {
|
|
280
|
+
formData.append('thumbnailImage', thumbnailFile); // Sesuai BE: req.files['thumbnailImage']
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log("step 4");
|
|
284
|
+
|
|
285
|
+
// 2. Olah Content Blocks
|
|
286
|
+
const blocks = contentContainer.querySelectorAll('.content-block');
|
|
287
|
+
const contentBlocksData = [];
|
|
288
|
+
|
|
289
|
+
console.log("step 5");
|
|
290
|
+
|
|
291
|
+
blocks.forEach((block) => {
|
|
292
|
+
let type = block.dataset.type;
|
|
293
|
+
type = type.toUpperCase();
|
|
294
|
+
const item = { blockType: type };
|
|
295
|
+
|
|
296
|
+
if (type === 'PARAGRAPH' || type === 'SUBHEADING' || type === 'VIDEO') {
|
|
297
|
+
item.contentValue = block.querySelector('[name="contentValue"]').value;
|
|
298
|
+
console.log(item.value);
|
|
299
|
+
|
|
300
|
+
if (type === 'VIDEO') {
|
|
301
|
+
item.caption = block.querySelector('[name="contentCaption"]').value;
|
|
302
|
+
console.log(item.caption);
|
|
303
|
+
|
|
304
|
+
}
|
|
305
|
+
} else if (type === 'IMAGE') {
|
|
306
|
+
const fileInput = block.querySelector('[name="contentFile"]');
|
|
307
|
+
const existingImagePath = block.querySelector('[name="existingImage"]');
|
|
308
|
+
const captionInput = block.querySelector('[name="contentCaption"]');
|
|
309
|
+
// if (fileInput.files.length > 0) {
|
|
310
|
+
// // Masukkan file ke key 'contentImages' sesuai BE
|
|
311
|
+
// formData.append('contentImages', fileInput.files[0]);
|
|
312
|
+
// item.value = fileInput.files[0].name; // Referensi nama file
|
|
313
|
+
// }
|
|
314
|
+
// item.caption = block.querySelector('[name="contentCaption"]').value;
|
|
315
|
+
if (fileInput && fileInput.files.length > 0) {
|
|
316
|
+
// Jika ada file baru yang diupload
|
|
317
|
+
formData.append('contentImages', fileInput.files[0]);
|
|
318
|
+
item.contentValue = fileInput.files[0].name; // Ini hanya sebagai tanda ada file
|
|
319
|
+
} else if (existingImagePath) {
|
|
320
|
+
// JIKA TIDAK ADA FILE BARU: Kirimkan path lama agar lolos validasi
|
|
321
|
+
item.contentValue = existingImagePath.value;
|
|
322
|
+
} else {
|
|
323
|
+
item.contentValue = ""; // Akan memicu error validasi karena benar-benar kosong
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
item.caption = captionInput ? captionInput.value : "";
|
|
327
|
+
}
|
|
328
|
+
contentBlocksData.push(item);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
console.log(contentBlocksData);
|
|
332
|
+
|
|
333
|
+
console.log("step 6");
|
|
334
|
+
|
|
335
|
+
// Kirim metadata blok sebagai string JSON (Standar multipart/form-data untuk array of objects)
|
|
336
|
+
formData.append('contentBlocks', JSON.stringify(contentBlocksData));
|
|
337
|
+
|
|
338
|
+
console.log("step 7");
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
let url = 'http://localhost:3000/berita/cms-admin/update/' + '<%= data.id %>';
|
|
342
|
+
const response = await fetch( url, {
|
|
343
|
+
method: 'PUT',
|
|
344
|
+
body: formData
|
|
345
|
+
// Jangan set Header Content-Type, browser akan otomatis mengurusnya untuk FormData
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
console.log("step 8");
|
|
349
|
+
|
|
350
|
+
const result = await response.json();
|
|
351
|
+
|
|
352
|
+
console.log("step 9");
|
|
353
|
+
|
|
354
|
+
if (response.ok && result.success) {
|
|
355
|
+
alert('Berita berhasil disimpan!');
|
|
356
|
+
window.location.href = '/berita/cms-admin/list';
|
|
357
|
+
} else {
|
|
358
|
+
alert('Gagal membuat berita: ' + (result.error || 'Terjadi kesalahan server'));
|
|
359
|
+
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error('Error saat submit:', error);
|
|
362
|
+
alert('Terjadi kesalahan koneksi ke server. ' + error.message);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
</script>
|
|
366
|
+
</body>
|
|
367
|
+
|
|
368
|
+
</html>
|
package/views/detail.ejs
CHANGED
|
@@ -0,0 +1,235 @@
|
|
|
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>
|
|
8
|
+
<%= news.title %> - DeNews
|
|
9
|
+
</title>
|
|
10
|
+
|
|
11
|
+
<!-- Font Awesome -->
|
|
12
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
|
13
|
+
|
|
14
|
+
<!-- CSS Styles -->
|
|
15
|
+
<link rel="stylesheet" href="/berita/style.css" />
|
|
16
|
+
</head>
|
|
17
|
+
|
|
18
|
+
<body>
|
|
19
|
+
<!-- Public Header -->
|
|
20
|
+
<nav class="navbar">
|
|
21
|
+
<i class="fa-solid fa-bars" id="menu-icon"></i>
|
|
22
|
+
<!-- Logo -->
|
|
23
|
+
<div class="logo">
|
|
24
|
+
<a href="/berita/list"><img src="/berita/img/logo.png" alt="Logo DeNews" /></a>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Menu Navbar -->
|
|
28
|
+
<ul class="menu">
|
|
29
|
+
<li>
|
|
30
|
+
<a href="/berita/list" class="<%= !query.category ? 'active' : '' %>">Beranda</a>
|
|
31
|
+
</li>
|
|
32
|
+
<li class="dropdown">
|
|
33
|
+
<a href="#" class="dropdown-toggle <%= (news && news.category) || query.category ? 'active' : '' %>">
|
|
34
|
+
Kategori <i class="fa-solid fa-chevron-down"></i>
|
|
35
|
+
</a>
|
|
36
|
+
<ul class="dropdown-menu">
|
|
37
|
+
<% categories.forEach(cat=> { %>
|
|
38
|
+
<li>
|
|
39
|
+
<a href="/berita/list?category=<%= cat %>"
|
|
40
|
+
class="<%= (news && news.category === cat) || query.category === cat ? 'dropdown-active' : '' %>">
|
|
41
|
+
<%= cat.charAt(0).toUpperCase() + cat.slice(1) %>
|
|
42
|
+
</a>
|
|
43
|
+
</li>
|
|
44
|
+
<% }); %>
|
|
45
|
+
</ul>
|
|
46
|
+
</li>
|
|
47
|
+
</ul>
|
|
48
|
+
|
|
49
|
+
<!-- Search and Login -->
|
|
50
|
+
<div class="search-login">
|
|
51
|
+
<!-- Search Bar -->
|
|
52
|
+
<div class="search-bar">
|
|
53
|
+
<i class="fa-solid fa-magnifying-glass"></i>
|
|
54
|
+
<div class="input-box">
|
|
55
|
+
<input type="text" placeholder="Search..." />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</nav>
|
|
60
|
+
|
|
61
|
+
<main class="container">
|
|
62
|
+
<div class="breadcrumb" style="margin-bottom: 20px; color: #666; font-size: 14px;">
|
|
63
|
+
<a href="/berita/list" style="color: #666; text-decoration: none;">Beranda</a> >
|
|
64
|
+
<a href="/berita/list?category=<%= news.category %>"
|
|
65
|
+
style="color: var(--primary); text-decoration: none; font-weight: 600;">
|
|
66
|
+
<%= news.category %>
|
|
67
|
+
</a>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="news-layout">
|
|
71
|
+
<!-- Main Article Content -->
|
|
72
|
+
<article class="main-article-content">
|
|
73
|
+
<!-- Hero Image -->
|
|
74
|
+
<% if (news.imagePath) { %>
|
|
75
|
+
<img src="<%= news.imagePath.replace('public/', '/berita/') %>" alt="<%= news.title %>"
|
|
76
|
+
class="detail-image" style="margin-top: 0; max-height: 400px; object-fit: cover; width: 100%;">
|
|
77
|
+
<% } %>
|
|
78
|
+
|
|
79
|
+
<header class="detail-header">
|
|
80
|
+
<h1 class="detail-title" style="font-size: 32px; margin: 20px 0;">
|
|
81
|
+
<%= news.title %>
|
|
82
|
+
</h1>
|
|
83
|
+
<div class="detail-meta">
|
|
84
|
+
<span style="display: flex; align-items: center; gap: 5px">
|
|
85
|
+
<i class="fa-solid fa-user-circle"></i>
|
|
86
|
+
Oleh: <%= news.authorName %>
|
|
87
|
+
</span>
|
|
88
|
+
<span>|</span>
|
|
89
|
+
<span>
|
|
90
|
+
<%= new Date(news.createdAt).toLocaleDateString('id-ID', { day: 'numeric' ,
|
|
91
|
+
month: 'long' , year: 'numeric' }) %>
|
|
92
|
+
</span>
|
|
93
|
+
<span>|</span>
|
|
94
|
+
<span>Kategori: <%= news.category %></span>
|
|
95
|
+
</div>
|
|
96
|
+
</header>
|
|
97
|
+
|
|
98
|
+
<div class="detail-content">
|
|
99
|
+
<% if (news.contentBlocks && news.contentBlocks.length> 0) { %>
|
|
100
|
+
<% news.contentBlocks.forEach(function(block) { %>
|
|
101
|
+
<% if (block.blockType==='PARAGRAPH' ) { %>
|
|
102
|
+
<p>
|
|
103
|
+
<%= block.contentValue %>
|
|
104
|
+
</p>
|
|
105
|
+
<% } else if (block.blockType==='SUBHEADING' ) { %>
|
|
106
|
+
<h2
|
|
107
|
+
style="font-size: 24px; font-weight: bold; margin-top: 30px; margin-bottom: 15px;">
|
|
108
|
+
<%= block.contentValue %>
|
|
109
|
+
</h2>
|
|
110
|
+
<% } else if (block.blockType==='IMAGE' ) { %>
|
|
111
|
+
<figure style="margin: 30px 0;">
|
|
112
|
+
<img src="<%= block.contentValue.replace('public/', '/berita/') %>"
|
|
113
|
+
class="detail-image"
|
|
114
|
+
alt="<%= block.caption || 'Gambar Berita' %>"
|
|
115
|
+
style="width: 100%; border-radius: 8px;">
|
|
116
|
+
<% if (block.caption) { %>
|
|
117
|
+
<figcaption class="caption"
|
|
118
|
+
style="text-align: center; margin-top: 10px; color: #666; font-size: 0.9em;">
|
|
119
|
+
<%= block.caption %>
|
|
120
|
+
</figcaption>
|
|
121
|
+
<% } %>
|
|
122
|
+
</figure>
|
|
123
|
+
<% } else if (block.blockType==='VIDEO' ) { %>
|
|
124
|
+
<div class="video-container" style="margin: 30px 0;">
|
|
125
|
+
<iframe width="100%" height="400"
|
|
126
|
+
src="<%= block.contentValue %>" frameborder="0"
|
|
127
|
+
allowfullscreen style="border-radius: 8px;"></iframe>
|
|
128
|
+
<% if (block.caption) { %>
|
|
129
|
+
<p class="caption"
|
|
130
|
+
style="text-align: center; margin-top: 10px; color: #666; font-size: 0.9em;">
|
|
131
|
+
<%= block.caption %>
|
|
132
|
+
</p>
|
|
133
|
+
<% } %>
|
|
134
|
+
</div>
|
|
135
|
+
<% } %>
|
|
136
|
+
<% }); %>
|
|
137
|
+
<% } else { %>
|
|
138
|
+
<p>Konten berita tidak tersedia.</p>
|
|
139
|
+
<% } %>
|
|
140
|
+
</div>
|
|
141
|
+
</article>
|
|
142
|
+
|
|
143
|
+
<!-- Sidebar -->
|
|
144
|
+
<aside>
|
|
145
|
+
<div class="sidebar-section">
|
|
146
|
+
<h2 class="section-title">Berita Lainnya</h2>
|
|
147
|
+
<div class="sidebar-list">
|
|
148
|
+
<% if (recommendation && recommendation.length> 0) { %>
|
|
149
|
+
<% recommendation.forEach(function(item) { %>
|
|
150
|
+
<a href="/berita/post/<%= item.slug %>" class="mini-card">
|
|
151
|
+
<img src="<%= item.imagePath ? item.imagePath.replace('public/', '/berita/') : '/berita/img/berita.png' %>"
|
|
152
|
+
class="mini-thumb" alt="<%= item.title %>"
|
|
153
|
+
onerror="this.src='/berita/img/berita.png'">
|
|
154
|
+
<div class="mini-content">
|
|
155
|
+
<h4 class="mini-title">
|
|
156
|
+
<%= item.title %>
|
|
157
|
+
</h4>
|
|
158
|
+
<p class="news-meta">
|
|
159
|
+
<%= new Date(item.createdAt).toLocaleDateString('id-ID', { day: 'numeric' ,
|
|
160
|
+
month: 'short' }) %>
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
</a>
|
|
164
|
+
<% }); %>
|
|
165
|
+
<% } else { %>
|
|
166
|
+
<p style="color: #888; font-style: italic;">Belum ada berita rekomendasi.</p>
|
|
167
|
+
<% } %>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</aside>
|
|
171
|
+
</div>
|
|
172
|
+
</main>
|
|
173
|
+
|
|
174
|
+
<!-- Footer -->
|
|
175
|
+
<footer class="site-footer">
|
|
176
|
+
<div class="footer-container">
|
|
177
|
+
<div class="footer-top">
|
|
178
|
+
<div class="footer-brand">
|
|
179
|
+
<div class="footer-logo">
|
|
180
|
+
<img src="/berita/img/logo.png" alt="DeNews Logo" class="logo" />
|
|
181
|
+
</div>
|
|
182
|
+
<p class="footer-desc">
|
|
183
|
+
DeNews adalah platform berita digital yang menyajikan informasi
|
|
184
|
+
akurat, cepat, dan terpercaya.
|
|
185
|
+
</p>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="footer-links">
|
|
189
|
+
<h3 class="footer-heading">Browse by Category</h3>
|
|
190
|
+
<ul class="category-grid">
|
|
191
|
+
<li><a href="/berita/list?category=Nasional">Nasional</a></li>
|
|
192
|
+
<li><a href="/berita/list?category=Teknologi">Teknologi</a></li>
|
|
193
|
+
<li><a href="/berita/list?category=Pendidikan">Pendidikan</a></li>
|
|
194
|
+
<li><a href="/berita/list?category=Internasional">Internasional</a></li>
|
|
195
|
+
<li><a href="/berita/list?category=Hiburan">Hiburan</a></li>
|
|
196
|
+
<li><a href="/berita/list?category=Kesehatan">Kesehatan</a></li>
|
|
197
|
+
<li><a href="/berita/list?category=Olahraga">Olahraga</a></li>
|
|
198
|
+
<li><a href="/berita/list?category=Bisnis">Bisnis</a></li>
|
|
199
|
+
</ul>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="footer-social">
|
|
203
|
+
<h3 class="footer-heading text-white">Follow Us</h3>
|
|
204
|
+
<div class="social-icons">
|
|
205
|
+
<a href="#" aria-label="Facebook"><i class="fab fa-facebook-square"></i></a>
|
|
206
|
+
<a href="#" aria-label="Twitter"><i class="fab fa-twitter-square"></i></a>
|
|
207
|
+
<a href="#" aria-label="Instagram"><i class="fab fa-instagram-square"></i></a>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<hr class="footer-divider" />
|
|
213
|
+
|
|
214
|
+
<div class="footer-bottom">
|
|
215
|
+
<div class="copyright">
|
|
216
|
+
© 2025 DeNews. Semua hak cipta dilindungi undang-undang.
|
|
217
|
+
</div>
|
|
218
|
+
<div class="legal-links">
|
|
219
|
+
<a href="#">Redaksi</a>
|
|
220
|
+
<span class="separator">|</span>
|
|
221
|
+
<a href="#">Pedoman Media Siber</a>
|
|
222
|
+
<span class="separator">|</span>
|
|
223
|
+
<a href="#">Kebijakan Privasi</a>
|
|
224
|
+
<span class="separator">|</span>
|
|
225
|
+
<a href="#">Disclaimer</a>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</footer>
|
|
230
|
+
|
|
231
|
+
<!-- Javascript -->
|
|
232
|
+
<script src="/berita/js/script.js"></script>
|
|
233
|
+
</body>
|
|
234
|
+
|
|
235
|
+
</html>
|