Updated SQL File to remove dummy data.
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal 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
19
TODO.md
Normal 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
15
add_project.php
Normal 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
18
add_subtask.php
Normal 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
18
add_task.php
Normal 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
21
db.php
Normal 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
6
delete_project.php
Normal 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
6
delete_subtask.php
Normal 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
6
delete_task.php
Normal 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
19
get_data.php
Normal 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
5
get_settings.php
Normal 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
784
index.php
Normal 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>
|
||||
6
reset_settings.php
Normal file
6
reset_settings.php
Normal 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
784
test.php
Normal 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
172
tinytask.sql
Normal 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
21
toggle_highlight.php
Normal 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
12
update_project.php
Normal 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
25
update_project_order.php
Normal 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
13
update_settings.php
Normal 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
11
update_task_order.php
Normal 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
|
||||
Reference in New Issue
Block a user