Updated SQL File to remove dummy data.

This commit is contained in:
Garcia-Gomez
2026-01-26 13:02:02 -08:00
commit 40aa44fd7a
21 changed files with 1977 additions and 0 deletions

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
### PHPUnit ###
# Covers PHPUnit
# Reference: https://phpunit.de/
# Generated files
.phpunit.result.cache
.phpunit.cache
# PHPUnit
/app/phpunit.xml
/phpunit.xml
# Build data
/build/

19
TODO.md Normal file
View File

@@ -0,0 +1,19 @@
1. Change the background color of the project container to a very light color.
2. Change the headers of the project to be bold with slight larger text.
3. Allow the project header of the background to be the same color as the project container, but slightly darker.
4. Change the button text to a button icon for + and -.
5. Align the + and - of the project buttons with the + and - of the tasks and subtasks.
6. Each project color should be different
7. Allow uploading a custom SVG icon via the settings modal.
8. Automatically detect dark mode and swap favicon colors accordingly.
9. Save and load a secondary accent color (e.g., for UI highlights or hover effects).
10. Enable keyboard shortcuts to open modals or trigger common actions.

15
add_project.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
require 'db.php';
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['name']) || empty(trim($data['name']))) {
http_response_code(400);
echo json_encode(['error' => 'Project name is required']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO projects (name) VALUES (?)");
$stmt->execute([trim($data['name'])]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
?>

18
add_subtask.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
require 'db.php';
$data = json_decode(file_get_contents('php://input'), true);
$name = trim($data['name'] ?? '');
$task_id = intval($data['task_id'] ?? 0);
if ($name === '' || $task_id <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Invalid input']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO subtasks (task_id, name) VALUES (?, ?)");
$stmt->execute([$task_id, $name]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
?>

18
add_task.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
require 'db.php';
$data = json_decode(file_get_contents('php://input'), true);
$name = trim($data['name'] ?? '');
$project_id = intval($data['project_id'] ?? 0);
if ($name === '' || $project_id <= 0) {
http_response_code(400);
echo json_encode(['error' => 'Invalid input']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO tasks (project_id, name) VALUES (?, ?)");
$stmt->execute([$project_id, $name]);
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
?>

21
db.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
$host = '192.168.0.201';
$db = 'tinytask';
$user = 'tinytask'; // change this if needed
$pass = 'GD3AVEZRNnE@A3P]'; // change this if needed
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
http_response_code(500);
echo "Database error: " . $e->getMessage();
exit;
}
?>

6
delete_project.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
require 'db.php';
$id = intval($_GET['id'] ?? 0);
if ($id > 0) {
$pdo->prepare("DELETE FROM projects WHERE id = ?")->execute([$id]);
}

6
delete_subtask.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
require 'db.php';
$id = intval($_GET['id'] ?? 0);
if ($id > 0) {
$pdo->prepare("DELETE FROM subtasks WHERE id = ?")->execute([$id]);
}

6
delete_task.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
require 'db.php';
$id = intval($_GET['id'] ?? 0);
if ($id > 0) {
$pdo->prepare("DELETE FROM tasks WHERE id = ?")->execute([$id]);
}

19
get_data.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
require 'db.php';
$projects = $pdo->query("SELECT * FROM projects ORDER BY sort_order ASC")->fetchAll();
foreach ($projects as &$project) {
$stmt = $pdo->prepare("SELECT * FROM tasks WHERE project_id = ? ORDER BY created_at");
$stmt->execute([$project['id']]);
$project['tasks'] = $stmt->fetchAll();
foreach ($project['tasks'] as &$task) {
$stmt = $pdo->prepare("SELECT * FROM subtasks WHERE task_id = ? ORDER BY created_at");
$stmt->execute([$task['id']]);
$task['subtasks'] = $stmt->fetchAll();
}
}
echo json_encode($projects);
?>

5
get_settings.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
require 'db.php';
$stmt = $pdo->query("SELECT title, icon_class, icon_color FROM settings LIMIT 1");
echo json_encode($stmt->fetch(PDO::FETCH_ASSOC));
?>

784
index.php Normal file
View File

@@ -0,0 +1,784 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tinyTask</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link rel="icon" href="" type="image/svg+xml" sizes="any">
<style>
html, body {
height: 100%;
margin: 0;
font-size: 8px;
}
.project-card {
background-color: #f9f9fc;
padding: 0px;
margin-bottom: 1rem;
width: 100%;
max-width: 100%;
width: 100%;
min-width: 200px;
font-size: 8px;
}
.project-header {
background-color: #e0e0f0;
padding: 0.2rem 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
}
.project-title {
flex-grow: 1;
font-weight: 800;
font-size: 1.4rem;
}
.project-body {
padding: 0.2rem 0.5rem;
font-size: 1.4rem;
}
.modal {
padding: 2rem; /* or try 3rem or a % like 5% */
}
.modal.show {
display: flex !important;
align-items: center;
justify-content: center;
}
.modal-dialog {
margin: 0;
}
.modal-content {
font-size: 2.0rem; /* slightly larger than Bootstrap's default (~0.875rem) */
}
.modal-title {
font-size: 2.0rem;
font-weight: 600;
}
.modal-body .form-control {
font-size: 2.0rem;
}
.modal-footer .btn {
font-size: 0.95rem;
padding: 0.5rem 1rem;
}
.add-project-btn {
font-size: 1.5rem; /* increase to your desired size */
line-height: 1;
}
.settings-btn {
font-size: 1.5rem; /* increase to your desired size */
line-height: 1;
color: #bbb;
}
.modal .btn {
font-size: 1.5rem; /* or 1.1rem for slightly larger */
}
.task-text {
font-weight: 700;
}
.highlighted {
border: 2px solid red;
border-radius: 6px;
padding: 2px 6px;
background-color: #transparent;
}
.btn-icon {
width: 20px;
height: 20px;
padding: 0;
font-size: 14px;
line-height: 1;
border-radius: 6px;
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
background-color: transparent;
border: none;
color: #555;
}
.btn-icon:hover,
.btn-icon:focus {
background-color: rgba(0, 0, 0, 0.05);
color: #000;
}
.btn-icon.text-danger:hover {
background-color: rgba(255, 0, 0, 0.1);
color: #d00;
}
.project-color-0 { background-color: #f9f9fc; }
.project-color-1 { background-color: #eefaf5; }
.project-color-2 { background-color: #fef7e0; }
.project-color-3 { background-color: #f0f4ff; }
.project-color-4 { background-color: #fff0f3; }
.project-color-5 { background-color: #f4f9f9; }
.project-color-6 { background-color: #f5f5ff; }
.project-color-7 { background-color: #f0fff4; }
.project-color-8 { background-color: #fff8e1; }
.project-color-9 { background-color: #eaf2ff; }
.project-color-0 .project-header { background-color: #e0e0f0; }
.project-color-1 .project-header { background-color: #d6f0e5; }
.project-color-2 .project-header { background-color: #fbeec2; }
.project-color-3 .project-header { background-color: #dce6ff; }
.project-color-4 .project-header { background-color: #ffe0e6; }
.project-color-5 .project-header { background-color: #dceeee; }
.project-color-6 .project-header { background-color: #e2e2ff; }
.project-color-7 .project-header { background-color: #d3f8e2; }
.project-color-8 .project-header { background-color: #fff3c2; }
.project-color-9 .project-header { background-color: #d0e4ff; }
.project-column {
width: 100%; /* 1 per row */
}
/* Small devices (landscape phones, ≥576px) */
@media (min-width: 576px) and (max-width: 767.98px) {
.project-column {
width: 50%; /* 2 per row */
}
}
/* Medium devices (tablets, ≥768px) */
@media (min-width: 768px) and (max-width: 991.98px) {
.project-column {
width: 33.3333%; /* 3 per row */
}
}
/* Large devices (desktops, ≥992px) */
@media (min-width: 992px) and (max-width: 1199.98px) {
.project-column {
width: 25%; /* 4 per row */
}
}
/* Extra Large (≥1200px) */
@media (min-width: 1200px) and (max-width: 1279.98px) {
.project-column {
width: 20%; /* 5 per row */
}
}
/* Super wide screens */
@media (min-width: 1280px) and (max-width: 1599.98px) {
.project-column {
width: 16.6666%; /* 6 per row */
}
}
@media (min-width: 1600px) and (max-width: 1919.98px) {
.project-column {
width: 13.0%; /* 8 per row */
}
}
@media (min-width: 1920px) and (max-width: 2559.98px) {
.project-column {
width: 11%; /* 10 per row */
}
}
@media (min-width: 2560px) {
.project-column {
width: 8.3333%; /* 12 per row */
}
}
</style>
</head>
<body>
<h1 class="mb-3 d-flex justify-content-between align-items-center px-3">
<span id="appTitle" class="d-flex align-items-center">
<i id="appIcon" class="bi me-2"></i>
<span class="fw-semibold me-2"></span>
<button class="btn btn-sm btn-clear p-0 settings-btn" data-bs-toggle="modal" data-bs-target="#settingsModal" title="Settings">
<i class="bi bi-gear-fill"></i>
</button>
</span>
<button class="btn btn-sm btn-success add-project-btn" data-bs-toggle="modal" data-bs-target="#addProjectModal" title="Add Project">
<i class="bi bi-plus"></i>
</button>
</h1>
<div class="container-fluid px-3">
<div class="row g-2" id="projectGrid" data-masonry='{"percentPosition": true }'>
<!-- JavaScript will populate projects here -->
</div>
</div>
<!-- Modal: Add New Project -->
<div class="modal fade" id="addProjectModal" tabindex="-1" aria-labelledby="addProjectModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="addProjectForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" class="form-control" id="projectName" placeholder="Project name" required>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
</div>
<!-- Modal: Add Task -->
<div class="modal fade" id="addTaskModal" tabindex="-1" aria-labelledby="addTaskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="addTaskForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addTaskModalLabel">Add Task</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" class="form-control" id="taskName" placeholder="Task name" required>
<input type="hidden" id="taskProjectId">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Save Task</button>
</div>
</div>
</form>
</div>
</div>
<!-- Modal: Add Subtask -->
<div class="modal fade" id="addSubtaskModal" tabindex="-1" aria-labelledby="addSubtaskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="addSubtaskForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addSubtaskModalLabel">Add Subtask</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" class="form-control" id="subtaskName" placeholder="Subtask name" required>
<input type="hidden" id="subtaskTaskId">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Save Subtask</button>
</div>
</div>
</form>
</div>
</div>
<!-- Modal: Settings -->
<div class="modal fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="settingsForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="settingsModalLabel">Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">App Title</label>
<input type="text" class="form-control" id="settingTitle">
</div>
<div class="mb-3">
<label class="form-label">Icon Class (e.g. kanban)</label>
<input type="text" class="form-control" id="settingIcon">
<div class="form-text">Use any <a href="https://icons.getbootstrap.com/" target="_blank">Bootstrap Icon</a> class.</div>
</div>
<div class="mb-3">
<label class="form-label">Icon Color</label>
<input type="color" class="form-control form-control-color" id="settingColor">
</div>
</div>
<div class="modal-footer">
<div class="d-flex justify-content-between gap-2 w-100">
<button type="button" class="btn btn-secondary" id="resetSettings">Reset to Default</button>
<button type="submit" class="btn btn-primary">Save Settings</button>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- Modal: Confirmation -->
<div class="modal fade" id="confirmationModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Action</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="confirmationModalBody">
Are you sure you want to continue?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmModalYes">Yes</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js"></script>
<script>
function showConfirmation(message, onConfirm) {
const modal = new bootstrap.Modal(document.getElementById('confirmationModal'));
document.getElementById('confirmationModalBody').textContent = message;
const confirmBtn = document.getElementById('confirmModalYes');
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn); // remove old handler
newConfirmBtn.addEventListener('click', () => {
modal.hide();
onConfirm();
});
modal.show();
}
function loadSettings() {
fetch('get_settings.php')
.then(res => res.json())
.then(data => {
// Update form fields
document.getElementById('settingTitle').value = data.title;
document.getElementById('settingIcon').value = data.icon_class;
document.getElementById('settingColor').value = data.icon_color;
// Update title area
document.getElementById('appTitle').innerHTML = `
<i id="appIcon" class="bi bi-${data.icon_class} me-2" style="color:${data.icon_color};"></i>
<span class="fw-semibold me-2">${data.title}</span>
<button class="btn btn-sm btn-clear p-0 settings-btn" data-bs-toggle="modal" data-bs-target="#settingsModal" title="Settings">
<i class="bi bi-gear-fill"></i>
</button>
`;
// Set favicon
const iconClass = data.icon_class; // e.g. "bi-kanban"
const iconName = iconClass.replace("bi-", ""); // e.g. "kanban"
const iconURL = `https://cdn.jsdelivr.net/npm/bootstrap-icons/icons/${iconName}.svg`;
fetch(iconURL)
.then(res => res.text())
.then(svg => {
const coloredSvg = svg
.replace(/fill="currentColor"/g, '') // remove existing fill
.replace(/<svg([^>]+)>/, `<svg$1 fill="${data.icon_color}">`);
const encoded = "data:image/svg+xml," + encodeURIComponent(coloredSvg);
let favicon = document.querySelector("link[rel~='icon']");
if (!favicon) {
favicon = document.createElement("link");
favicon.rel = "icon";
document.head.appendChild(favicon);
}
favicon.href = encoded;
favicon.type = "image/svg+xml";
});
});
}
function fetchProjects() {
fetch('get_data.php')
.then(res => res.json())
.then(data => {
const container = document.getElementById('projectGrid');
container.innerHTML = '';
data.forEach((project, idx) => {
const projectId = `project-${project.id}`;
const colorClass = `project-color-${idx % 10}`;
const col = document.createElement('div');
col.className = `col-12 col-sm-6 col-md-4 col-lg-2 col-xl-2 col-xxl-1 g-2 m-0 project-column`;
col.dataset.projectId = project.id;
col.classList.add("draggable-project");
col.innerHTML = `
<div class="project-card ${colorClass}">
<div class="project-header">
<span class="project-title">${project.name}</span>
<div class="d-flex align-items-center">
<button class="btn-icon add-task-btn" data-project-id="${project.id}" title="Add Task">
<i class="bi bi-plus"></i>
</button>
<button class="btn-icon text-danger delete-project-btn" data-project-id="${project.id}" title="Delete Project">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
<div class="project-body">
${project.tasks.map(task => `
<div class="d-flex justify-content-between align-items-center small-text mb-1 task-text">
<span class="${task.highlighted ? 'highlighted' : ''}" data-task-id="${task.id}">${task.name}</span>
<div class="d-flex">
<button class="btn-icon add-subtask-btn" data-task-id="${task.id}" title="Add Subtask">
<i class="bi bi-plus"></i>
</button>
<button class="btn-icon text-danger delete-task-btn" data-task-id="${task.id}" title="Delete Task">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
<div class="ms-3">
${task.subtasks.map(sub => `
<div class="d-flex justify-content-between align-items-center small-text mb-1">
<span class="${sub.highlighted ? 'highlighted' : ''}" data-subtask-id="${sub.id}">${sub.name}</span>
<button class="btn-icon text-danger delete-subtask-btn" data-subtask-id="${sub.id}" title="Delete Subtask">
<i class="bi bi-dash"></i>
</button>
</div>
`).join('')}
</div>
`).join('')}
</div>
</div>
`;
container.appendChild(col);
});
// Create the drop indicator element
const placeholder = document.createElement('div');
placeholder.className = 'project-column';
placeholder.innerHTML = `<div class="project-placeholder"></div>`;
// Setup drag events for project reordering
document.querySelectorAll('.project-column').forEach(col => {
col.setAttribute('draggable', 'true');
col.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', col.dataset.projectId);
col.classList.add('dragging');
});
col.addEventListener('dragend', () => {
col.classList.remove('dragging');
placeholder.remove();
});
col.addEventListener('dragover', (e) => {
e.preventDefault();
const dragging = document.querySelector('.dragging');
if (!dragging || dragging === col) return;
const bounding = col.getBoundingClientRect();
const offset = e.clientY - bounding.top;
if (offset < bounding.height / 2) {
col.parentNode.insertBefore(placeholder, col);
} else {
col.parentNode.insertBefore(placeholder, col.nextSibling);
}
});
col.addEventListener('drop', (e) => {
e.preventDefault();
const dragging = document.querySelector('.dragging');
if (placeholder && placeholder.parentNode) {
placeholder.parentNode.insertBefore(dragging, placeholder);
placeholder.remove();
updateProjectOrder(); // This already exists in your code
}
});
});
// Enable drag-and-drop sorting
new Sortable(container, {
animation: 150,
handle: '.project-header',
onEnd: function () {
const order = [...container.querySelectorAll('.project-column')].map((el, idx) => ({
id: el.dataset.projectId,
order: idx
}));
fetch('update_project_order.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(order)
}).then(() => {
fetchProjects();
});
}
});
const msnry = new Masonry(container, {
itemSelector: '.project-column',
columnWidth: '.project-column',
gutter: 0,
percentPosition: true
});
document.querySelectorAll('.add-task-btn').forEach(btn => {
btn.addEventListener('click', () => {
const projectId = btn.dataset.projectId;
document.getElementById('taskProjectId').value = projectId;
document.getElementById('taskName').value = '';
const modal = new bootstrap.Modal(document.getElementById('addTaskModal'));
modal.show();
});
});
document.querySelectorAll('.add-subtask-btn').forEach(btn => {
btn.addEventListener('click', () => {
const taskId = btn.dataset.taskId;
document.getElementById('subtaskTaskId').value = taskId;
document.getElementById('subtaskName').value = '';
const modal = new bootstrap.Modal(document.getElementById('addSubtaskModal'));
modal.show();
});
});
// Delete Project
document.querySelectorAll('.delete-project-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.projectId;
showConfirmation('Delete this project and all its tasks?', () => {
fetch(`delete_project.php?id=${id}`).then(() => fetchProjects());
});
});
});
// Delete Task
document.querySelectorAll('.delete-task-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.taskId;
showConfirmation('Delete this task and all its subtasks?', () => {
fetch(`delete_task.php?id=${id}`).then(() => fetchProjects());
});
});
});
// Delete Subtask
document.querySelectorAll('.delete-subtask-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.subtaskId;
showConfirmation('Delete this task?', () => {
fetch(`delete_subtask.php?id=${id}`).then(() => fetchProjects());
});
});
});
document.querySelectorAll('[data-task-id]').forEach(span => {
span.addEventListener('click', () => {
const taskId = span.dataset.taskId;
const isHighlighted = span.classList.toggle('highlighted');
fetch('toggle_highlight.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'task', id: taskId, highlighted: isHighlighted })
});
});
});
document.querySelectorAll('[data-subtask-id]').forEach(span => {
span.addEventListener('click', () => {
const subtaskId = span.dataset.subtaskId;
const isHighlighted = span.classList.toggle('highlighted');
fetch('toggle_highlight.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'subtask', id: subtaskId, highlighted: isHighlighted })
});
});
});
let itemToDelete = null;
document.querySelectorAll('.delete-task-btn').forEach(btn => {
btn.addEventListener('click', () => {
itemToDelete = btn.dataset.taskId;
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
modal.show();
});
});
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
if (itemToDelete) {
fetch(`delete_task.php?id=${itemToDelete}`)
.then(() => location.reload()); // or re-fetch your UI
}
});
});
}
document.getElementById('addProjectForm').addEventListener('submit', function(e) {
e.preventDefault();
const name = document.getElementById('projectName').value.trim();
if (!name) return;
fetch('add_project.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
})
.then(res => res.json())
.then(response => {
if (response.success) {
document.getElementById('projectName').value = '';
bootstrap.Modal.getInstance(document.getElementById('addProjectModal')).hide();
fetchProjects(); // or your equivalent project reloader
}
});
});
document.getElementById('addTaskForm').addEventListener('submit', function(e) {
e.preventDefault();
const name = document.getElementById('taskName').value.trim();
const projectId = document.getElementById('taskProjectId').value;
if (!name || !projectId) return;
fetch('add_task.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, project_id: projectId })
})
.then(res => res.json())
.then(response => {
if (response.success) {
bootstrap.Modal.getInstance(document.getElementById('addTaskModal')).hide();
fetchProjects();
}
});
});
document.getElementById('addSubtaskForm').addEventListener('submit', function(e) {
e.preventDefault();
const name = document.getElementById('subtaskName').value.trim();
const taskId = document.getElementById('subtaskTaskId').value;
if (!name || !taskId) return;
fetch('add_subtask.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, task_id: taskId })
})
.then(res => res.json())
.then(response => {
if (response.success) {
bootstrap.Modal.getInstance(document.getElementById('addSubtaskModal')).hide();
location.reload(); // or call your re-rendering logic
}
});
});
document.getElementById('settingsForm').addEventListener('submit', function (e) {
e.preventDefault();
const newTitle = document.getElementById('titleText').value.trim();
const newIcon = document.getElementById('iconClass').value.trim();
if (newTitle) {
document.getElementById('appTitle').childNodes[1].textContent = newTitle;
}
if (newIcon) {
const icon = document.getElementById('appIcon');
icon.className = `bi ${newIcon} me-2`;
}
bootstrap.Modal.getInstance(document.getElementById('settingsModal')).hide();
});
document.getElementById('settingsForm').addEventListener('submit', function (e) {
e.preventDefault();
const title = document.getElementById('settingTitle').value;
const icon_class = document.getElementById('settingIcon').value;
const icon_color = document.getElementById('settingColor').value;
fetch('update_settings.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, icon_class, icon_color })
})
.then(res => res.json())
.then(() => {
const modal = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
modal.hide();
loadSettings();
});
});
document.getElementById('resetSettings').addEventListener('click', () => {
fetch('reset_settings.php')
.then(() => {
loadSettings();
});
});
window.addEventListener('DOMContentLoaded', () => {
loadSettings();
});
document.addEventListener('DOMContentLoaded', fetchProjects);
</script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
</body>
</html>

1
info.php Normal file
View File

@@ -0,0 +1 @@
<?php phpinfo(); ?>

6
reset_settings.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
require 'db.php';
$stmt = $pdo->prepare("UPDATE settings SET title = 'tinyTask', icon_class = 'kanban', icon_color = '#ff0000' WHERE id = 1");
$stmt->execute();
echo json_encode(['success' => true]);
?>

784
test.php Normal file
View File

@@ -0,0 +1,784 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tinyTask</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link rel="icon" href="" type="image/svg+xml" sizes="any">
<style>
html, body {
height: 100%;
margin: 0;
font-size: 8px;
}
.project-card {
background-color: #f9f9fc;
padding: 0px;
margin-bottom: 1rem;
width: 100%;
max-width: 100%;
width: 100%;
min-width: 200px;
font-size: 8px;
}
.project-header {
background-color: #e0e0f0;
padding: 0.2rem 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
}
.project-title {
flex-grow: 1;
font-weight: 800;
font-size: 1.4rem;
}
.project-body {
padding: 0.2rem 0.5rem;
font-size: 1.4rem;
}
.modal {
padding: 2rem; /* or try 3rem or a % like 5% */
}
.modal.show {
display: flex !important;
align-items: center;
justify-content: center;
}
.modal-dialog {
margin: 0;
}
.modal-content {
font-size: 2.0rem; /* slightly larger than Bootstrap's default (~0.875rem) */
}
.modal-title {
font-size: 2.0rem;
font-weight: 600;
}
.modal-body .form-control {
font-size: 2.0rem;
}
.modal-footer .btn {
font-size: 0.95rem;
padding: 0.5rem 1rem;
}
.add-project-btn {
font-size: 1.5rem; /* increase to your desired size */
line-height: 1;
}
.settings-btn {
font-size: 1.5rem; /* increase to your desired size */
line-height: 1;
color: #bbb;
}
.modal .btn {
font-size: 1.5rem; /* or 1.1rem for slightly larger */
}
.task-text {
font-weight: 700;
}
.highlighted {
border: 2px solid red;
border-radius: 6px;
padding: 2px 6px;
background-color: #transparent;
}
.btn-icon {
width: 20px;
height: 20px;
padding: 0;
font-size: 14px;
line-height: 1;
border-radius: 6px;
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
background-color: transparent;
border: none;
color: #555;
}
.btn-icon:hover,
.btn-icon:focus {
background-color: rgba(0, 0, 0, 0.05);
color: #000;
}
.btn-icon.text-danger:hover {
background-color: rgba(255, 0, 0, 0.1);
color: #d00;
}
.project-color-0 { background-color: #f9f9fc; }
.project-color-1 { background-color: #eefaf5; }
.project-color-2 { background-color: #fef7e0; }
.project-color-3 { background-color: #f0f4ff; }
.project-color-4 { background-color: #fff0f3; }
.project-color-5 { background-color: #f4f9f9; }
.project-color-6 { background-color: #f5f5ff; }
.project-color-7 { background-color: #f0fff4; }
.project-color-8 { background-color: #fff8e1; }
.project-color-9 { background-color: #eaf2ff; }
.project-color-0 .project-header { background-color: #e0e0f0; }
.project-color-1 .project-header { background-color: #d6f0e5; }
.project-color-2 .project-header { background-color: #fbeec2; }
.project-color-3 .project-header { background-color: #dce6ff; }
.project-color-4 .project-header { background-color: #ffe0e6; }
.project-color-5 .project-header { background-color: #dceeee; }
.project-color-6 .project-header { background-color: #e2e2ff; }
.project-color-7 .project-header { background-color: #d3f8e2; }
.project-color-8 .project-header { background-color: #fff3c2; }
.project-color-9 .project-header { background-color: #d0e4ff; }
.project-column {
width: 100%; /* 1 per row */
}
/* Small devices (landscape phones, ≥576px) */
@media (min-width: 576px) and (max-width: 767.98px) {
.project-column {
width: 50%; /* 2 per row */
}
}
/* Medium devices (tablets, ≥768px) */
@media (min-width: 768px) and (max-width: 991.98px) {
.project-column {
width: 33.3333%; /* 3 per row */
}
}
/* Large devices (desktops, ≥992px) */
@media (min-width: 992px) and (max-width: 1199.98px) {
.project-column {
width: 25%; /* 4 per row */
}
}
/* Extra Large (≥1200px) */
@media (min-width: 1200px) and (max-width: 1279.98px) {
.project-column {
width: 20%; /* 5 per row */
}
}
/* Super wide screens */
@media (min-width: 1280px) and (max-width: 1599.98px) {
.project-column {
width: 16.6666%; /* 6 per row */
}
}
@media (min-width: 1600px) and (max-width: 1919.98px) {
.project-column {
width: 13.0%; /* 8 per row */
}
}
@media (min-width: 1920px) and (max-width: 2559.98px) {
.project-column {
width: 11%; /* 10 per row */
}
}
@media (min-width: 2560px) {
.project-column {
width: 8.3333%; /* 12 per row */
}
}
</style>
</head>
<body>
<h1 class="mb-3 d-flex justify-content-between align-items-center px-3">
<span id="appTitle" class="d-flex align-items-center">
<i id="appIcon" class="bi me-2"></i>
<span class="fw-semibold me-2"></span>
<button class="btn btn-sm btn-clear p-0 settings-btn" data-bs-toggle="modal" data-bs-target="#settingsModal" title="Settings">
<i class="bi bi-gear-fill"></i>
</button>
</span>
<button class="btn btn-sm btn-success add-project-btn" data-bs-toggle="modal" data-bs-target="#addProjectModal" title="Add Project">
<i class="bi bi-plus"></i>
</button>
</h1>
<div class="container-fluid px-3">
<div class="row g-2" id="projectGrid" data-masonry='{"percentPosition": true }'>
<!-- JavaScript will populate projects here -->
</div>
</div>
<!-- Modal: Add New Project -->
<div class="modal fade" id="addProjectModal" tabindex="-1" aria-labelledby="addProjectModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="addProjectForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add New Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" class="form-control" id="projectName" placeholder="Project name" required>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</form>
</div>
</div>
<!-- Modal: Add Task -->
<div class="modal fade" id="addTaskModal" tabindex="-1" aria-labelledby="addTaskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="addTaskForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addTaskModalLabel">Add Task</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" class="form-control" id="taskName" placeholder="Task name" required>
<input type="hidden" id="taskProjectId">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Save Task</button>
</div>
</div>
</form>
</div>
</div>
<!-- Modal: Add Subtask -->
<div class="modal fade" id="addSubtaskModal" tabindex="-1" aria-labelledby="addSubtaskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="addSubtaskForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addSubtaskModalLabel">Add Subtask</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" class="form-control" id="subtaskName" placeholder="Subtask name" required>
<input type="hidden" id="subtaskTaskId">
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Save Subtask</button>
</div>
</div>
</form>
</div>
</div>
<!-- Modal: Settings -->
<div class="modal fade" id="settingsModal" tabindex="-1" aria-labelledby="settingsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form id="settingsForm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="settingsModalLabel">Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">App Title</label>
<input type="text" class="form-control" id="settingTitle">
</div>
<div class="mb-3">
<label class="form-label">Icon Class (e.g. kanban)</label>
<input type="text" class="form-control" id="settingIcon">
<div class="form-text">Use any <a href="https://icons.getbootstrap.com/" target="_blank">Bootstrap Icon</a> class.</div>
</div>
<div class="mb-3">
<label class="form-label">Icon Color</label>
<input type="color" class="form-control form-control-color" id="settingColor">
</div>
</div>
<div class="modal-footer">
<div class="d-flex justify-content-between gap-2 w-100">
<button type="button" class="btn btn-secondary" id="resetSettings">Reset to Default</button>
<button type="submit" class="btn btn-primary">Save Settings</button>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- Modal: Confirmation -->
<div class="modal fade" id="confirmationModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Action</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="confirmationModalBody">
Are you sure you want to continue?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmModalYes">Yes</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js"></script>
<script>
function showConfirmation(message, onConfirm) {
const modal = new bootstrap.Modal(document.getElementById('confirmationModal'));
document.getElementById('confirmationModalBody').textContent = message;
const confirmBtn = document.getElementById('confirmModalYes');
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn); // remove old handler
newConfirmBtn.addEventListener('click', () => {
modal.hide();
onConfirm();
});
modal.show();
}
function loadSettings() {
fetch('get_settings.php')
.then(res => res.json())
.then(data => {
// Update form fields
document.getElementById('settingTitle').value = data.title;
document.getElementById('settingIcon').value = data.icon_class;
document.getElementById('settingColor').value = data.icon_color;
// Update title area
document.getElementById('appTitle').innerHTML = `
<i id="appIcon" class="bi bi-${data.icon_class} me-2" style="color:${data.icon_color};"></i>
<span class="fw-semibold me-2">${data.title}</span>
<button class="btn btn-sm btn-clear p-0 settings-btn" data-bs-toggle="modal" data-bs-target="#settingsModal" title="Settings">
<i class="bi bi-gear-fill"></i>
</button>
`;
// Set favicon
const iconClass = data.icon_class; // e.g. "bi-kanban"
const iconName = iconClass.replace("bi-", ""); // e.g. "kanban"
const iconURL = `https://cdn.jsdelivr.net/npm/bootstrap-icons/icons/${iconName}.svg`;
fetch(iconURL)
.then(res => res.text())
.then(svg => {
const coloredSvg = svg
.replace(/fill="currentColor"/g, '') // remove existing fill
.replace(/<svg([^>]+)>/, `<svg$1 fill="${data.icon_color}">`);
const encoded = "data:image/svg+xml," + encodeURIComponent(coloredSvg);
let favicon = document.querySelector("link[rel~='icon']");
if (!favicon) {
favicon = document.createElement("link");
favicon.rel = "icon";
document.head.appendChild(favicon);
}
favicon.href = encoded;
favicon.type = "image/svg+xml";
});
});
}
function fetchProjects() {
fetch('get_data.php')
.then(res => res.json())
.then(data => {
const container = document.getElementById('projectGrid');
container.innerHTML = '';
data.forEach((project, idx) => {
const projectId = `project-${project.id}`;
const colorClass = `project-color-${idx % 10}`;
const col = document.createElement('div');
col.className = `col-12 col-sm-6 col-md-4 col-lg-2 col-xl-2 col-xxl-1 g-2 m-0 project-column`;
col.dataset.projectId = project.id;
col.classList.add("draggable-project");
col.innerHTML = `
<div class="project-card ${colorClass}">
<div class="project-header">
<span class="project-title">${project.name}</span>
<div class="d-flex align-items-center">
<button class="btn-icon add-task-btn" data-project-id="${project.id}" title="Add Task">
<i class="bi bi-plus"></i>
</button>
<button class="btn-icon text-danger delete-project-btn" data-project-id="${project.id}" title="Delete Project">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
<div class="project-body">
${project.tasks.map(task => `
<div class="d-flex justify-content-between align-items-center small-text mb-1 task-text">
<span class="${task.highlighted ? 'highlighted' : ''}" data-task-id="${task.id}">${task.name}</span>
<div class="d-flex">
<button class="btn-icon add-subtask-btn" data-task-id="${task.id}" title="Add Subtask">
<i class="bi bi-plus"></i>
</button>
<button class="btn-icon text-danger delete-task-btn" data-task-id="${task.id}" title="Delete Task">
<i class="bi bi-dash"></i>
</button>
</div>
</div>
<div class="ms-3">
${task.subtasks.map(sub => `
<div class="d-flex justify-content-between align-items-center small-text mb-1">
<span class="${sub.highlighted ? 'highlighted' : ''}" data-subtask-id="${sub.id}">${sub.name}</span>
<button class="btn-icon text-danger delete-subtask-btn" data-subtask-id="${sub.id}" title="Delete Subtask">
<i class="bi bi-dash"></i>
</button>
</div>
`).join('')}
</div>
`).join('')}
</div>
</div>
`;
container.appendChild(col);
});
// Create the drop indicator element
const placeholder = document.createElement('div');
placeholder.className = 'project-column';
placeholder.innerHTML = `<div class="project-placeholder"></div>`;
// Setup drag events for project reordering
document.querySelectorAll('.project-column').forEach(col => {
col.setAttribute('draggable', 'true');
col.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', col.dataset.projectId);
col.classList.add('dragging');
});
col.addEventListener('dragend', () => {
col.classList.remove('dragging');
placeholder.remove();
});
col.addEventListener('dragover', (e) => {
e.preventDefault();
const dragging = document.querySelector('.dragging');
if (!dragging || dragging === col) return;
const bounding = col.getBoundingClientRect();
const offset = e.clientY - bounding.top;
if (offset < bounding.height / 2) {
col.parentNode.insertBefore(placeholder, col);
} else {
col.parentNode.insertBefore(placeholder, col.nextSibling);
}
});
col.addEventListener('drop', (e) => {
e.preventDefault();
const dragging = document.querySelector('.dragging');
if (placeholder && placeholder.parentNode) {
placeholder.parentNode.insertBefore(dragging, placeholder);
placeholder.remove();
updateProjectOrder(); // This already exists in your code
}
});
});
// Enable drag-and-drop sorting
new Sortable(container, {
animation: 150,
handle: '.project-header',
onEnd: function () {
const order = [...container.querySelectorAll('.project-column')].map((el, idx) => ({
id: el.dataset.projectId,
order: idx
}));
fetch('update_project_order.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(order)
}).then(() => {
fetchProjects();
});
}
});
const msnry = new Masonry(container, {
itemSelector: '.project-column',
columnWidth: '.project-column',
gutter: 0,
percentPosition: true
});
document.querySelectorAll('.add-task-btn').forEach(btn => {
btn.addEventListener('click', () => {
const projectId = btn.dataset.projectId;
document.getElementById('taskProjectId').value = projectId;
document.getElementById('taskName').value = '';
const modal = new bootstrap.Modal(document.getElementById('addTaskModal'));
modal.show();
});
});
document.querySelectorAll('.add-subtask-btn').forEach(btn => {
btn.addEventListener('click', () => {
const taskId = btn.dataset.taskId;
document.getElementById('subtaskTaskId').value = taskId;
document.getElementById('subtaskName').value = '';
const modal = new bootstrap.Modal(document.getElementById('addSubtaskModal'));
modal.show();
});
});
// Delete Project
document.querySelectorAll('.delete-project-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.projectId;
showConfirmation('Delete this project and all its tasks?', () => {
fetch(`delete_project.php?id=${id}`).then(() => fetchProjects());
});
});
});
// Delete Task
document.querySelectorAll('.delete-task-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.taskId;
showConfirmation('Delete this task and all its subtasks?', () => {
fetch(`delete_task.php?id=${id}`).then(() => fetchProjects());
});
});
});
// Delete Subtask
document.querySelectorAll('.delete-subtask-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.subtaskId;
showConfirmation('Delete this task?', () => {
fetch(`delete_subtask.php?id=${id}`).then(() => fetchProjects());
});
});
});
document.querySelectorAll('[data-task-id]').forEach(span => {
span.addEventListener('click', () => {
const taskId = span.dataset.taskId;
const isHighlighted = span.classList.toggle('highlighted');
fetch('toggle_highlight.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'task', id: taskId, highlighted: isHighlighted })
});
});
});
document.querySelectorAll('[data-subtask-id]').forEach(span => {
span.addEventListener('click', () => {
const subtaskId = span.dataset.subtaskId;
const isHighlighted = span.classList.toggle('highlighted');
fetch('toggle_highlight.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'subtask', id: subtaskId, highlighted: isHighlighted })
});
});
});
let itemToDelete = null;
document.querySelectorAll('.delete-task-btn').forEach(btn => {
btn.addEventListener('click', () => {
itemToDelete = btn.dataset.taskId;
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
modal.show();
});
});
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
if (itemToDelete) {
fetch(`delete_task.php?id=${itemToDelete}`)
.then(() => location.reload()); // or re-fetch your UI
}
});
});
}
document.getElementById('addProjectForm').addEventListener('submit', function(e) {
e.preventDefault();
const name = document.getElementById('projectName').value.trim();
if (!name) return;
fetch('add_project.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
})
.then(res => res.json())
.then(response => {
if (response.success) {
document.getElementById('projectName').value = '';
bootstrap.Modal.getInstance(document.getElementById('addProjectModal')).hide();
fetchProjects(); // or your equivalent project reloader
}
});
});
document.getElementById('addTaskForm').addEventListener('submit', function(e) {
e.preventDefault();
const name = document.getElementById('taskName').value.trim();
const projectId = document.getElementById('taskProjectId').value;
if (!name || !projectId) return;
fetch('add_task.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, project_id: projectId })
})
.then(res => res.json())
.then(response => {
if (response.success) {
bootstrap.Modal.getInstance(document.getElementById('addTaskModal')).hide();
fetchProjects();
}
});
});
document.getElementById('addSubtaskForm').addEventListener('submit', function(e) {
e.preventDefault();
const name = document.getElementById('subtaskName').value.trim();
const taskId = document.getElementById('subtaskTaskId').value;
if (!name || !taskId) return;
fetch('add_subtask.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, task_id: taskId })
})
.then(res => res.json())
.then(response => {
if (response.success) {
bootstrap.Modal.getInstance(document.getElementById('addSubtaskModal')).hide();
location.reload(); // or call your re-rendering logic
}
});
});
document.getElementById('settingsForm').addEventListener('submit', function (e) {
e.preventDefault();
const newTitle = document.getElementById('titleText').value.trim();
const newIcon = document.getElementById('iconClass').value.trim();
if (newTitle) {
document.getElementById('appTitle').childNodes[1].textContent = newTitle;
}
if (newIcon) {
const icon = document.getElementById('appIcon');
icon.className = `bi ${newIcon} me-2`;
}
bootstrap.Modal.getInstance(document.getElementById('settingsModal')).hide();
});
document.getElementById('settingsForm').addEventListener('submit', function (e) {
e.preventDefault();
const title = document.getElementById('settingTitle').value;
const icon_class = document.getElementById('settingIcon').value;
const icon_color = document.getElementById('settingColor').value;
fetch('update_settings.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, icon_class, icon_color })
})
.then(res => res.json())
.then(() => {
const modal = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
modal.hide();
loadSettings();
});
});
document.getElementById('resetSettings').addEventListener('click', () => {
fetch('reset_settings.php')
.then(() => {
loadSettings();
});
});
window.addEventListener('DOMContentLoaded', () => {
loadSettings();
});
document.addEventListener('DOMContentLoaded', fetchProjects);
</script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
</body>
</html>

172
tinytask.sql Normal file
View File

@@ -0,0 +1,172 @@
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- Host: 127.0.0.1
-- Generation Time: May 09, 2025 at 09:19 AM
-- Server version: 10.4.32-MariaDB
-- PHP Version: 8.2.12
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!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 */;
--
-- Database: `taskmanage`
--
-- --------------------------------------------------------
--
-- Table structure for table `projects`
--
CREATE TABLE `projects` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`sort_order` int(11) DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
--
-- Table structure for table `settings`
--
CREATE TABLE `settings` (
`id` int(11) NOT NULL,
`title` varchar(255) DEFAULT 'Task Management',
`icon_class` varchar(255) DEFAULT 'bi-kanban',
`icon_color` varchar(50) DEFAULT '#6c757d'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--
-- Dumping data for table `settings`
--
-- --------------------------------------------------------
--
-- Table structure for table `subtasks`
--
CREATE TABLE `subtasks` (
`id` int(11) NOT NULL,
`task_id` int(11) DEFAULT NULL,
`name` varchar(255) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`highlighted` tinyint(1) DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--
-- Dumping data for table `subtasks`
--
-- --------------------------------------------------------
--
-- Table structure for table `tasks`
--
CREATE TABLE `tasks` (
`id` int(11) NOT NULL,
`project_id` int(11) DEFAULT NULL,
`name` varchar(255) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`highlighted` tinyint(1) DEFAULT 0,
`task_order` int(11) DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
--
-- Dumping data for table `tasks`
--
--
-- Indexes for dumped tables
--
--
-- Indexes for table `projects`
--
ALTER TABLE `projects`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `settings`
--
ALTER TABLE `settings`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `subtasks`
--
ALTER TABLE `subtasks`
ADD PRIMARY KEY (`id`),
ADD KEY `task_id` (`task_id`);
--
-- Indexes for table `tasks`
--
ALTER TABLE `tasks`
ADD PRIMARY KEY (`id`),
ADD KEY `project_id` (`project_id`);
--
-- AUTO_INCREMENT for dumped tables
--
--
-- AUTO_INCREMENT for table `projects`
--
ALTER TABLE `projects`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=48;
--
-- AUTO_INCREMENT for table `settings`
--
ALTER TABLE `settings`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `subtasks`
--
ALTER TABLE `subtasks`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=64;
--
-- AUTO_INCREMENT for table `tasks`
--
ALTER TABLE `tasks`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=78;
--
-- Constraints for dumped tables
--
--
-- Constraints for table `subtasks`
--
ALTER TABLE `subtasks`
ADD CONSTRAINT `subtasks_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE;
--
-- Constraints for table `tasks`
--
ALTER TABLE `tasks`
ADD CONSTRAINT `tasks_ibfk_1` FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`) ON DELETE CASCADE;
COMMIT;
/*!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 */;

21
toggle_highlight.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
require 'db.php';
$data = json_decode(file_get_contents('php://input'), true);
$type = $data['type'];
$id = intval($data['id']);
$highlighted = isset($data['highlighted']) && $data['highlighted'] ? 1 : 0;
if ($type === 'task') {
$stmt = $pdo->prepare("UPDATE tasks SET highlighted = ? WHERE id = ?");
$stmt->execute([$highlighted, $id]);
echo json_encode(['success' => true]);
} elseif ($type === 'subtask') {
$stmt = $pdo->prepare("UPDATE subtasks SET highlighted = ? WHERE id = ?");
$stmt->execute([$highlighted, $id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Invalid type']);
}
?>

12
update_project.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
$data = json_decode(file_get_contents('php://input'), true);
$id = intval($data['id']);
$name = trim($data['name']);
if ($id && $name) {
$pdo = new PDO('sqlite:data.db');
$stmt = $pdo->prepare('UPDATE projects SET name = ? WHERE id = ?');
echo json_encode(['success' => $stmt->execute([$name, $id])]);
} else {
echo json_encode(['success' => false]);
}

25
update_project_order.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
require 'db.php';
header('Content-Type: application/json');
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !is_array($data)) {
echo json_encode(['success' => false, 'message' => 'Invalid input']);
exit;
}
$stmt = $pdo->prepare("UPDATE projects SET sort_order = ? WHERE id = ?");
foreach ($data as $item) {
$id = $item['id'];
$order = $item['order'];
if (!is_numeric($id) || !is_numeric($order)) continue;
$stmt->execute([$order, $id]);
}
echo json_encode(['success' => true]);
exit;

13
update_settings.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
require 'db.php';
$data = json_decode(file_get_contents('php://input'), true);
$title = $data['title'] ?? 'tinyTask';
$icon = $data['icon_class'] ?? 'kanban';
$color = $data['icon_color'] ?? '#ff0000';
$stmt = $pdo->prepare("UPDATE settings SET title = ?, icon_class = ?, icon_color = ? WHERE id = 1");
$stmt->execute([$title, $icon, $color]);
echo json_encode(['success' => true]);
?>

11
update_task_order.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
require 'db.php'; // Adjust path if needed
$data = json_decode(file_get_contents("php://input"), true);
if (isset($data['task_ids']) && is_array($data['task_ids'])) {
$stmt = $pdo->prepare("UPDATE tasks SET task_order = ? WHERE id = ?");
foreach ($data['task_ids'] as $order => $id) {
$stmt->execute([$order, $id]);
}
echo json_encode