diff --git a/css/styles.css b/css/styles.css
new file mode 100644
index 0000000..128d114
--- /dev/null
+++ b/css/styles.css
@@ -0,0 +1,487 @@
+
+/* styles.css */
+
+html {
+ font-size: 14px; /* Reduce from default 16px */
+}
+
+body {
+ font-family: Arial, sans-serif;
+ margin: 0;
+ padding: 0;
+
+}
+
+.tabs-container {
+ position: sticky;
+ top: 0;
+ z-index: 1000; /* Ensure it stays above other content */
+ background-color: #f1f1f1; /* Match the background color */
+}
+
+.tabs {
+ display: flex;
+ flex-direction: column; /* Stack tabs vertically */
+ width: 150px; /* Adjust width of the tabs */
+ border-right: 1px solid #ddd; /* Add a vertical separator */
+ background-color: #f5f5f5;
+ height: 100vh; /* Make tabs stretch vertically to fill the viewport */
+ overflow-y: auto; /* Enable scrolling if the tabs exceed the height */
+ position: fixed; /* Keep tabs fixed on the left side */
+ left: 0; /* Align tabs to the left edge */
+ top: 0;
+ z-index: 1000; /* Ensure tabs are above other content */
+}
+
+/* Individual Tab */
+.tab {
+ padding: 15px 20px;
+ font-size: 16px;
+ background: none;
+ border: none;
+ text-align: left;
+ cursor: pointer;
+ transition: background-color 0.3s;
+ border-bottom: 1px solid #ddd; /* Add a separator between tabs */
+ width: 100%;
+}
+
+/* Active Tab */
+.tab.active {
+ background-color: #ddd;
+ color: #000;
+ font-weight: bold;
+}
+
+/* Hover Effect */
+.tab:hover {
+ background-color: #eee;
+}
+
+/* Optional: Remove last tab's margin */
+.tabs .tab:last-child {
+ margin-right: 0;
+}
+
+#content {
+ margin-left: 150px; /* Match the width of the tabs */
+ padding: 20px;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border-top: none;
+ padding-top: 20px;
+}
+
+.modal {
+ display: none; /* Hidden by default */
+ position: fixed;
+ z-index: 1;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
+}
+
+.modal-content {
+ background-color: #fefefe;
+ margin: 5% auto; /* Reduced top margin for more vertical space */
+ padding: 15px; /* Reduced padding */
+ border: 1px solid #888;
+ width: 35%; /* Reduced width from 50% to 35% */
+ max-width: 500px; /* Added max-width for larger screens */
+ position: relative;
+ border-radius: 8px; /* Optional: Rounded corners for a modern look */
+}
+
+#close-modal {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ cursor: pointer;
+}
+
+
+/* Add padding and spacing for form fields inside the Edit Modal */
+#edit-modal .form-container {
+ display: flex;
+ flex-direction: column;
+ gap: 10px; /* Reduced gap between form elements */
+ padding: 0; /* Removed extra padding */
+ background-color: transparent; /* Remove background to match modal */
+ box-shadow: none; /* Remove shadow inside modal */
+}
+
+/* Ensure form fields are styled properly */
+#edit-modal .form-container .form-group {
+ margin-bottom: 10px; /* Add spacing between individual form groups */
+}
+
+#edit-modal .form-container label {
+ margin-bottom: 3px;
+ font-weight: bold;
+}
+
+#edit-modal .form-container input[type="text"],
+#edit-modal .form-container input[type="date"],
+#edit-modal .form-container input[type="url"],
+#edit-modal .form-container textarea,
+#edit-modal .form-container select {
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+#edit-modal .form-container textarea {
+ resize: vertical;
+ min-height: 80px;
+}
+
+/* Buttons at the bottom of the form */
+#edit-modal .form-container .form-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px; /* Space between buttons */
+}
+
+#edit-modal .form-container .form-buttons button {
+ padding: 10px 15px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+#edit-modal .form-container .form-buttons button[type="submit"] {
+ padding: 8px 12px; /* Reduced padding */
+ font-size: 14px; /* Reduced font size */
+ background-color: #007bff;
+ color: #fff;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+#edit-modal .form-container .form-buttons button[type="button"] {
+ background-color: #dc3545;
+ color: #fff;
+}
+
+#edit-modal .form-container .form-buttons button:hover {
+ opacity: 0.9;
+}
+
+/* Style for the Add Record form */
+#add-form {
+ margin-bottom: 40px;
+ padding: 10px;
+ background-color: #f9f9f9;
+}
+
+tr.completed {
+ background-color: lightgreen !important;
+}
+
+.suggested {
+ background-color: yellow;
+}
+
+.watching {
+ background-color: yellow;
+}
+
+/* Form Container */
+.form-container-horizontal {
+ max-width: 100%;
+ margin: 20px auto;
+ padding: 20px;
+ background-color: #f1f1f1; /* Light background */
+ border-radius: 8px;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+}
+
+.form-container-horizontal h3 {
+ text-align: center;
+ margin-bottom: 20px;
+}
+
+/* Form Row */
+.form-row {
+ display: flex;
+ gap: 10px; /* space between the columns */
+}
+
+/* Optional: make each form-group share space evenly */
+.form-row .form-group {
+ flex: 1;
+}
+
+/* Remove margin from the last form group in a row */
+.form-row .form-group:last-child {
+ margin-right: 0;
+}
+
+/* Labels */
+.form-group label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: bold;
+}
+
+/* Input fields */
+.form-control {
+ width: 100%;
+ padding: 8px;
+ box-sizing: border-box;
+}
+
+/* Checkbox and Buttons Row */
+#edit-modal .form-container .form-check {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.form-check-input {
+ margin-right: 5px;
+}
+
+.btn {
+ padding: 10px 20px;
+ font-size: 16px;
+ cursor: pointer;
+ border: none;
+ border-radius: 4px;
+ text-decoration: none;
+}
+
+.btn-primary {
+ background-color: #007bff;
+ color: #fff;
+}
+
+.btn-primary:hover {
+ background-color: #0069d9;
+}
+
+.set-complete-button {
+ background-color: #00eb0c;
+}
+
+.set-complete-button:hover {
+ background-color: #038c0a;
+}
+
+.set-currently-watching-button {
+ background-color: #f6fa02;
+}
+
+.set-currently-watching-button:hover {
+ background-color: #bcbf02;
+}
+
+.delete-button {
+ background-color: #f44336;
+}
+
+.delete-button:hover {
+ background-color: #d32f2f;
+}
+
+.close-button {
+ position: absolute;
+ top: 10px;
+ right: 15px;
+ font-size: 24px; /* Reduced font size */
+ color: #aaa;
+ cursor: pointer;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .form-row {
+ flex-direction: column;
+ }
+ .form-row .form-group {
+ margin-right: 0;
+ margin-bottom: 15px;
+ }
+ .form-check {
+ margin-bottom: 15px;
+ }
+
+ .tabs {
+ flex-direction: column;
+ }
+ .tab {
+ margin-bottom: 5px;
+ }
+}
+
+
+/* Close Button */
+.close-button {
+ position: absolute;
+ top: 10px;
+ right: 20px;
+ font-size: 30px;
+ font-weight: bold;
+ color: #aaa;
+ cursor: pointer;
+}
+
+.close-button:hover {
+ color: #000;
+}
+
+.record-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-family: Arial, sans-serif;
+}
+
+.record-table {
+
+ width: 100%;
+
+ border-collapse: collapse;
+
+ font-family: Arial, sans-serif;
+
+ border: 1px solid #000; /* Added black border to the table */
+
+}
+
+
+/* Column widths using CSS selectors */
+
+.record-table colgroup col:nth-child(1) { width: 3%; } /* # */
+
+.record-table colgroup col:nth-child(2) { width: 35%; } /* Name */
+
+.record-table colgroup col:nth-child(3) { width: 4%; }/* Type */
+
+.record-table colgroup col:nth-child(4) { width: 37%; }/* Comment */
+
+.record-table colgroup col:nth-child(5) { width: 6%; }/* Date completed */
+
+.record-table colgroup col:nth-child(6) { width: 15%; }/* Actions */
+
+/* Column widths using CSS selectors */
+
+.record-table colgroup col:nth-child(1) #rewatch { width: 3%; } /* # */
+
+.record-table colgroup col:nth-child(2) #rewatch { width: 40%; } /* Name */
+
+.record-table colgroup col:nth-child(3) #rewatch { width: 47%; }/* Comment */
+
+.record-table colgroup col:nth-child(4) #rewatch { width: 10%; }/* Actions */
+
+/* Manga table */
+
+.record-table colgroup col:nth-child(1) #manga { width: 3%; } /* # */
+
+.record-table colgroup col:nth-child(2) #manga { width: 35%; } /* Name */
+
+.record-table colgroup col:nth-child(4) #manga { width: 41%; }/* Comment */
+
+.record-table colgroup col:nth-child(5) #manga { width: 6%; }/* Date completed */
+
+.record-table colgroup col:nth-child(6) #manga { width: 15%; }/* Actions */
+
+
+/* Styling table headers and cells */
+
+.record-table th, .record-table td {
+
+ border: 1px solid #000; /* Changed border color to black */
+
+ padding: 8px;
+
+}
+
+
+/* Style for table headers */
+
+.record-table th {
+
+ background-color: #f2f2f2;
+
+ font-weight: bold;
+
+}
+
+.record-table tbody tr:hover {
+
+ background-color: #e9e9e9;
+
+}
+
+
+/* Text alignment in columns */
+
+.record-table th:nth-child(1), .record-table td:nth-child(1),
+
+.record-table th:nth-child(3), .record-table td:nth-child(3),
+
+.record-table th:nth-child(5), .record-table td:nth-child(5),
+
+.record-table th:nth-child(6), .record-table td:nth-child(6) {
+
+ text-align: center;
+
+}
+
+/* Styling action buttons */
+
+.record-table td button {
+ padding: 4px 6px; /* Reduce button padding */
+ margin: 0; /* Remove margins */
+ font-size: 12px; /* Reduce font size */
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 5px;
+
+}
+
+
+th, td {
+ padding: 1px 2px; /* Reduced padding */
+ line-height: 0.8; /* Reduced line height */
+ font-size: 14px; /* Reduced font size */
+ vertical-align: middle; /* Center content vertically */
+ text-align: left; /* Existing alignment */
+ white-space: nowrap; /* Prevent text from wrapping */
+ overflow: hidden; /* Hide overflow */
+ text-overflow: ellipsis; /* Add ellipsis for overflowing text */
+}
+
+th {
+ background-color: #f2f2f2;
+ cursor: pointer;
+ background: linear-gradient(to bottom, lightgrey, white);
+}
+
+.record-table table, .record-table th, .record-table td {
+ border: 1px solid gray;
+}
+
+
+#image-tooltip {
+ position: absolute;
+ border: 1px solid #ccc;
+ background: #fff;
+ padding: 5px;
+ z-index: 1000;
+ max-width: 200px;
+ max-height: 300px;
+ overflow: hidden;
+ display: none;
+}
+
+
diff --git a/icons/android-chrome-192x192.png b/icons/android-chrome-192x192.png
new file mode 100644
index 0000000..ad38971
Binary files /dev/null and b/icons/android-chrome-192x192.png differ
diff --git a/icons/android-chrome-512x512.png b/icons/android-chrome-512x512.png
new file mode 100644
index 0000000..fb1a751
Binary files /dev/null and b/icons/android-chrome-512x512.png differ
diff --git a/icons/apple-touch-icon.png b/icons/apple-touch-icon.png
new file mode 100644
index 0000000..ae28f8a
Binary files /dev/null and b/icons/apple-touch-icon.png differ
diff --git a/icons/favicon-16x16.png b/icons/favicon-16x16.png
new file mode 100644
index 0000000..546259e
Binary files /dev/null and b/icons/favicon-16x16.png differ
diff --git a/icons/favicon-32x32.png b/icons/favicon-32x32.png
new file mode 100644
index 0000000..0cb7049
Binary files /dev/null and b/icons/favicon-32x32.png differ
diff --git a/icons/favicon.ico b/icons/favicon.ico
new file mode 100644
index 0000000..0021910
Binary files /dev/null and b/icons/favicon.ico differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..74fdfc6
--- /dev/null
+++ b/index.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+ Backlog of unwatched anime
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/main.js b/js/main.js
new file mode 100644
index 0000000..e69b541
--- /dev/null
+++ b/js/main.js
@@ -0,0 +1,540 @@
+let currentYear;
+
+//Adding new element in the array creates new tab
+const years = ['pre-2009', '2009', '2010', '2011', '2012', '2013', '2014', '2015',
+ '2016','2017','2018','2019','2020','2021','2022','2023', '2024', 're-watch', 'manga'];
+
+function loadYearTabs() {
+ const yearTabs = document.getElementById('year-tabs');
+
+ // Populate the tabs
+ years.forEach(year => {
+ const li = document.createElement('li');
+ li.textContent = year;
+ li.classList.add('tab');
+ li.dataset.year = year; // Set data-year attribute for each tab
+
+ // Add a click event listener to each tab
+ li.addEventListener('click', () => {
+ loadYearContent(year);
+ });
+
+ yearTabs.appendChild(li);
+ // Now check if all entries are completed for this year
+ fetch(`php/check_completed_year.php?year=${encodeURIComponent(year)}`)
+ .then(response => response.json())
+ .then(data => {
+ if (data.all_completed) {
+ // Strike through the tab if all are completed
+ li.style.textDecoration = 'line-through';
+ }
+ })
+ .catch(error => console.error('Error checking completed status:', error));
+ });
+
+ // Load the last active tab or default to 'pre-2009'
+ const activeYear = localStorage.getItem('activeYear') || 'pre-2009';
+ loadYearContent(activeYear);
+}
+
+function loadYearContent(year) {
+ currentYear = year;
+ // Save the current year in localStorage for persistence
+ localStorage.setItem('activeYear', year);
+
+ // Highlight the active tab
+ const tabs = document.querySelectorAll('#year-tabs .tab');
+ tabs.forEach(tab => {
+ // Add or remove the active class based on the tab's data-year
+ if (tab.dataset.year === year) {
+ tab.classList.add('active');
+ } else {
+ tab.classList.remove('active');
+ }
+ });
+
+ // Fetch and display content for the selected year
+ fetch(`php/fetch_data.php?year=${encodeURIComponent(year)}`)
+ .then(response => response.text())
+ .then(html => {
+ document.getElementById('content').innerHTML = html;
+ setupFormSubmission();
+ setupEditButtons();
+ setupDeleteButtons();
+ setupSuggestButton();
+ setupSetCompleteButtons();
+ setupSetCurrentlyWatchingButtons();
+ })
+ .catch(error => console.error('Error loading content:', error));
+}
+
+function setupFormSubmission() {
+ const form = document.getElementById('add-form');
+
+ function handleFormSubmit(event) {
+ event.preventDefault();
+ const formData = new FormData(form);
+ fetch('php/add_record.php', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => {
+ if (!response.ok) {
+ // Если сервер вернул ошибку (например, 403)
+ return response.json().then(errorData => {
+ if (response.status === 403) {
+ alert(errorData.message || 'Access denied: You are not authorized to perform this action.');
+ }
+ throw new Error(errorData.message || 'An error occurred.');
+ });
+ }
+ return response.text();
+ })
+ .then(() => {
+ // Если запись добавлена успешно
+ loadYearContent(currentYear);
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ });
+ }
+
+ form.addEventListener('submit', handleFormSubmit);
+
+ form.addEventListener('keydown', event => {
+ if (event.ctrlKey && event.key === 'Enter') {
+ event.preventDefault();
+ handleFormSubmit(event);
+ }
+ });
+}
+
+
+function setupEditButtons() {
+ const editButtons = document.querySelectorAll('.edit-button');
+ editButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const recordId = button.dataset.id;
+ openEditModal(recordId);
+ });
+ });
+}
+
+function setupDeleteButtons() {
+ const deleteButtons = document.querySelectorAll('.delete-button');
+ deleteButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const recordId = button.dataset.id;
+ if (confirm('Are you sure you want to delete this record?')) {
+ fetch(`php/delete_record.php?id=${recordId}`, {
+ method: 'DELETE' // Используем метод DELETE для удаления
+ })
+ .then(response => {
+ if (!response.ok) {
+ // Если сервер вернул ошибку (например, 403)
+ return response.json().then(errorData => {
+ if (response.status === 403) {
+ alert(errorData.message || 'Access denied: You are not authorized to delete this record.');
+ }
+ throw new Error(errorData.message || 'An error occurred while deleting the record.');
+ });
+ }
+ return response.text();
+ })
+ .then(() => {
+ // Если удаление прошло успешно
+ loadYearContent(currentYear);
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ });
+ }
+ });
+ });
+}
+
+
+function openEditModal(id) {
+ // Fetch record data
+ fetch(`php/fetch_record.php?id=${id}`)
+ .then(response => response.json())
+ .then(data => {
+ const modal = document.getElementById('edit-modal');
+ const form = document.getElementById('edit-form');
+ form.innerHTML = generateEditForm(data);
+ modal.style.display = 'block';
+ setupEditFormSubmission();
+ });
+}
+
+function setupModal() {
+ const modal = document.getElementById('edit-modal');
+ const closeModal = document.getElementById('close-modal');
+ closeModal.addEventListener('click', () => modal.style.display = 'none');
+
+ window.addEventListener('keydown', event => {
+ if (event.key === 'Escape') {
+ modal.style.display = 'none';
+ }
+ });
+
+ // Close modal when clicking outside the modal content
+ modal.addEventListener('click', event => {
+ if (event.target === modal) {
+ modal.style.display = 'none';
+ }
+ });
+}
+
+function mapLabelToYear(label) {
+ if (label === 'pre-2009') return -1;
+ if (label === 're-watch') return -2;
+ return parseInt(label, 10);
+}
+
+function setupEditFormSubmission() {
+ const form = document.getElementById('edit-form');
+
+ function handleEditFormSubmit(event) {
+ event.preventDefault();
+
+ const yearLabelSelect = form.querySelector('#year_label');
+ const numericYearInput = form.querySelector('#year_numeric');
+ const selectedLabel = yearLabelSelect.value;
+
+ // Use the mapLabelToYear function
+ numericYearInput.value = mapLabelToYear(selectedLabel);
+
+ const formData = new FormData(form);
+ fetch('php/edit_record.php', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => {
+ if (!response.ok) {
+ return response.json().then(errorData => {
+ if (response.status === 403) {
+ alert(errorData.message || 'Access denied: You are not authorized to edit this record.');
+ }
+ throw new Error(errorData.message || 'An error occurred while editing the record.');
+ });
+ }
+ return response.text();
+ })
+ .then(() => {
+ const modal = document.getElementById('edit-modal');
+ modal.style.display = 'none';
+ loadYearContent(currentYear);
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ });
+ }
+
+
+ form.addEventListener('submit', handleEditFormSubmit);
+
+ form.addEventListener('keydown', event => {
+ if (event.ctrlKey && event.key === 'Enter') {
+ event.preventDefault();
+ handleEditFormSubmit(event);
+ }
+ });
+}
+
+function generateEditForm(data) {
+
+ function mapYearToLabel(yearValue) {
+ if (yearValue == -1) return 'pre-2009';
+ if (yearValue == -2) return 're-watch';
+ if (yearValue == -3) return 'manga';
+ return yearValue.toString();
+ }
+
+ const selectedYearLabel = mapYearToLabel(data.year);
+
+ return `
+ Edit Record
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+}
+
+
+function setupSuggestButton() {
+ const suggestButton = document.getElementById('suggest-button');
+ if (suggestButton) {
+ suggestButton.addEventListener('click', () => {
+ makeSuggestion();
+ });
+ }
+}
+
+
+function makeSuggestion() {
+ // Clear any previous suggestions
+ const previousSuggestion = document.getElementById('suggestion-display');
+ if (previousSuggestion) {
+ previousSuggestion.remove();
+ }
+
+ // Get all table rows that are not completed
+ const rows = Array.from(document.querySelectorAll('table tbody tr')).filter(row => {
+ return !row.classList.contains('completed');
+ });
+
+ // Exclude header rows and ensure there are at least two entries
+ if (rows.length < 1) {
+ alert('Not enough uncompleted entries to make a suggestion.');
+ return;
+ }
+
+ // Randomly select two different rows
+ const indices = [];
+ while (indices.length < 1) {
+ const index = Math.floor(Math.random() * rows.length);
+ if (!indices.includes(index)) {
+ indices.push(index);
+ }
+ }
+
+ const selectedRows = [rows[indices[0]]];
+
+ // Get the names of the selected records
+ const names = selectedRows.map(row => row.getAttribute('data-name'));
+
+ // Display the names at the top of the content
+ const content = document.getElementById('content');
+ const suggestionDisplayDiv = document.createElement('div');
+ suggestionDisplayDiv.id = 'suggestion-display';
+ suggestionDisplayDiv.innerHTML = `${names.join('
')}
`;
+ content.insertBefore(suggestionDisplayDiv, content.querySelector('button')); // Insert before the add form
+}
+
+function setupStickyTabs() {
+ const tabsContainer = document.querySelector('.tabs-container');
+
+ const observer = new IntersectionObserver(
+ ([e]) => e.target.classList.toggle('is-sticky', e.intersectionRatio < 1),
+ { threshold: [1] }
+ );
+
+ observer.observe(tabsContainer);
+}
+
+function setupSetCompleteButtons() {
+ const setCompleteButtons = document.querySelectorAll('.set-complete-button');
+ setCompleteButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const recordId = button.dataset.id;
+ const formData = new FormData();
+ formData.append('id', recordId);
+ fetch('php/set_complete.php', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => {
+ if (!response.ok) {
+ return response.json().then(errorData => {
+ if (response.status === 403) {
+ alert(errorData.message || 'Access denied: You are not authorized to perform this action.');
+ }
+ throw new Error(errorData.message || 'An error occurred while setting the record as complete.');
+ });
+ }
+ return response.json();
+ })
+ .then(() => {
+ // Reload the table to reflect the changes
+ loadYearContent(currentYear);
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ });
+ });
+ });
+}
+
+function setupSetCurrentlyWatchingButtons() {
+ const setCurrentlyWatchingButtons = document.querySelectorAll('.set-currently-watching-button');
+ setCurrentlyWatchingButtons.forEach(button => {
+ button.addEventListener('click', () => {
+ const recordId = button.dataset.id;
+ const formData = new FormData();
+ formData.append('id', recordId);
+ fetch('php/set_currently_watching.php', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => {
+ if (!response.ok) {
+ return response.json().then(errorData => {
+ if (response.status === 403) {
+ alert(errorData.message || 'Access denied: You are not authorized to perform this action.');
+ }
+ throw new Error(errorData.message || 'An error occurred while setting the record as currently watching.');
+ });
+ }
+ return response.json();
+ })
+ .then(() => {
+ // Reload the table to reflect the changes
+ loadYearContent(currentYear);
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ });
+ });
+ });
+}
+
+
+document.addEventListener('DOMContentLoaded', () => {
+ console.log('DOM fully loaded and parsed');
+ loadYearTabs();
+ setupModal();
+ var imageCache = {};
+ var currentHoverTarget = null;
+
+ function showImageTooltip(event, imageUrl) {
+ var tooltip = document.getElementById('image-tooltip');
+ if (!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.id = 'image-tooltip';
+ tooltip.style.position = 'absolute';
+ tooltip.style.border = '1px solid #ccc';
+ tooltip.style.background = '#fff';
+ tooltip.style.padding = '5px';
+ tooltip.style.zIndex = 1000;
+ tooltip.style.maxWidth = '200px';
+ tooltip.style.maxHeight = '300px';
+ tooltip.style.overflow = 'hidden';
+ tooltip.style.display = 'none';
+ document.body.appendChild(tooltip);
+ }
+
+ tooltip.innerHTML = '';
+ tooltip.style.left = (event.pageX + 15) + 'px';
+ tooltip.style.top = (event.pageY + 15) + 'px';
+ tooltip.style.display = 'block';
+ }
+
+ function hideImageTooltip() {
+ var tooltip = document.getElementById('image-tooltip');
+ if (tooltip) {
+ tooltip.style.display = 'none';
+ }
+ }
+
+ // Добавляем обработчики событий на родительский элемент
+ var contentDiv = document.getElementById('content');
+
+ contentDiv.addEventListener('mouseover', function (event) {
+ var target = event.target;
+
+ // Проверяем, является ли целевой элемент ссылкой с атрибутом data-url
+ if (target.tagName.toLowerCase() === 'a' && target.hasAttribute('data-url')) {
+ var url = target.getAttribute('data-url');
+ currentHoverTarget = target; // Set the current hover target
+
+ if (imageCache[url]) {
+ showImageTooltip(event, imageCache[url]);
+ } else {
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', './php/get_image.php?url=' + encodeURIComponent(url), true);
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState === 4 && xhr.status === 200) {
+ var response = JSON.parse(xhr.responseText);
+ if (response.image_url) {
+ imageCache[url] = response.image_url;
+ if (currentHoverTarget === target) {
+ showImageTooltip(event, response.image_url);
+ }
+ } else if (response.error) {
+ console.error(response.error);
+ }
+ }
+ };
+ xhr.send();
+ }
+ }
+ });
+
+ contentDiv.addEventListener('mouseout', function (event) {
+ var target = event.target;
+
+ if (target.tagName.toLowerCase() === 'a' && target.hasAttribute('data-url')) {
+ hideImageTooltip();
+ if (currentHoverTarget === target) {
+ currentHoverTarget = null; // Clear the current hover target
+ }
+ }
+ });
+
+ contentDiv.addEventListener('mousemove', function (event) {
+ var tooltip = document.getElementById('image-tooltip');
+ if (tooltip && tooltip.style.display === 'block') {
+ tooltip.style.left = (event.pageX + 15) + 'px';
+ tooltip.style.top = (event.pageY + 15) + 'px';
+ }
+ });
+});
diff --git a/php/add_record.php b/php/add_record.php
new file mode 100644
index 0000000..857b6db
--- /dev/null
+++ b/php/add_record.php
@@ -0,0 +1,68 @@
+ 'error',
+ 'message' => 'Access denied: Your IP is not authorized to add records.'
+ ]);
+ exit;
+}
+
+$name = $_POST['name'];
+$year = $_POST['year'];
+$season = $_POST['season'];
+$type = $_POST['type'];
+$comment = $_POST['comment'];
+$is_completed = isset($_POST['is_completed']) ? 1 : 0;
+$currently_watching = isset($_POST['currently_watching']) ? 1 : 0;
+$date_completed = $_POST['date_completed'];
+$url = $_POST['url'];
+
+// Handle empty date_completed
+if (empty($date_completed)) {
+ $date_completed = NULL;
+}
+
+$stmt = $conn->prepare("INSERT INTO anime_list (name, year, season, type, comment, is_completed, date_completed, url, currently_watching) VALUES (:name, :year, :season, :type, :comment, :is_completed, :date_completed, :url, :currently_watching)");
+$stmt->bindParam(':name', $name);
+$stmt->bindParam(':year', $year, PDO::PARAM_INT);
+$stmt->bindParam(':season', $season);
+$stmt->bindParam(':type', $type);
+$stmt->bindParam(':comment', $comment);
+$stmt->bindParam(':is_completed', $is_completed, PDO::PARAM_INT);
+$stmt->bindParam(':currently_watching', $currently_watching, PDO::PARAM_INT);
+$stmt->bindParam(':url', $url);
+
+// Use bindValue with PDO::PARAM_NULL if date_completed is NULL
+if ($date_completed === NULL) {
+ $stmt->bindValue(':date_completed', NULL, PDO::PARAM_NULL);
+} else {
+ $stmt->bindParam(':date_completed', $date_completed);
+}
+
+$stmt->execute();
+
+// Log the action
+// $action_time = new DateTime('now', new DateTimeZone('GMT+5'));
+// $action_time_formatted = $action_time->format('Y-m-d H:i:s');
+// $ip_address = $_SERVER['REMOTE_ADDR'];
+// $anime_name = $name;
+// $anime_year = $year;
+// $action_type = 'adding';
+
+// $log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, year) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
+// $log_stmt->bindParam(':action_time', $action_time_formatted);
+// $log_stmt->bindParam(':ip_address', $ip_address);
+// $log_stmt->bindParam(':anime_name', $anime_name);
+// $log_stmt->bindParam(':action_type', $action_type);
+// $log_stmt->bindParam(':anime_year', $anime_year);
+// $log_stmt->execute();
+
+?>
+
diff --git a/php/check_allowed_ip.php b/php/check_allowed_ip.php
new file mode 100644
index 0000000..1767ddc
--- /dev/null
+++ b/php/check_allowed_ip.php
@@ -0,0 +1,26 @@
+ -1,
+ 're-watch' => -2,
+ 'manga' => -3
+];
+
+$yearValue = $yearMap[$year] ?? (int)$year;
+
+// Count total records and completed records for that year
+$stmt = $conn->prepare("SELECT COUNT(*) AS total_records, SUM(is_completed) AS total_completed FROM anime_list WHERE year = :year");
+$stmt->bindParam(':year', $yearValue, PDO::PARAM_INT);
+$stmt->execute();
+$row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+$all_completed = false;
+if ($row && $row['total_records'] > 0 && $row['total_completed'] == $row['total_records']) {
+ $all_completed = true;
+}
+
+header('Content-Type: application/json');
+echo json_encode(['all_completed' => $all_completed]);
diff --git a/php/db_connect.example.php b/php/db_connect.example.php
new file mode 100644
index 0000000..e8ec69a
--- /dev/null
+++ b/php/db_connect.example.php
@@ -0,0 +1,19 @@
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+} catch(PDOException $e) {
+ echo "Connection failed: " . $e->getMessage();
+ exit();
+}
+?>
diff --git a/php/delete_record.php b/php/delete_record.php
new file mode 100644
index 0000000..f221bda
--- /dev/null
+++ b/php/delete_record.php
@@ -0,0 +1,52 @@
+ 'error',
+ 'message' => 'Access denied: Your IP is not authorized to delete records.'
+ ]);
+ exit;
+}
+
+$id = $_GET['id'];
+
+// Fetch the anime name and year before deleting
+$stmt = $conn->prepare("SELECT name, `year` FROM anime_list WHERE id = :id");
+$stmt->bindParam(':id', $id, PDO::PARAM_INT);
+$stmt->execute();
+$anime = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if (!$anime) {
+ echo 'No anime found with id ' . htmlspecialchars($id);
+ exit;
+}
+
+$anime_name = $anime['name'];
+$anime_year = $anime['year'];
+
+// Delete the record
+$stmt = $conn->prepare("DELETE FROM anime_list WHERE id = :id");
+$stmt->bindParam(':id', $id, PDO::PARAM_INT);
+$stmt->execute();
+
+// Log the action
+// $action_time = new DateTime('now', new DateTimeZone('GMT+5'));
+// $action_time_formatted = $action_time->format('Y-m-d H:i:s');
+// $ip_address = $_SERVER['REMOTE_ADDR'];
+// $action_type = 'deleting';
+
+// $log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, `year`) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
+// $log_stmt->bindParam(':action_time', $action_time_formatted);
+// $log_stmt->bindParam(':ip_address', $ip_address);
+// $log_stmt->bindParam(':anime_name', $anime_name);
+// $log_stmt->bindParam(':action_type', $action_type);
+// $log_stmt->bindParam(':anime_year', $anime_year, PDO::PARAM_INT);
+// $log_stmt->execute();
+?>
+
diff --git a/php/edit_record.php b/php/edit_record.php
new file mode 100644
index 0000000..7cf865a
--- /dev/null
+++ b/php/edit_record.php
@@ -0,0 +1,70 @@
+ 'error',
+ 'message' => 'Access denied: Your IP is not authorized to modify records.'
+ ]);
+ exit;
+}
+
+$id = $_POST['id'];
+$name = $_POST['name'];
+$year = $_POST['year'];
+$season = $_POST['season'];
+$type = $_POST['type'];
+$comment = $_POST['comment'];
+$is_completed = isset($_POST['is_completed']) ? 1 : 0;
+$currently_watching = isset($_POST['currently_watching']) ? 1 : 0;
+$date_completed = $_POST['date_completed'];
+$url = $_POST['url'];
+
+// Handle empty date_completed
+if (empty($date_completed)) {
+ $date_completed = NULL;
+}
+
+$stmt = $conn->prepare("UPDATE anime_list SET name = :name, year = :year, season = :season, type = :type, comment = :comment, is_completed = :is_completed, date_completed = :date_completed, url = :url, currently_watching = :currently_watching WHERE id = :id");
+$stmt->bindParam(':id', $id, PDO::PARAM_INT);
+$stmt->bindParam(':name', $name);
+$stmt->bindParam(':year', $year, PDO::PARAM_INT);
+$stmt->bindParam(':season', $season);
+$stmt->bindParam(':type', $type);
+$stmt->bindParam(':comment', $comment);
+$stmt->bindParam(':is_completed', $is_completed, PDO::PARAM_INT);
+$stmt->bindParam(':currently_watching', $currently_watching, PDO::PARAM_INT);
+$stmt->bindParam(':url', $url);
+
+// Use bindValue with PDO::PARAM_NULL if date_completed is NULL
+if ($date_completed === NULL) {
+ $stmt->bindValue(':date_completed', NULL, PDO::PARAM_NULL);
+} else {
+ $stmt->bindParam(':date_completed', $date_completed);
+}
+
+$stmt->execute();
+
+// // Log the action
+// $action_time = new DateTime('now', new DateTimeZone('GMT+5'));
+// $action_time_formatted = $action_time->format('Y-m-d H:i:s');
+// $ip_address = $_SERVER['REMOTE_ADDR'];
+// $anime_name = $name;
+// $anime_year = $year;
+// $action_type = 'editing';
+
+// $log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, year) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
+// $log_stmt->bindParam(':action_time', $action_time_formatted);
+// $log_stmt->bindParam(':ip_address', $ip_address);
+// $log_stmt->bindParam(':anime_name', $anime_name);
+// $log_stmt->bindParam(':anime_year', $anime_year);
+// $log_stmt->bindParam(':action_type', $action_type);
+// $log_stmt->execute();
+
+?>
+
diff --git a/php/fetch_data.php b/php/fetch_data.php
new file mode 100644
index 0000000..6441944
--- /dev/null
+++ b/php/fetch_data.php
@@ -0,0 +1,90 @@
+ -1,
+ 're-watch' => -2,
+ 'manga' => -3
+];
+
+$yearValue = $yearMap[$year] ?? (int)$year;
+
+// Seasons array to organize records
+$seasons = ['winter', 'spring', 'summer', 'fall', 'omake'];
+
+// Fetch records from DB based on mode
+if ($yearValue == -1 || $yearValue == -2 || $yearValue == -3) {
+ // For pre-2009, re-watch, or manga, we order by name (or differently if you prefer)
+ $stmt = $conn->prepare("SELECT * FROM anime_list WHERE year = :year ORDER BY name ASC");
+} else {
+ // Normal year mode
+ $stmt = $conn->prepare("SELECT * FROM anime_list WHERE year = :year ORDER BY
+ CASE
+ WHEN season = 'winter' THEN 1
+ WHEN season = 'spring' THEN 2
+ WHEN season = 'summer' THEN 3
+ WHEN season = 'fall' THEN 4
+ WHEN season = 'omake' THEN 5
+ ELSE 6
+ END, name ASC");
+}
+$stmt->bindParam(':year', $yearValue, PDO::PARAM_INT);
+$stmt->execute();
+$records = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+if ($yearValue == -1 || $yearValue == -2) {
+ $mode = 'non-season-table';
+} else if ($yearValue == -3) {
+ $mode = 'manga';
+} else {
+ $mode = 'normal';
+}
+
+// If normal mode, group records by season
+$seasonRecords = [];
+if ($mode === 'normal') {
+ // Initialize season arrays
+ foreach ($seasons as $s) {
+ $seasonRecords[$s] = [];
+ }
+
+ // Place records into their respective season
+ foreach ($records as $record) {
+ $recordSeason = strtolower($record['season']);
+ if (in_array($recordSeason, $seasons)) {
+ $seasonRecords[$recordSeason][] = $record;
+ } else {
+ // If season is unknown, treat as 'omake'
+ $seasonRecords['omake'][] = $record;
+ }
+ }
+}
+
+$yearTotal = count($records);
+$yearCompleted = 0;
+foreach ($records as $r) {
+ if ($r['is_completed']) {
+ $yearCompleted++;
+ }
+}
+$yearPercent = $yearTotal > 0 ? round(($yearCompleted / $yearTotal) * 100) : 0;
+
+// Prepare data for the view
+$viewData = [
+ 'year' => $year,
+ 'yearValue' => $yearValue,
+ 'mode' => $mode, // either 'non-season-table' or 'normal'
+ 'records' => $records,
+ 'seasonRecords' => $seasonRecords ?? [],
+ 'seasons' => $seasons,
+ 'showSuggestButton' => ($mode === 'normal'),
+ 'yearTotal' => $yearTotal,
+ 'yearCompleted' => $yearCompleted,
+ 'yearPercent' => $yearPercent
+];
+
+// Include the view
+include '../tmpl/fetch_data_view.php';
diff --git a/php/fetch_record.php b/php/fetch_record.php
new file mode 100644
index 0000000..7173902
--- /dev/null
+++ b/php/fetch_record.php
@@ -0,0 +1,12 @@
+prepare("SELECT * FROM anime_list WHERE id = :id");
+$stmt->bindParam(':id', $id, PDO::PARAM_INT);
+$stmt->execute();
+$record = $stmt->fetch(PDO::FETCH_ASSOC);
+
+echo json_encode($record);
+?>
diff --git a/php/get_image.php b/php/get_image.php
new file mode 100644
index 0000000..d514305
--- /dev/null
+++ b/php/get_image.php
@@ -0,0 +1,136 @@
+ 'No URL provided']);
+ exit;
+}
+
+$url = $_GET['url'];
+
+if (strpos($url, 'myanimelist.net') === false) {
+ echo json_encode(['error' => 'Invalid URL']);
+ exit;
+}
+
+// Используем абсолютные пути для директорий кэша
+$cache_dir = __DIR__ . '/cache/images/';
+$cache_meta_dir = __DIR__ . '/cache/meta/';
+
+// Проверяем и создаём директории, если они не существуют
+if (!is_dir($cache_dir)) {
+ if (!mkdir($cache_dir, 0755, true)) {
+ echo json_encode(['error' => 'Failed to create images cache directory']);
+ exit;
+ }
+}
+
+if (!is_dir($cache_meta_dir)) {
+ if (!mkdir($cache_meta_dir, 0755, true)) {
+ echo json_encode(['error' => 'Failed to create meta cache directory']);
+ exit;
+ }
+}
+
+$hash = md5($url);
+$cache_meta_file = $cache_meta_dir . $hash . '.json';
+$cache_image_file = $cache_dir . $hash . '.jpg'; // Предполагаем, что изображения в формате JPG
+
+// Проверяем, существует ли кэшированное изображение
+if (file_exists($cache_image_file)) {
+ // Возвращаем путь к локальному изображению
+ $image_url_local = '/plan-to-watch/php/cache/images/' . $hash . '.jpg';
+ echo json_encode(['image_url' => $image_url_local]);
+ exit;
+}
+
+// Если кэшированного изображения нет, получаем URL изображения
+// Используем cURL для получения страницы
+$ch = curl_init();
+curl_setopt($ch, CURLOPT_URL, $url);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0');
+$html = curl_exec($ch);
+curl_close($ch);
+
+if ($html === false) {
+ echo json_encode(['error' => 'Could not fetch the page']);
+ exit;
+}
+
+$doc = new DOMDocument();
+libxml_use_internal_errors(true);
+$doc->loadHTML($html);
+libxml_clear_errors();
+
+$xpath = new DOMXPath($doc);
+
+// Ищем мета-тег og:image
+$image_nodes = $xpath->query("//meta[@property='og:image']");
+if ($image_nodes->length > 0) {
+ $image_url = $image_nodes->item(0)->getAttribute('content');
+
+ // Проверяем, получили ли мы URL изображения
+ if (!empty($image_url)) {
+ // Скачиваем изображение
+ $image_data = file_get_contents($image_url);
+ if ($image_data === false) {
+ echo json_encode(['error' => 'Could not download the image']);
+ exit;
+ }
+
+ // Определяем тип изображения
+ $image_info = getimagesizefromstring($image_data);
+ if ($image_info === false) {
+ echo json_encode(['error' => 'Invalid image data']);
+ exit;
+ }
+
+ // Определяем расширение файла
+ $mime = $image_info['mime'];
+ switch ($mime) {
+ case 'image/jpeg':
+ $extension = '.jpg';
+ break;
+ case 'image/png':
+ $extension = '.png';
+ break;
+ case 'image/gif':
+ $extension = '.gif';
+ break;
+ default:
+ echo json_encode(['error' => 'Unsupported image type']);
+ exit;
+ }
+
+ // Обновляем путь к кэшированному изображению с правильным расширением
+ $cache_image_file = $cache_dir . $hash . $extension;
+ $image_url_local = '/plan-to-watch/php/cache/images/' . $hash . $extension;
+
+ // Сохраняем изображение на сервере
+ if (file_put_contents($cache_image_file, $image_data) === false) {
+ echo json_encode(['error' => 'Failed to save the image']);
+ exit;
+ }
+
+ // Сохраняем метаданные (опционально)
+ $meta_data = json_encode(['image_url' => $image_url_local]);
+ if (file_put_contents($cache_meta_file, $meta_data) === false) {
+ echo json_encode(['error' => 'Failed to save meta data']);
+ exit;
+ }
+
+ echo json_encode(['image_url' => $image_url_local]);
+ exit;
+ }
+}
+
+echo json_encode(['error' => 'Image not found']);
+exit;
+?>
+
diff --git a/php/get_logs.php b/php/get_logs.php
new file mode 100644
index 0000000..d846792
--- /dev/null
+++ b/php/get_logs.php
@@ -0,0 +1,55 @@
+prepare("TRUNCATE TABLE action_logs");
+ $stmt->execute();
+
+ // Regenerate CSRF token
+ $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+
+ // Redirect back to panel.php
+ header("Location: panel.php");
+ exit();
+}
+
+// Handle pagination parameters passed from logs.php
+$per_page = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 10;
+$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
+if ($page < 1) $page = 1;
+
+// Count total logs
+$count_stmt = $conn->query("SELECT COUNT(*) AS total FROM action_logs");
+$total_logs = (int)$count_stmt->fetchColumn();
+
+// Calculate offset
+$offset = ($page - 1) * $per_page;
+
+// Fetch logs with LIMIT and OFFSET
+$stmt = $conn->prepare("SELECT * FROM action_logs ORDER BY action_time DESC LIMIT :limit OFFSET :offset");
+$stmt->bindValue(':limit', $per_page, PDO::PARAM_INT);
+$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
+$stmt->execute();
+$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
diff --git a/php/get_options.php b/php/get_options.php
new file mode 100644
index 0000000..8b28695
--- /dev/null
+++ b/php/get_options.php
@@ -0,0 +1,28 @@
+prepare("INSERT INTO options (option_name, option_value) VALUES ('auto_add_completed_date', :val)
+ ON DUPLICATE KEY UPDATE option_value = :val");
+ $stmt->bindParam(':val', $autoAdd, PDO::PARAM_STR);
+ $stmt->execute();
+
+ // Redirect to panel.php to avoid form resubmission
+ header("Location: panel.php");
+ exit;
+}
+
+// Fetch the current option value
+$stmt = $conn->prepare("SELECT option_value FROM options WHERE option_name = 'auto_add_completed_date'");
+$stmt->execute();
+$autoAddCompletedDate = $stmt->fetchColumn();
+
+// If not found in DB, default to '0'
+if ($autoAddCompletedDate === false) {
+ $autoAddCompletedDate = '0';
+}
diff --git a/php/get_stats.php b/php/get_stats.php
new file mode 100644
index 0000000..0fb9b08
--- /dev/null
+++ b/php/get_stats.php
@@ -0,0 +1,66 @@
+getSize();
+ }
+ }
+ return $size;
+}
+
+// Функция для подсчета количества файлов в директории
+function getFileCount($dir) {
+ $count = 0;
+
+ if (is_dir($dir)) {
+ foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)) as $file) {
+ if ($file->isFile()) {
+ $count++;
+ }
+ }
+ }
+ return $count;
+}
+
+// Определяем абсолютные пути к директориям кэша для расчета статистики
+$cache_images_dir = __DIR__ . '/cache/images/';
+$cache_meta_dir = __DIR__ . '/cache/meta/';
+
+// Расчет размера кэша (только изображения)
+$cache_size_bytes = getDirectorySize($cache_images_dir);
+$cache_size_mb = round($cache_size_bytes / (1024 * 1024), 2);
+
+// Подсчет количества изображений в кэше
+$image_count = getFileCount($cache_images_dir);
+
+// Fetch statistics
+$stats = [];
+
+// Total number of entries
+$query = "SELECT COUNT(*) AS total_entries FROM anime_list";
+$stats['total_entries'] = $conn->query($query)->fetch(PDO::FETCH_ASSOC)['total_entries'];
+
+// Total number of completed anime
+$query = "SELECT COUNT(*) AS completed_anime FROM anime_list WHERE is_completed = 1";
+$stats['completed_anime'] = $conn->query($query)->fetch(PDO::FETCH_ASSOC)['completed_anime'];
+
+// Count by type
+$query = "SELECT type, COUNT(*) AS count FROM anime_list GROUP BY type";
+$stats['by_type'] = $conn->query($query)->fetchAll(PDO::FETCH_ASSOC);
+
+// Most recent completion date
+$query = "SELECT MAX(date_completed) AS most_recent_completion FROM anime_list WHERE date_completed IS NOT NULL";
+$stats['most_recent_completion'] = $conn->query($query)->fetch(PDO::FETCH_ASSOC)['most_recent_completion'];
+
+// DB size
+$query = "SELECT SUM(data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = :dbname";
+$stmt = $conn->prepare($query);
+$stmt->execute(['dbname' => $dbname]);
+$row = $stmt->fetch(PDO::FETCH_ASSOC);
+$size_in_bytes = $row['size'];
+$size_in_kilobytes = $size_in_bytes / 1024;
+
+?>
diff --git a/php/options_helper.php b/php/options_helper.php
new file mode 100644
index 0000000..d026684
--- /dev/null
+++ b/php/options_helper.php
@@ -0,0 +1,11 @@
+prepare("SELECT option_value FROM options WHERE option_name = :option_name");
+ $stmt->bindParam(':option_name', $optionName, PDO::PARAM_STR);
+ $stmt->execute();
+ $value = $stmt->fetchColumn();
+ return $value !== false ? $value : $default;
+}
+
diff --git a/php/panel.php b/php/panel.php
new file mode 100644
index 0000000..5694463
--- /dev/null
+++ b/php/panel.php
@@ -0,0 +1,49 @@
+ 0 ? (int)$_GET['page'] : 1;
+
+// Pass these parameters to get_logs.php for the query
+$_GET['per_page'] = $per_page;
+$_GET['page'] = $page;
+
+include 'get_logs.php';
+include 'get_stats.php';
+
+// Prepare data for the view
+$viewData = [
+ 'per_page' => $per_page,
+ 'page' => $page,
+ 'allowed_per_page' => $allowed_per_page,
+ 'logs' => $logs,
+ 'total_logs' => $total_logs,
+ 'csrf_token' => $csrf_token,
+ 'stats' => $stats,
+ 'size_in_kilobytes' => $size_in_kilobytes,
+ 'cache_size_mb' => $cache_size_mb,
+ 'image_count' => $image_count,
+ 'autoAddCompletedDate' => $autoAddCompletedDate
+];
+
+
+include '../tmpl/panel_view.php';
diff --git a/php/set_complete.php b/php/set_complete.php
new file mode 100644
index 0000000..fb25692
--- /dev/null
+++ b/php/set_complete.php
@@ -0,0 +1,67 @@
+ 'error',
+ 'message' => 'Access denied: Your IP is not authorized to modify records.'
+ ]);
+ exit;
+}
+
+$id = $_POST['id'];
+
+// Fetch the anime name and year for logging
+$stmt = $conn->prepare("SELECT name, year FROM anime_list WHERE id = :id");
+$stmt->bindParam(':id', $id, PDO::PARAM_INT);
+$stmt->execute();
+$record = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if (!$record) {
+ http_response_code(404);
+ echo json_encode([
+ 'status' => 'error',
+ 'message' => 'Record not found.'
+ ]);
+ exit;
+}
+
+$anime_name = $record['name'];
+$anime_year = $record['year'];
+
+// Update the record to set is_completed to 1
+
+$auto_add_complelete_date = getOptionValue($conn, 'auto_add_completed_date', '0');
+if ($auto_add_complelete_date == 1) {
+ $current_date = date('Y-m-d');
+ $stmt = $conn->prepare("UPDATE anime_list SET is_completed = 1, date_completed = :current_date WHERE id = :id");
+ $stmt->bindParam(':id', $id, PDO::PARAM_INT);
+ $stmt->bindParam(':current_date', $current_date);
+} else {
+ $stmt = $conn->prepare("UPDATE anime_list SET is_completed = 1 WHERE id = :id");
+ $stmt->bindParam(':id', $id, PDO::PARAM_INT);
+}
+$stmt->execute();
+
+// Log the action
+$action_time = new DateTime('now', new DateTimeZone('GMT+5'));
+$action_time_formatted = $action_time->format('Y-m-d H:i:s');
+$ip_address = $_SERVER['REMOTE_ADDR'];
+$action_type = 'set_complete';
+
+$log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, year) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
+$log_stmt->bindParam(':action_time', $action_time_formatted);
+$log_stmt->bindParam(':ip_address', $ip_address);
+$log_stmt->bindParam(':anime_name', $anime_name);
+$log_stmt->bindParam(':anime_year', $anime_year);
+$log_stmt->bindParam(':action_type', $action_type);
+$log_stmt->execute();
+
+echo json_encode(['status' => 'success']);
+?>
diff --git a/php/set_currently_watching.php b/php/set_currently_watching.php
new file mode 100644
index 0000000..2068b3d
--- /dev/null
+++ b/php/set_currently_watching.php
@@ -0,0 +1,60 @@
+ 'error',
+ 'message' => 'Access denied: Your IP is not authorized to modify records.'
+ ]);
+ exit;
+}
+
+$id = $_POST['id'];
+
+// Fetch the current state of currently_watching
+$stmt = $conn->prepare("SELECT name, year, currently_watching FROM anime_list WHERE id = :id");
+$stmt->bindParam(':id', $id, PDO::PARAM_INT);
+$stmt->execute();
+$record = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if (!$record) {
+ http_response_code(404);
+ echo json_encode([
+ 'status' => 'error',
+ 'message' => 'Record not found.'
+ ]);
+ exit;
+}
+
+$anime_name = $record['name'];
+$anime_year = $record['year'];
+$currentState = (int)$record['currently_watching'];
+
+// Toggle currently_watching
+$newState = $currentState === 1 ? 0 : 1;
+
+$stmt = $conn->prepare("UPDATE anime_list SET currently_watching = :newState WHERE id = :id");
+$stmt->bindParam(':newState', $newState, PDO::PARAM_INT);
+$stmt->bindParam(':id', $id, PDO::PARAM_INT);
+$stmt->execute();
+
+// Log the action
+// $action_time = new DateTime('now', new DateTimeZone('GMT+5'));
+// $action_time_formatted = $action_time->format('Y-m-d H:i:s');
+// $ip_address = $_SERVER['REMOTE_ADDR'];
+// $action_type = $newState === 1 ? 'set_currently_watching' : 'unset_currently_watching';
+
+// $log_stmt = $conn->prepare("INSERT INTO action_logs (action_time, ip_address, anime_name, action_type, year) VALUES (:action_time, :ip_address, :anime_name, :action_type, :anime_year)");
+// $log_stmt->bindParam(':action_time', $action_time_formatted);
+// $log_stmt->bindParam(':ip_address', $ip_address);
+// $log_stmt->bindParam(':anime_name', $anime_name);
+// $log_stmt->bindParam(':anime_year', $anime_year);
+// $log_stmt->bindParam(':action_type', $action_type);
+// $log_stmt->execute();
+
+echo json_encode(['status' => 'success']);
diff --git a/schema.sql b/schema.sql
new file mode 100644
index 0000000..0a4cf84
--- /dev/null
+++ b/schema.sql
@@ -0,0 +1,84 @@
+-- MySQL dump 10.19 Distrib 10.3.39-MariaDB, for debian-linux-gnu (x86_64)
+--
+-- Host: localhost Database: plan_to_watch
+-- ------------------------------------------------------
+-- Server version 10.3.39-MariaDB-0+deb10u2
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `action_logs`
+--
+
+DROP TABLE IF EXISTS `action_logs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `action_logs` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `action_time` datetime DEFAULT NULL,
+ `ip_address` varchar(45) DEFAULT NULL,
+ `anime_name` varchar(255) DEFAULT NULL,
+ `action_type` varchar(50) DEFAULT NULL,
+ `year` int(11) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Table structure for table `anime_list`
+--
+
+DROP TABLE IF EXISTS `anime_list`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `anime_list` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) NOT NULL,
+ `year` int(11) DEFAULT NULL,
+ `season` varchar(50) DEFAULT NULL,
+ `type` varchar(50) DEFAULT NULL,
+ `comment` text DEFAULT NULL,
+ `is_completed` tinyint(1) DEFAULT 0,
+ `date_completed` date DEFAULT NULL,
+ `url` varchar(255) DEFAULT NULL,
+ `currently_watching` tinyint(1) NOT NULL DEFAULT 0,
+ `table_type` varchar(20) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=985 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
+--
+-- Table structure for table `options`
+--
+
+DROP TABLE IF EXISTS `options`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `options` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `option_name` varchar(255) NOT NULL,
+ `option_value` varchar(255) NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `option_name` (`option_name`)
+) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2025-01-06 22:57:21
diff --git a/tmpl/fetch_data_view.php b/tmpl/fetch_data_view.php
new file mode 100644
index 0000000..e664fb4
--- /dev/null
+++ b/tmpl/fetch_data_view.php
@@ -0,0 +1,391 @@
+
+
+
+
+
+
+ Total entries in : , Completed: (%)
+
+
+Suggest';
+}
+
+if ($mode === 'non-season-table') {
+ if (!empty($records)) {
+ renderPre2009Table($records, $yearTotal, $yearCompleted, $yearPercent);
+ } else {
+ echo 'No records found.
';
+ }
+ renderAddRecordForm($yearValue, $seasons);
+
+} else if ($mode === 'manga') {
+ if (!empty($records)) {
+ renderMangaTable($records, $yearTotal, $yearCompleted, $yearPercent);
+ }
+ else {
+ echo 'No records found.
';
+ }
+ renderAddMangaForm($yearValue, $seasons);
+}
+else {
+ renderSeasonTables($seasons, $seasonRecords);
+ renderAddRecordForm($yearValue, $seasons);
+}
+
+// Renders table for pre-2009 records
+function renderPre2009Table($records, $yearTotal, $yearCompleted, $yearPercent) {
+ // Since pre-2009 mode doesn't have seasons, we've already shown year stats above.
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+ # |
+ Name |
+ Type |
+ Comment |
+ Date Completed |
+ Actions |
+
+
+
+
+
+
+ data-id="" data-name="">
+ |
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ # |
+ Name |
+ Comment |
+ Date Completed |
+ Actions |
+
+
+
+
+
+
+ data-id="" data-name="">
+ |
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+ 0 ? round(($seasonCompleted / $seasonTotal) * 100) : 0;
+
+ echo '' . ucfirst($season) . ' ' . get_season_emoji($season) . '
';
+ // Display season stats:
+ echo "Completed: $seasonCompleted ($seasonPercent%)
";
+
+ if (!empty($seasonRecordsThis)) {
+ ?>
+
+
+
+
+
+
+ # |
+ Name |
+ Type |
+ Comment |
+ Date Completed |
+ Actions |
+
+
+
+
+
+
+ data-id="" data-name="">
+ |
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+ No records for this season.';
+ }
+ }
+}
+
+function renderAddMangaForm($yearValue, $seasons) {
+ ?>
+
+
+
+
+
+ 0) ? ceil($total_logs / $per_page) : 1;
+
+?>
+
+
+
+ Control Panel
+
+
+
+
+
+ Action Logs
+
+
+
+
+
+
+
+
+ Date and Time (GMT+5) |
+ IP Address |
+ Anime Name |
+ Action |
+
+ 0): ?>
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+
+
+ No logs available. |
+
+
+
+
+
+
+ 0 && $total_pages > 1): ?>
+
+
+
+ Stats
+ Total Entries:
+ Completed: (%)
+ Database size: Kb.
+ Image cache size: Mb.
+ Number of cached images:
+
+ Count by Type
+
+
+
+
+ Most Recent Completion Date:
+ format('F j, Y');
+ else:
+ echo "No date found.";
+ endif; ?>
+
+
+ Options
+
+
+
+