|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Advanced Timetable Creator</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"> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> |
|
<style> |
|
.time-slot { |
|
min-height: 80px; |
|
} |
|
.draggable { |
|
cursor: move; |
|
user-select: none; |
|
} |
|
.dropzone { |
|
transition: background-color 0.3s; |
|
} |
|
.dropzone.highlight { |
|
background-color: rgba(147, 197, 253, 0.3); |
|
} |
|
.subject-card { |
|
transition: transform 0.2s, box-shadow 0.2s; |
|
} |
|
.subject-card:hover { |
|
transform: translateY(-2px); |
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); |
|
} |
|
@media print { |
|
.no-print { |
|
display: none !important; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50"> |
|
<div class="container mx-auto px-4 py-8"> |
|
<header class="mb-8 text-center"> |
|
<h1 class="text-4xl font-bold text-indigo-700 mb-2">Timetable Creator</h1> |
|
<p class="text-gray-600">Create, customize and export your perfect timetable</p> |
|
</header> |
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6"> |
|
|
|
<div class="lg:col-span-1 bg-white p-6 rounded-lg shadow-md no-print"> |
|
<h2 class="text-xl font-semibold mb-4 text-indigo-600 border-b pb-2">Subjects</h2> |
|
|
|
<div class="mb-6"> |
|
<div class="flex mb-4"> |
|
<input type="text" id="subjectName" placeholder="Subject name" class="flex-1 px-3 py-2 border rounded-l focus:outline-none focus:ring-2 focus:ring-indigo-400"> |
|
<input type="color" id="subjectColor" value="#3B82F6" class="w-10 h-10 border"> |
|
<button onclick="addSubject()" class="bg-indigo-600 text-white px-4 py-2 rounded-r hover:bg-indigo-700 transition">Add</button> |
|
</div> |
|
|
|
<div id="subjectList" class="space-y-2 max-h-64 overflow-y-auto p-2 border rounded"> |
|
|
|
</div> |
|
</div> |
|
|
|
<div class="mb-6"> |
|
<h3 class="font-medium mb-2 text-gray-700">Timetable Options</h3> |
|
<div class="grid grid-cols-2 gap-2"> |
|
<div> |
|
<label class="block text-sm text-gray-600 mb-1">Days</label> |
|
<select id="dayCount" class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-indigo-400"> |
|
<option value="5">5 (Mon-Fri)</option> |
|
<option value="6">6 (Mon-Sat)</option> |
|
<option value="7">7 (Mon-Sun)</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label class="block text-sm text-gray-600 mb-1">Periods</label> |
|
<select id="periodCount" class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-indigo-400"> |
|
<option value="6">6</option> |
|
<option value="7">7</option> |
|
<option value="8">8</option> |
|
<option value="9">9</option> |
|
<option value="10">10</option> |
|
</select> |
|
</div> |
|
</div> |
|
<button onclick="generateTimetable()" class="mt-3 w-full bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700 transition">Generate Timetable</button> |
|
</div> |
|
|
|
<div> |
|
<h3 class="font-medium mb-2 text-gray-700">Export Options</h3> |
|
<div class="grid grid-cols-3 gap-2"> |
|
<button onclick="exportToPDF()" class="bg-red-500 text-white px-3 py-2 rounded hover:bg-red-600 transition flex items-center justify-center"> |
|
<i class="fas fa-file-pdf mr-2"></i> PDF |
|
</button> |
|
<button onclick="exportToExcel()" class="bg-green-600 text-white px-3 py-2 rounded hover:bg-green-700 transition flex items-center justify-center"> |
|
<i class="fas fa-file-excel mr-2"></i> Excel |
|
</button> |
|
<button onclick="exportToImage()" class="bg-blue-500 text-white px-3 py-2 rounded hover:bg-blue-600 transition flex items-center justify-center"> |
|
<i class="fas fa-image mr-2"></i> Image |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="lg:col-span-3"> |
|
<div class="bg-white p-6 rounded-lg shadow-md"> |
|
<div class="flex justify-between items-center mb-4"> |
|
<h2 class="text-xl font-semibold text-indigo-600">Weekly Timetable</h2> |
|
<div class="flex space-x-2"> |
|
<input type="text" id="timetableTitle" placeholder="Timetable Title" class="px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-indigo-400" value="My Weekly Timetable"> |
|
<button onclick="printTimetable()" class="bg-gray-200 text-gray-700 px-3 py-2 rounded hover:bg-gray-300 transition no-print"> |
|
<i class="fas fa-print mr-1"></i> Print |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div id="timetableContainer" class="overflow-x-auto"> |
|
<table id="timetable" class="w-full border-collapse"> |
|
<thead> |
|
<tr> |
|
<th class="border p-2 bg-gray-100 font-medium">Time/Day</th> |
|
|
|
</tr> |
|
</thead> |
|
<tbody> |
|
|
|
</tbody> |
|
</table> |
|
</div> |
|
</div> |
|
|
|
<div class="mt-6 bg-white p-6 rounded-lg shadow-md no-print"> |
|
<h2 class="text-xl font-semibold text-indigo-600 mb-4 border-b pb-2">Timetable Summary</h2> |
|
<div id="summaryContainer" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
let subjects = []; |
|
|
|
|
|
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; |
|
|
|
|
|
function addSubject() { |
|
const nameInput = document.getElementById('subjectName'); |
|
const colorInput = document.getElementById('subjectColor'); |
|
|
|
const name = nameInput.value.trim(); |
|
const color = colorInput.value; |
|
|
|
if (name === '') { |
|
alert('Please enter a subject name'); |
|
return; |
|
} |
|
|
|
|
|
const subject = { |
|
id: Date.now().toString(), |
|
name: name, |
|
color: color, |
|
hours: 0 |
|
}; |
|
|
|
subjects.push(subject); |
|
|
|
|
|
renderSubjects(); |
|
|
|
|
|
nameInput.value = ''; |
|
nameInput.focus(); |
|
} |
|
|
|
|
|
function renderSubjects() { |
|
const subjectList = document.getElementById('subjectList'); |
|
subjectList.innerHTML = ''; |
|
|
|
if (subjects.length === 0) { |
|
subjectList.innerHTML = '<p class="text-gray-500 text-center py-4">No subjects added yet</p>'; |
|
return; |
|
} |
|
|
|
subjects.forEach(subject => { |
|
const subjectElement = document.createElement('div'); |
|
subjectElement.className = 'subject-card draggable flex items-center justify-between p-3 rounded border cursor-move'; |
|
subjectElement.style.backgroundColor = `${subject.color}20`; |
|
subjectElement.style.borderColor = subject.color; |
|
subjectElement.draggable = true; |
|
subjectElement.dataset.id = subject.id; |
|
|
|
subjectElement.innerHTML = ` |
|
<div class="flex items-center"> |
|
<div class="w-4 h-4 rounded-full mr-2" style="background-color: ${subject.color}"></div> |
|
<span class="font-medium">${subject.name}</span> |
|
</div> |
|
<button onclick="removeSubject('${subject.id}')" class="text-red-500 hover:text-red-700"> |
|
<i class="fas fa-times"></i> |
|
</button> |
|
`; |
|
|
|
|
|
subjectElement.addEventListener('dragstart', dragStart); |
|
|
|
subjectList.appendChild(subjectElement); |
|
}); |
|
} |
|
|
|
|
|
function removeSubject(id) { |
|
subjects = subjects.filter(subject => subject.id !== id); |
|
renderSubjects(); |
|
|
|
|
|
document.querySelectorAll(`[data-subject-id="${id}"]`).forEach(el => { |
|
el.remove(); |
|
}); |
|
|
|
updateSummary(); |
|
} |
|
|
|
|
|
function generateTimetable() { |
|
const dayCount = parseInt(document.getElementById('dayCount').value); |
|
const periodCount = parseInt(document.getElementById('periodCount').value); |
|
|
|
const timetable = document.getElementById('timetable'); |
|
|
|
|
|
timetable.innerHTML = ''; |
|
|
|
|
|
const headerRow = document.createElement('tr'); |
|
headerRow.innerHTML = '<th class="border p-2 bg-gray-100 font-medium">Time/Day</th>'; |
|
|
|
for (let i = 0; i < dayCount; i++) { |
|
headerRow.innerHTML += `<th class="border p-2 bg-gray-100 font-medium">${days[i]}</th>`; |
|
} |
|
|
|
timetable.appendChild(headerRow); |
|
|
|
|
|
const tbody = document.createElement('tbody'); |
|
|
|
for (let i = 0; i < periodCount; i++) { |
|
const row = document.createElement('tr'); |
|
|
|
|
|
const startHour = 8 + i; |
|
const endHour = startHour + 1; |
|
const timeCell = document.createElement('td'); |
|
timeCell.className = 'border p-2 bg-gray-50 text-center font-medium'; |
|
timeCell.textContent = `${startHour}:00 - ${endHour}:00`; |
|
row.appendChild(timeCell); |
|
|
|
|
|
for (let j = 0; j < dayCount; j++) { |
|
const cell = document.createElement('td'); |
|
cell.className = 'border p-1 time-slot dropzone'; |
|
cell.dataset.day = days[j]; |
|
cell.dataset.period = i + 1; |
|
|
|
|
|
cell.addEventListener('dragover', dragOver); |
|
cell.addEventListener('dragleave', dragLeave); |
|
cell.addEventListener('drop', drop); |
|
|
|
row.appendChild(cell); |
|
} |
|
|
|
tbody.appendChild(row); |
|
} |
|
|
|
timetable.appendChild(tbody); |
|
|
|
|
|
subjects.forEach(subject => { |
|
subject.hours = 0; |
|
}); |
|
|
|
updateSummary(); |
|
} |
|
|
|
|
|
function dragStart(e) { |
|
e.dataTransfer.setData('text/plain', e.target.dataset.id); |
|
e.dataTransfer.effectAllowed = 'move'; |
|
setTimeout(() => { |
|
e.target.classList.add('opacity-0'); |
|
}, 0); |
|
} |
|
|
|
function dragOver(e) { |
|
e.preventDefault(); |
|
e.dataTransfer.dropEffect = 'move'; |
|
e.target.classList.add('highlight'); |
|
} |
|
|
|
function dragLeave(e) { |
|
e.target.classList.remove('highlight'); |
|
} |
|
|
|
function drop(e) { |
|
e.preventDefault(); |
|
e.target.classList.remove('highlight'); |
|
|
|
const id = e.dataTransfer.getData('text/plain'); |
|
const subject = subjects.find(s => s.id === id); |
|
|
|
if (!subject) return; |
|
|
|
|
|
const existingSubject = e.target.querySelector('.subject-card'); |
|
if (existingSubject) { |
|
|
|
const existingId = existingSubject.dataset.subjectId; |
|
const existingSubjectObj = subjects.find(s => s.id === existingId); |
|
|
|
if (existingSubjectObj) { |
|
existingSubjectObj.hours--; |
|
} |
|
|
|
existingSubject.remove(); |
|
} |
|
|
|
|
|
const subjectElement = document.createElement('div'); |
|
subjectElement.className = 'subject-card p-2 rounded text-white text-center text-sm font-medium mb-1'; |
|
subjectElement.style.backgroundColor = subject.color; |
|
subjectElement.dataset.subjectId = subject.id; |
|
|
|
subjectElement.innerHTML = ` |
|
<div>${subject.name}</div> |
|
<button onclick="removeSubjectFromCell(this)" class="text-white hover:text-gray-200 mt-1"> |
|
<i class="fas fa-times text-xs"></i> |
|
</button> |
|
`; |
|
|
|
e.target.appendChild(subjectElement); |
|
|
|
|
|
subject.hours++; |
|
|
|
updateSummary(); |
|
|
|
|
|
document.querySelector(`[data-id="${id}"]`).classList.remove('opacity-0'); |
|
} |
|
|
|
|
|
function removeSubjectFromCell(button) { |
|
const card = button.closest('.subject-card'); |
|
const subjectId = card.dataset.subjectId; |
|
|
|
|
|
const subject = subjects.find(s => s.id === subjectId); |
|
if (subject) { |
|
subject.hours--; |
|
updateSummary(); |
|
} |
|
|
|
card.remove(); |
|
} |
|
|
|
|
|
function updateSummary() { |
|
const summaryContainer = document.getElementById('summaryContainer'); |
|
summaryContainer.innerHTML = ''; |
|
|
|
if (subjects.length === 0) { |
|
summaryContainer.innerHTML = '<p class="text-gray-500 text-center py-4 col-span-3">Add subjects to see summary</p>'; |
|
return; |
|
} |
|
|
|
|
|
const activeSubjects = subjects.filter(subject => subject.hours > 0); |
|
|
|
if (activeSubjects.length === 0) { |
|
summaryContainer.innerHTML = '<p class="text-gray-500 text-center py-4 col-span-3">Drag subjects to timetable to see summary</p>'; |
|
return; |
|
} |
|
|
|
|
|
const totalHours = activeSubjects.reduce((sum, subject) => sum + subject.hours, 0); |
|
|
|
|
|
const totalCard = document.createElement('div'); |
|
totalCard.className = 'bg-indigo-100 border border-indigo-200 rounded-lg p-4'; |
|
totalCard.innerHTML = ` |
|
<div class="flex items-center justify-between"> |
|
<h3 class="font-semibold text-indigo-800">Total Hours</h3> |
|
<div class="bg-indigo-600 text-white rounded-full w-8 h-8 flex items-center justify-center"> |
|
${totalHours} |
|
</div> |
|
</div> |
|
<div class="mt-2 text-sm text-indigo-600"> |
|
${totalHours} hours scheduled this week |
|
</div> |
|
`; |
|
summaryContainer.appendChild(totalCard); |
|
|
|
|
|
activeSubjects.forEach(subject => { |
|
const card = document.createElement('div'); |
|
card.className = 'bg-white border rounded-lg p-4 shadow-sm'; |
|
card.innerHTML = ` |
|
<div class="flex items-center justify-between"> |
|
<div class="flex items-center"> |
|
<div class="w-3 h-3 rounded-full mr-2" style="background-color: ${subject.color}"></div> |
|
<h3 class="font-semibold">${subject.name}</h3> |
|
</div> |
|
<div class="bg-gray-100 text-gray-800 rounded-full px-3 py-1 text-sm"> |
|
${subject.hours} ${subject.hours === 1 ? 'hour' : 'hours'} |
|
</div> |
|
</div> |
|
<div class="mt-2"> |
|
<div class="w-full bg-gray-200 rounded-full h-2"> |
|
<div class="bg-blue-600 h-2 rounded-full" style="width: ${(subject.hours / totalHours) * 100}%"></div> |
|
</div> |
|
</div> |
|
`; |
|
summaryContainer.appendChild(card); |
|
}); |
|
} |
|
|
|
|
|
function exportToPDF() { |
|
const { jsPDF } = window.jspdf; |
|
const doc = new jsPDF({ |
|
orientation: 'landscape', |
|
unit: 'mm' |
|
}); |
|
|
|
const title = document.getElementById('timetableTitle').value || 'My Timetable'; |
|
const timetable = document.getElementById('timetableContainer'); |
|
|
|
html2canvas(timetable).then(canvas => { |
|
const imgData = canvas.toDataURL('image/png'); |
|
const imgWidth = 280; |
|
const imgHeight = canvas.height * imgWidth / canvas.width; |
|
|
|
doc.setFontSize(20); |
|
doc.text(title, 10, 10); |
|
doc.addImage(imgData, 'PNG', 10, 15, imgWidth, imgHeight); |
|
doc.save(`${title.replace(/\s+/g, '_')}.pdf`); |
|
}); |
|
} |
|
|
|
function exportToExcel() { |
|
const title = document.getElementById('timetableTitle').value || 'My Timetable'; |
|
const workbook = XLSX.utils.book_new(); |
|
|
|
|
|
const timetable = document.getElementById('timetable'); |
|
const worksheet = XLSX.utils.table_to_sheet(timetable); |
|
|
|
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, 'Timetable'); |
|
|
|
|
|
XLSX.writeFile(workbook, `${title.replace(/\s+/g, '_')}.xlsx`); |
|
} |
|
|
|
function exportToImage() { |
|
const title = document.getElementById('timetableTitle').value || 'My Timetable'; |
|
const timetable = document.getElementById('timetableContainer'); |
|
|
|
html2canvas(timetable).then(canvas => { |
|
const link = document.createElement('a'); |
|
link.download = `${title.replace(/\s+/g, '_')}.png`; |
|
link.href = canvas.toDataURL('image/png'); |
|
link.click(); |
|
}); |
|
} |
|
|
|
|
|
function printTimetable() { |
|
window.print(); |
|
} |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
renderSubjects(); |
|
generateTimetable(); |
|
|
|
|
|
document.querySelectorAll('.draggable').forEach(el => { |
|
el.addEventListener('dragstart', dragStart); |
|
}); |
|
|
|
document.querySelectorAll('.dropzone').forEach(el => { |
|
el.addEventListener('dragover', dragOver); |
|
el.addEventListener('dragleave', dragLeave); |
|
el.addEventListener('drop', drop); |
|
}); |
|
}); |
|
</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=theharby/tt" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |