diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..ab6bbcd --- /dev/null +++ b/css/style.css @@ -0,0 +1,87 @@ +html { + color: black; + font: 16px "Roboto", sans-serif; +} + +h1 { + font-size: 1.6em; + line-height: 1.4; +} + +h2 { + font-size: 1.25em; +} + +h3 { + font-size: 1.2em; +} + +main { + margin-top: 56.8px; +} + +header { + box-shadow: 0 4px 5px 0 #00000033; +} + +a { + color: inherit; + text-decoration: none; +} + +a:hover { + color: inherit; + text-decoration: underline 7.5% !important; +} + +button[data-bs-target="#filter-modal"].filtered i { + color: #00c200 !important; +} + +.col-form-label { + line-height: 1; +} + +#topics-bar label:hover { + cursor: pointer; + background-color: #9999993b !important; +} + +#topics-bar label.active { + background-color: var(--bs-primary) !important; +} + +.card-img-top { + height: 100%; + object-fit: cover; +} + +.source-name { + position: relative; + z-index: 1; +} + +@media screen and (min-width: 768px) { + main { + margin-top: 100px; + } + + #search-form { + width: 70%; + } + + #topics-bar { + border-radius: 0; + font-size: 0.9em; + } + + #topics-bar li { + border-bottom-left-radius: 0 !important; + border-bottom-right-radius: 0 !important; + } + + .card-img-top { + border-top-right-radius: 0; + border-bottom-left-radius: 0.25rem; + } +} diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000..116ea43 Binary files /dev/null and b/images/logo.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..0691e45 --- /dev/null +++ b/index.html @@ -0,0 +1,284 @@ + + + + + + + + Top News + + + + + + + + + + +
+ + + + +
+ + + + + +
+ + + + + + + + + + diff --git a/js/filter.js b/js/filter.js new file mode 100644 index 0000000..7f012dc --- /dev/null +++ b/js/filter.js @@ -0,0 +1,50 @@ +const filterForm = $('#filter-form'); +const filterInputs = filterForm.find('.filter-input').toArray(); +const searchBtn = filterForm.find('[type=submit]'); +const closeBtn = filterForm.find('.btn-close'); + +filterForm.on({ + submit: event => { + event.preventDefault(); + let keywords = $('#keywords').val(); + let exactMatch = $('#exact-match').val(); + let excludeWords = $('#exclude-words').val(); + + // Đối tượng chứa một số tham số cần xử lý đặc biệt trước khi gửi + // Sô tham số còn lại sẽ lấy trực tiếp từ biểu mẫu + let specialParams = { + // Xử lý và nối các đoạn tìm kiếm lại + q: ( + enhanceSearchPhrase(keywords) + + exactMatch.replace(/"/g, '').replace(/.+/, ' "$&"') + + enhanceSearchPhrase(excludeWords).replace(/\s|^(?=.+)/g, ' NOT ') + ).trim(), + + // Gắn giờ cho ngày bắt đầu và kết thúc + // Do GNews yêu cầu thời gian phải ở định dạng đầy đủ ngày giờ + from: $('#start-date').val().replace(/.+/, '$&T00:00:00Z'), + to: $('#end-date').val().replace(/.+/, '$&T23:59:59Z') + }; + + // Lấy dữ liệu được lọc + let endpoint = + specialParams.q && !filterForm.find('[name=topic]').val() ? 'search' : 'top-headlines'; + let params = $.param(specialParams) + '&' + filterForm.serialize(); + getArticles(endpoint, params); + + // Xử lý sau khi lọc + closeBtn.click(); + searchInput.val(''); + openFilterBtn.addClass('filtered'); + if ($(window).width() <= 768) searchBarToggle.click(); + }, + + input: () => { + // Chỉ cần một trong số các tiêu chí lọc được cung cấp thì sẽ cho phép người dùng được lọc + searchBtn.toggleClass('disabled', !filterInputs.some(input => input.value.trim())); + }, + + reset: () => { + searchBtn.addClass('disabled'); + } +}); diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..da07105 --- /dev/null +++ b/js/main.js @@ -0,0 +1,133 @@ +const API_TOKEN = 'a6ec55d716fd3d21c7280c89985206da'; + +const main = $('main'); +const articleTemplate = $($('#article-template').prop('content')); +const openFilterBtn = $('[data-bs-target="#filter-modal"]'); +const searchBarToggle = $('[data-bs-target="#searchbar-collapse"]'); +const searchInput = $('#search-input'); +const topicsBarToggle = $('[data-bs-target="#topics-collapse"]'); + +// Xử lý nội dung người dùng tìm kiếm để có thể gửi đi được +function enhanceSearchPhrase(phrase) { + return phrase + .replace(/"/g, '') + .replace(/\s+/g, ' ') + .replace(/(\s|^)\S*[^a-zA-Z0-9 ]+\S*(?=\s|$)/g, match => { + return ' "' + match.trim() + '"'; + }) + .trim(); +} + +// Lấy và hiển thị dữ liệu từ trang GNews với endpoint và các tham số được truyền vào +function getArticles(endpoint, params = '') { + $(window).scrollTop(0); + openFilterBtn.removeClass('filtered'); + $('label.active').removeClass('active').children('input').prop('checked', false); + + // Hiển thị hiệu ứng loading + main.html(''); + for (let i = 0; i < 10; i++) { + main.append(articleTemplate.clone()); + } + + // Lấy dữ liệu từ API + $.getJSON(`https://gnews.io/api/v4/${endpoint}?token=${API_TOKEN}&${params}`, data => { + main.html(''); + + if (!data.totalArticles) { + // Hiển thị thông báo khi không tìm thấy bài báo nào + main.html('

No results found.

'); + } else { + // Hiển thị từng bài báo + data.articles.forEach(article => { + let articleEl = articleTemplate.clone(); + + // Thay các dữ liệu lấy được vào vị trí các placeholder của bài báo + articleEl.find('.card-img-top').replaceWith( + $('') + .addClass('card-img-top placeholder') + .attr('src', article.image) + .on('load error', function () { + $(this) + .removeClass('placeholder') + .closest('article') + .removeClass('placeholder-glow'); + }) + ); + + articleEl.find('.card-title').html( + $('') + .addClass('stretched-link') + .attr({ + href: article.url, + target: '_blank' + }) + .text(article.title) + ); + + articleEl.find('.description').text(article.description); + + let source = $('') + .addClass('source-name') + .attr({ + href: article.source.url, + target: '_blank', + title: 'Go to ' + article.source.name + }) + .text(article.source.name); + + let time = new Date(article.publishedAt).toLocaleString('vi'); + + articleEl + .find('.footer-text > small') + .addClass('text-muted') + .html(`${source.get(0).outerHTML} · `); + + // Gắn bài báo vào thân trang web + main.append(articleEl); + }); + } + + // Hiển thị thông báo lỗi khi lấy dữ liệu thất bại + }).fail(err => { + let errorMessage = 'Error: ' + err.status + ' - ' + err.responseJSON.errors; + + // Nếu lỗi là do hết lượt lấy dữ liệu thì thông báo cho người dùng một cách dễ hiểu hơn + if (err.status == 403) { + errorMessage = 'Oops! You have read too much news today. Please come back tomorrow.'; + } + + main.html(`

${errorMessage}

`); + }); +} + +// Đổi chủ đề khi người dùng nhấn vào chủ đề đó +$('#topics-bar').on('change', 'input', function () { + // Nếu thiết bị có màn hình nhỏ thì ẩn thanh chủ đề sau khi người dùng đổi chủ đề + if ($(window).width() <= 768) topicsBarToggle.click(); + + getArticles('top-headlines', 'topic=' + $(this).val()); + + $('#topics-bar label.active').removeClass('active'); + $(this).parent().addClass('active'); +}); + +// Tìm kiếm với từ khóa mà người dùng nhập vào +$('#search-form').on('submit', event => { + event.preventDefault(); + // Nếu thiết bị có màn hình nhỏ thì ẩn thanh tìm kiếm sau khi người dùng tìm kiếm + if ($(window).width() <= 768) searchBarToggle.click(); + + getArticles('search', $.param({ q: enhanceSearchPhrase(searchInput.val()) })); +}); + +// Khi thanh tìm kiếm mở ra trên thiết bị có màn hình nhỏ thì hiển thị bàn phím ảo +searchBarToggle.on('click', () => { + if (!$('#searchbar-collapse').hasClass('show') && !searchBarToggle.hasClass('collapsed')) { + searchInput.focus(); + } +}); + +// Các tin tức hàng đầu sẽ được hiển thị khi người dùng vào trang web +getArticles('top-headlines'); +$('input[name=topic][value=breaking-news]').parent().addClass('active');