news-cms-module 0.1.2 → 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.
@@ -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
+ &copy; 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>