Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Dev Workflow Tracker</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.gradient-bg { | |
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); | |
} | |
.card-hover:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
} | |
.transition-all { | |
transition: all 0.3s ease; | |
} | |
.command-box { | |
font-family: 'Courier New', monospace; | |
background-color: #2d3748; | |
color: #f7fafc; | |
} | |
.status-pending { | |
background-color: #f6e05e; | |
color: #975a16; | |
} | |
.status-success { | |
background-color: #68d391; | |
color: #276749; | |
} | |
.status-failed { | |
background-color: #fc8181; | |
color: #9b2c2c; | |
} | |
.status-merged { | |
background-color: #63b3ed; | |
color: #2c5282; | |
} | |
.service-tag { | |
display: inline-flex; | |
align-items: center; | |
background-color: #ebf8ff; | |
color: #3182ce; | |
padding: 0.25rem 0.5rem; | |
border-radius: 0.25rem; | |
margin-right: 0.25rem; | |
margin-bottom: 0.25rem; | |
} | |
.service-tag-remove { | |
margin-left: 0.25rem; | |
cursor: pointer; | |
color: #3182ce; | |
} | |
.service-input-container { | |
display: flex; | |
flex-wrap: wrap; | |
align-items: center; | |
min-height: 42px; | |
padding: 0.25rem; | |
border: 1px solid #d1d5db; | |
border-radius: 0.375rem; | |
} | |
.service-input { | |
flex: 1; | |
min-width: 150px; | |
border: none; | |
outline: none; | |
padding: 0.25rem; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 min-h-screen"> | |
<div class="gradient-bg text-white py-6 shadow-lg"> | |
<div class="container mx-auto px-4"> | |
<div class="flex justify-between items-center"> | |
<h1 class="text-3xl font-bold flex items-center"> | |
<i class="fas fa-code-branch mr-3"></i> Dev Workflow Tracker | |
</h1> | |
<button id="newTaskBtn" class="bg-white text-blue-800 px-4 py-2 rounded-lg font-semibold hover:bg-blue-100 transition-all"> | |
<i class="fas fa-plus mr-2"></i> New Task | |
</button> | |
</div> | |
</div> | |
</div> | |
<div class="container mx-auto px-4 py-8"> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
<!-- Tasks Column --> | |
<div class="bg-white rounded-lg shadow-md p-6"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 class="text-xl font-semibold text-gray-800"> | |
<i class="fas fa-tasks mr-2 text-blue-500"></i> Active Tasks | |
</h2> | |
<span class="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm font-medium"> | |
<span id="taskCount">0</span> tasks | |
</span> | |
</div> | |
<div id="tasksContainer" class="space-y-4"> | |
<!-- Tasks will be added here dynamically --> | |
</div> | |
</div> | |
<!-- Merge Status Column --> | |
<div class="bg-white rounded-lg shadow-md p-6"> | |
<h2 class="text-xl font-semibold text-gray-800 mb-6"> | |
<i class="fas fa-code-merge mr-2 text-green-500"></i> Merge Status | |
</h2> | |
<div id="mergeStatusContainer" class="space-y-4"> | |
<!-- Merge status will be added here dynamically --> | |
</div> | |
</div> | |
<!-- Commands Column --> | |
<div class="bg-white rounded-lg shadow-md p-6"> | |
<h2 class="text-xl font-semibold text-gray-800 mb-6"> | |
<i class="fas fa-terminal mr-2 text-purple-500"></i> Common Commands | |
</h2> | |
<div class="space-y-4"> | |
<div class="command-box p-4 rounded-lg"> | |
<div class="flex justify-between items-center mb-2"> | |
<span class="font-semibold">Git Branch</span> | |
<button class="copy-btn text-blue-400 hover:text-blue-300" data-clipboard-text="git checkout -b feature/your-feature"> | |
<i class="far fa-copy"></i> | |
</button> | |
</div> | |
<code>git checkout -b feature/your-feature</code> | |
</div> | |
<div class="command-box p-4 rounded-lg"> | |
<div class="flex justify-between items-center mb-2"> | |
<span class="font-semibold">Rebase</span> | |
<button class="copy-btn text-blue-400 hover:text-blue-300" data-clipboard-text="git pull --rebase origin dev"> | |
<i class="far fa-copy"></i> | |
</button> | |
</div> | |
<code>git pull --rebase origin dev</code> | |
</div> | |
<div class="command-box p-4 rounded-lg"> | |
<div class="flex justify-between items-center mb-2"> | |
<span class="font-semibold">Merge</span> | |
<button class="copy-btn text-blue-400 hover:text-blue-300" data-clipboard-text="git merge --no-ff feature/your-feature"> | |
<i class="far fa-copy"></i> | |
</button> | |
</div> | |
<code>git merge --no-ff feature/your-feature</code> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- New Task Modal --> | |
<div id="taskModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden"> | |
<div class="bg-white rounded-lg shadow-xl w-full max-w-md mx-4"> | |
<div class="gradient-bg text-white rounded-t-lg px-6 py-4"> | |
<h2 class="text-xl font-bold">Add New Task</h2> | |
</div> | |
<div class="p-6"> | |
<form id="taskForm"> | |
<div class="mb-4"> | |
<label class="block text-gray-700 font-medium mb-2" for="taskType">Task Type</label> | |
<select id="taskType" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
<option value="feature">Feature</option> | |
<option value="bug">Bug</option> | |
<option value="hotfix">Hotfix</option> | |
<option value="refactor">Refactor</option> | |
</select> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-gray-700 font-medium mb-2" for="taskTitle">Title</label> | |
<input type="text" id="taskTitle" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="What are you working on?"> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-gray-700 font-medium mb-2" for="branchName">Branch Name</label> | |
<input type="text" id="branchName" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="feature/your-feature"> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-gray-700 font-medium mb-2">Affected Services</label> | |
<div class="service-input-container"> | |
<div id="serviceTagsContainer" class="flex flex-wrap"></div> | |
<input type="text" id="serviceInput" class="service-input" placeholder="Type service name and press Enter"> | |
</div> | |
<div class="mt-1 text-xs text-gray-500">Press Enter to add a service</div> | |
</div> | |
<div class="mb-4"> | |
<label class="block text-gray-700 font-medium mb-2" for="mergeCommands">Merge Commands</label> | |
<textarea id="mergeCommands" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" rows="3" placeholder="Any special merge commands or notes"></textarea> | |
</div> | |
<div class="flex justify-end space-x-3"> | |
<button type="button" id="cancelTaskBtn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-100">Cancel</button> | |
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Add Task</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Sample data for demonstration | |
const sampleTasks = [ | |
{ | |
id: 1, | |
type: 'feature', | |
title: 'Add user profile page', | |
branch: 'feature/user-profile', | |
services: ['Frontend', 'User Service'], | |
commands: 'git pull --rebase origin dev\nnpm run test\n', | |
devStatus: 'pending', | |
masterStatus: 'pending' | |
}, | |
{ | |
id: 2, | |
type: 'bug', | |
title: 'Fix payment validation', | |
branch: 'bugfix/payment-validation', | |
services: ['Payment Service'], | |
commands: 'git fetch origin\n', | |
devStatus: 'merged', | |
masterStatus: 'pending' | |
} | |
]; | |
let tasks = JSON.parse(localStorage.getItem('devTasks')) || sampleTasks; | |
let currentEditingTaskId = null; | |
// DOM elements | |
const tasksContainer = document.getElementById('tasksContainer'); | |
const mergeStatusContainer = document.getElementById('mergeStatusContainer'); | |
const taskModal = document.getElementById('taskModal'); | |
const newTaskBtn = document.getElementById('newTaskBtn'); | |
const cancelTaskBtn = document.getElementById('cancelTaskBtn'); | |
const taskForm = document.getElementById('taskForm'); | |
const taskCount = document.getElementById('taskCount'); | |
const serviceInput = document.getElementById('serviceInput'); | |
const serviceTagsContainer = document.getElementById('serviceTagsContainer'); | |
// Initialize clipboard functionality | |
document.querySelectorAll('.copy-btn').forEach(btn => { | |
btn.addEventListener('click', function() { | |
const text = this.getAttribute('data-clipboard-text'); | |
navigator.clipboard.writeText(text).then(() => { | |
const originalIcon = this.innerHTML; | |
this.innerHTML = '<i class="fas fa-check"></i>'; | |
setTimeout(() => { | |
this.innerHTML = originalIcon; | |
}, 2000); | |
}); | |
}); | |
}); | |
// Modal controls | |
newTaskBtn.addEventListener('click', () => { | |
currentEditingTaskId = null; | |
serviceTagsContainer.innerHTML = ''; | |
taskModal.classList.remove('hidden'); | |
}); | |
cancelTaskBtn.addEventListener('click', () => { | |
taskModal.classList.add('hidden'); | |
}); | |
// Service input functionality | |
serviceInput.addEventListener('keydown', function(e) { | |
if (e.key === 'Enter' && this.value.trim()) { | |
e.preventDefault(); | |
addServiceTag(this.value.trim()); | |
this.value = ''; | |
} | |
}); | |
function addServiceTag(serviceName) { | |
const tagId = Date.now(); | |
const tagElement = document.createElement('div'); | |
tagElement.className = 'service-tag'; | |
tagElement.innerHTML = ` | |
${serviceName} | |
<span class="service-tag-remove" data-tag-id="${tagId}"> | |
<i class="fas fa-times"></i> | |
</span> | |
`; | |
serviceTagsContainer.appendChild(tagElement); | |
// Add remove event listener | |
tagElement.querySelector('.service-tag-remove').addEventListener('click', function() { | |
this.parentElement.remove(); | |
}); | |
} | |
function renderServiceTags(services) { | |
serviceTagsContainer.innerHTML = ''; | |
services.forEach(service => { | |
addServiceTag(service); | |
}); | |
} | |
function getCurrentServices() { | |
const serviceTags = Array.from(serviceTagsContainer.querySelectorAll('.service-tag')); | |
return serviceTags.map(tag => { | |
return tag.textContent.trim().replace('×', '').trim(); | |
}); | |
} | |
// Form submission | |
taskForm.addEventListener('submit', function(e) { | |
e.preventDefault(); | |
const type = document.getElementById('taskType').value; | |
const title = document.getElementById('taskTitle').value.trim(); | |
const branch = document.getElementById('branchName').value.trim(); | |
if (!title || !branch) { | |
alert('Please fill in all required fields'); | |
return; | |
} | |
// Get services from tags | |
const services = getCurrentServices(); | |
const commands = document.getElementById('mergeCommands').value.trim(); | |
if (currentEditingTaskId) { | |
// Update existing task | |
tasks = tasks.map(task => { | |
if (task.id === currentEditingTaskId) { | |
return { | |
...task, | |
type, | |
title, | |
branch, | |
services, | |
commands | |
}; | |
} | |
return task; | |
}); | |
} else { | |
// Create new task | |
const newTask = { | |
id: Date.now(), | |
type, | |
title, | |
branch, | |
services, | |
commands, | |
devStatus: 'pending', | |
masterStatus: 'pending' | |
}; | |
tasks.push(newTask); | |
} | |
saveTasks(); | |
renderTasks(); | |
renderMergeStatus(); | |
taskModal.classList.add('hidden'); | |
taskForm.reset(); | |
serviceTagsContainer.innerHTML = ''; | |
currentEditingTaskId = null; | |
}); | |
// Save tasks to localStorage | |
function saveTasks() { | |
localStorage.setItem('devTasks', JSON.stringify(tasks)); | |
updateTaskCount(); | |
} | |
// Update task count | |
function updateTaskCount() { | |
taskCount.textContent = tasks.length; | |
} | |
// Render tasks | |
function renderTasks() { | |
tasksContainer.innerHTML = ''; | |
tasks.forEach(task => { | |
const taskElement = document.createElement('div'); | |
taskElement.className = 'bg-white border border-gray-200 rounded-lg p-4 shadow-sm card-hover transition-all'; | |
const typeIcon = task.type === 'feature' ? 'fa-star' : | |
task.type === 'bug' ? 'fa-bug' : | |
task.type === 'hotfix' ? 'fa-fire' : 'fa-code'; | |
taskElement.innerHTML = ` | |
<div class="flex justify-between items-start mb-2"> | |
<div class="flex items-center"> | |
<i class="fas ${typeIcon} mr-2 ${task.type === 'feature' ? 'text-blue-500' : 'text-red-500'}"></i> | |
<h3 class="font-semibold text-gray-800">${task.title}</h3> | |
</div> | |
<button class="delete-task text-gray-400 hover:text-red-500" data-id="${task.id}"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div class="mb-2"> | |
<span class="text-sm font-medium text-gray-600">Branch:</span> | |
<span class="branch-name text-sm ml-2 font-mono bg-gray-100 px-2 py-1 rounded cursor-pointer hover:bg-gray-200" data-id="${task.id}">${task.branch}</span> | |
</div> | |
<div class="mb-3"> | |
<span class="text-sm font-medium text-gray-600">Services:</span> | |
<div class="flex flex-wrap gap-1 mt-1"> | |
${task.services.map(service => ` | |
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">${service}</span> | |
`).join('')} | |
</div> | |
</div> | |
${task.commands ? ` | |
<div class="mb-3"> | |
<span class="text-sm font-medium text-gray-600">Commands:</span> | |
<div class="command-box p-2 rounded mt-1 text-xs"> | |
<pre>${task.commands}</pre> | |
</div> | |
</div> | |
` : ''} | |
<div class="flex justify-between items-center pt-2 border-t border-gray-100"> | |
<div> | |
<span class="text-xs text-gray-500">Created: ${new Date(task.id).toLocaleDateString()}</span> | |
</div> | |
<div class="flex space-x-2"> | |
<button class="edit-task text-blue-500 hover:text-blue-700 text-sm" data-id="${task.id}"> | |
<i class="fas fa-edit mr-1"></i>Edit | |
</button> | |
</div> | |
</div> | |
`; | |
tasksContainer.appendChild(taskElement); | |
}); | |
// Add event listeners for branch name clicks (status toggle) | |
document.querySelectorAll('.branch-name').forEach(branch => { | |
branch.addEventListener('click', function() { | |
const taskId = parseInt(this.getAttribute('data-id')); | |
const task = tasks.find(t => t.id === taskId); | |
if (task) { | |
if (task.devStatus === 'pending') { | |
task.devStatus = 'merged'; | |
} else if (task.masterStatus === 'pending') { | |
task.masterStatus = 'merged'; | |
} else { | |
// Reset both statuses if both are merged | |
task.devStatus = 'pending'; | |
task.masterStatus = 'pending'; | |
} | |
saveTasks(); | |
renderTasks(); | |
renderMergeStatus(); | |
} | |
}); | |
}); | |
// Add event listeners for delete buttons | |
document.querySelectorAll('.delete-task').forEach(btn => { | |
btn.addEventListener('click', function() { | |
const taskId = parseInt(this.getAttribute('data-id')); | |
tasks = tasks.filter(task => task.id !== taskId); | |
saveTasks(); | |
renderTasks(); | |
renderMergeStatus(); | |
}); | |
}); | |
// Add event listeners for edit buttons | |
document.querySelectorAll('.edit-task').forEach(btn => { | |
btn.addEventListener('click', function() { | |
const taskId = parseInt(this.getAttribute('data-id')); | |
const task = tasks.find(t => t.id === taskId); | |
if (task) { | |
currentEditingTaskId = taskId; | |
// Populate the form with task data | |
document.getElementById('taskType').value = task.type; | |
document.getElementById('taskTitle').value = task.title; | |
document.getElementById('branchName').value = task.branch; | |
document.getElementById('mergeCommands').value = task.commands || ''; | |
// Render service tags | |
renderServiceTags(task.services); | |
// Show the modal | |
taskModal.classList.remove('hidden'); | |
} | |
}); | |
}); | |
updateTaskCount(); | |
} | |
// Render merge status | |
function renderMergeStatus() { | |
mergeStatusContainer.innerHTML = ''; | |
tasks.forEach(task => { | |
const statusElement = document.createElement('div'); | |
statusElement.className = 'bg-white border border-gray-200 rounded-lg p-4 shadow-sm card-hover transition-all'; | |
statusElement.innerHTML = ` | |
<div class="flex justify-between items-start mb-2"> | |
<h3 class="font-semibold text-gray-800">${task.title}</h3> | |
<span class="branch-name text-xs font-mono bg-gray-100 px-2 py-1 rounded cursor-pointer hover:bg-gray-200" data-id="${task.id}">${task.branch}</span> | |
</div> | |
<div class="grid grid-cols-2 gap-4"> | |
<div class="p-2 rounded text-center cursor-pointer status-dev" data-id="${task.id}"> | |
<div class="text-xs font-medium text-gray-600 mb-1">DEV</div> | |
<span class="px-3 py-1 rounded-full text-xs font-medium status-${task.devStatus}"> | |
${task.devStatus === 'pending' ? 'Pending' : 'Merged'} | |
</span> | |
</div> | |
<div class="p-2 rounded text-center cursor-pointer status-master" data-id="${task.id}"> | |
<div class="text-xs font-medium text-gray-600 mb-1">MASTER</div> | |
<span class="px-3 py-1 rounded-full text-xs font-medium status-${task.masterStatus}"> | |
${task.masterStatus === 'pending' ? 'Pending' : 'Merged'} | |
</span> | |
</div> | |
</div> | |
${task.services.length > 0 ? ` | |
<div class="mt-3 pt-2 border-t border-gray-100"> | |
<div class="text-xs font-medium text-gray-600 mb-1">Services:</div> | |
<div class="flex flex-wrap gap-1"> | |
${task.services.map(service => ` | |
<span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">${service}</span> | |
`).join('')} | |
</div> | |
</div> | |
` : ''} | |
`; | |
mergeStatusContainer.appendChild(statusElement); | |
}); | |
// Add click handlers for status toggling | |
document.querySelectorAll('.status-dev').forEach(el => { | |
el.addEventListener('click', function() { | |
const taskId = parseInt(this.getAttribute('data-id')); | |
const task = tasks.find(t => t.id === taskId); | |
if (task) { | |
task.devStatus = task.devStatus === 'pending' ? 'merged' : 'pending'; | |
saveTasks(); | |
renderTasks(); | |
renderMergeStatus(); | |
} | |
}); | |
}); | |
document.querySelectorAll('.status-master').forEach(el => { | |
el.addEventListener('click', function() { | |
const taskId = parseInt(this.getAttribute('data-id')); | |
const task = tasks.find(t => t.id === taskId); | |
if (task) { | |
task.masterStatus = task.masterStatus === 'pending' ? 'merged' : 'pending'; | |
saveTasks(); | |
renderTasks(); | |
renderMergeStatus(); | |
} | |
}); | |
}); | |
} | |
// Initial render | |
renderTasks(); | |
renderMergeStatus(); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=salmanarshad/tasksandbugs" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |