Spaces:
Running
Running
I want to add history (calendar) form viewer. There should be only viewer option, not edit or delete option. - Initial Deployment
81224c0
verified
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Job Interview Tracker | Notion Style</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> | |
| .sidebar { | |
| transition: all 0.3s ease; | |
| } | |
| .card-hover:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | |
| } | |
| .status-1st { | |
| background-color: #ecfdf5; | |
| color: #065f46; | |
| } | |
| .status-2nd { | |
| background-color: #eff6ff; | |
| color: #1e40af; | |
| } | |
| .status-offer { | |
| background-color: #fef2f2; | |
| color: #991b1b; | |
| } | |
| .status-rejected, .status-rejected-1st, .status-rejected-2nd, .status-rejected-culture, .status-rejected-exp { | |
| background-color: #f3f4f6; | |
| color: #6b7280; | |
| } | |
| .modal { | |
| transition: opacity 0.3s ease; | |
| } | |
| .tag { | |
| transition: all 0.2s ease; | |
| } | |
| .tag:hover { | |
| transform: scale(1.05); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 font-sans"> | |
| <div class="flex h-screen overflow-hidden"> | |
| <!-- Sidebar --> | |
| <div class="sidebar bg-white w-64 border-r border-gray-200 flex flex-col"> | |
| <div class="p-4 border-b border-gray-200"> | |
| <h1 class="text-xl font-bold text-gray-800">Job Tracker</h1> | |
| <p class="text-sm text-gray-500">Track your interview progress</p> | |
| </div> | |
| <div class="flex-1 overflow-y-auto p-4"> | |
| <div class="mb-6 space-y-2"> | |
| <button id="newEntryBtn" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md flex items-center justify-center"> | |
| <i class="fas fa-plus mr-2"></i> New Entry | |
| </button> | |
| <button id="calendarViewBtn" class="w-full bg-gray-100 hover:bg-gray-200 text-gray-800 py-2 px-4 rounded-md flex items-center justify-center"> | |
| <i class="fas fa-calendar-alt mr-2"></i> Calendar View | |
| </button> | |
| </div> | |
| <div> | |
| <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Filters</h3> | |
| <div class="space-y-1"> | |
| <button class="filter-btn w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 flex items-center" data-filter="all"> | |
| <i class="fas fa-layer-group mr-2 text-gray-500"></i> All Applications | |
| </button> | |
| <button class="filter-btn w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 flex items-center" data-filter="1st"> | |
| <i class="fas fa-flag mr-2 text-green-500"></i> 1st Round | |
| </button> | |
| <button class="filter-btn w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 flex items-center" data-filter="2nd"> | |
| <i class="fas fa-flag mr-2 text-blue-500"></i> 2nd Round | |
| </button> | |
| <button class="filter-btn w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 flex items-center" data-filter="offer"> | |
| <i class="fas fa-trophy mr-2 text-red-500"></i> Offers | |
| </button> | |
| <button class="filter-btn w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 flex items-center" data-filter="rejected"> | |
| <i class="fas fa-times mr-2 text-gray-500"></i> All Rejected | |
| </button> | |
| <button class="filter-btn w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 flex items-center" data-filter="rejected-1st"> | |
| <i class="fas fa-times mr-2 text-gray-400"></i> Rejected (1st) | |
| </button> | |
| <button class="filter-btn w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 flex items-center" data-filter="rejected-2nd"> | |
| <i class="fas fa-times mr-2 text-gray-400"></i> Rejected (2nd) | |
| </button> | |
| <button class="filter-btn w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 flex items-center" data-filter="rejected-culture"> | |
| <i class="fas fa-times mr-2 text-gray-400"></i> Rejected (Culture) | |
| </button> | |
| <button class="filter-btn w-full text-left px-3 py-2 rounded-md hover:bg-gray-100 flex items-center" data-filter="rejected-exp"> | |
| <i class="fas fa-times mr-2 text-gray-400"></i> Rejected (Exp) | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mt-6"> | |
| <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Stats</h3> | |
| <div class="bg-gray-50 rounded-lg p-3"> | |
| <div class="flex justify-between text-sm mb-1"> | |
| <span class="text-gray-600">Total Applications:</span> | |
| <span class="font-medium" id="totalCount">0</span> | |
| </div> | |
| <div class="flex justify-between text-sm mb-1"> | |
| <span class="text-gray-600">Active:</span> | |
| <span class="font-medium" id="activeCount">0</span> | |
| </div> | |
| <div class="flex justify-between text-sm"> | |
| <span class="text-gray-600">Offer Rate:</span> | |
| <span class="font-medium" id="offerRate">0%</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-4 border-t border-gray-200"> | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center"> | |
| <i class="fas fa-user text-blue-500"></i> | |
| </div> | |
| <div class="ml-2"> | |
| <p class="text-sm font-medium">User Name</p> | |
| <p class="text-xs text-gray-500">Free Plan</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="flex-1 flex flex-col overflow-hidden"> | |
| <!-- Header --> | |
| <div class="bg-white border-b border-gray-200 p-4"> | |
| <div class="flex items-center justify-between"> | |
| <div> | |
| <h2 class="text-lg font-semibold text-gray-800" id="currentFilter">All Applications</h2> | |
| <p class="text-sm text-gray-500" id="filterCount">0 applications</p> | |
| </div> | |
| <div class="relative"> | |
| <input type="text" placeholder="Search applications..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Content --> | |
| <div class="flex-1 overflow-y-auto p-6"> | |
| <div id="calendarView" class="hidden"> | |
| <div class="bg-white rounded-lg shadow-sm p-6"> | |
| <h3 class="text-lg font-semibold mb-4">Application Timeline</h3> | |
| <div id="timelineContainer" class="space-y-4"> | |
| <!-- Timeline items will be added here --> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="applicationsContainer"> | |
| <!-- Application cards will be added here dynamically --> | |
| <div class="text-center py-10 text-gray-400" id="emptyState"> | |
| <i class="fas fa-briefcase text-4xl mb-3"></i> | |
| <h3 class="text-lg font-medium">No applications yet</h3> | |
| <p class="text-sm">Add your first job application to get started</p> | |
| <button id="emptyStateBtn" class="mt-4 bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md"> | |
| Add Application | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Modal --> | |
| <div id="applicationModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 modal opacity-0 pointer-events-none"> | |
| <div class="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto"> | |
| <div class="p-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-lg font-semibold" id="modalTitle">Add New Application</h3> | |
| <button id="closeModal" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <form id="applicationForm"> | |
| <input type="hidden" id="applicationId"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label for="companyName" class="block text-sm font-medium text-gray-700 mb-1">Company Name</label> | |
| <input type="text" id="companyName" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" required> | |
| </div> | |
| <div> | |
| <label for="jobTitle" class="block text-sm font-medium text-gray-700 mb-1">Job Title</label> | |
| <input type="text" id="jobTitle" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" required> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label for="salary" class="block text-sm font-medium text-gray-700 mb-1">Salary (USD)</label> | |
| <input type="number" id="salary" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| </div> | |
| <div> | |
| <label for="status" class="block text-sm font-medium text-gray-700 mb-1">Status</label> | |
| <select id="status" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" required> | |
| <option value="1st">1st Round</option> | |
| <option value="2nd">2nd Round</option> | |
| <option value="offer">Offer Received</option> | |
| <option value="rejected">Rejected (General)</option> | |
| <option value="rejected-1st">Rejected (1st Round)</option> | |
| <option value="rejected-2nd">Rejected (2nd Round)</option> | |
| <option value="rejected-culture">Rejected (Culture Fit)</option> | |
| <option value="rejected-exp">Rejected (Experience)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <label for="jobDescription" class="block text-sm font-medium text-gray-700 mb-1">Job Description</label> | |
| <textarea id="jobDescription" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea> | |
| </div> | |
| <div class="mb-4"> | |
| <label for="interviewNotes" class="block text-sm font-medium text-gray-700 mb-1">Interview Notes</label> | |
| <textarea id="interviewNotes" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Application History</label> | |
| <div id="historyContainer" class="space-y-2"> | |
| <!-- History items will be added here --> | |
| </div> | |
| <div class="mt-2 flex"> | |
| <input type="text" id="newHistoryNote" placeholder="Add new history note" class="flex-1 px-3 py-2 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| <button type="button" id="addHistoryBtn" class="px-3 py-2 bg-blue-600 text-white rounded-r-md hover:bg-blue-700"> | |
| <i class="fas fa-plus"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <label for="applicationDate" class="block text-sm font-medium text-gray-700 mb-1">Application Date</label> | |
| <input type="date" id="applicationDate" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" required> | |
| </div> | |
| <div> | |
| <label for="experienceLevel" class="block text-sm font-medium text-gray-700 mb-1">Experience Level</label> | |
| <select id="experienceLevel" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
| <option value="entry">Entry Level (<2 YoE)</option> | |
| <option value="mid">Mid Level (2-5 YoE)</option> | |
| <option value="senior">Senior Level (5+ YoE)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="flex justify-end space-x-3"> | |
| <button type="button" id="cancelBtn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">Cancel</button> | |
| <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Save Application</button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const applicationsContainer = document.getElementById('applicationsContainer'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const newEntryBtn = document.getElementById('newEntryBtn'); | |
| const emptyStateBtn = document.getElementById('emptyStateBtn'); | |
| const applicationModal = document.getElementById('applicationModal'); | |
| const closeModal = document.getElementById('closeModal'); | |
| const cancelBtn = document.getElementById('cancelBtn'); | |
| const applicationForm = document.getElementById('applicationForm'); | |
| const filterBtns = document.querySelectorAll('.filter-btn'); | |
| const currentFilter = document.getElementById('currentFilter'); | |
| const filterCount = document.getElementById('filterCount'); | |
| const totalCount = document.getElementById('totalCount'); | |
| const activeCount = document.getElementById('activeCount'); | |
| const offerRate = document.getElementById('offerRate'); | |
| // State | |
| let applications = JSON.parse(localStorage.getItem('jobApplications')) || []; | |
| let currentFilterValue = 'all'; | |
| let editingId = null; | |
| // Initialize | |
| updateStats(); | |
| renderApplications(); | |
| // Event Listeners | |
| document.getElementById('addHistoryBtn').addEventListener('click', addHistoryNote); | |
| document.getElementById('calendarViewBtn').addEventListener('click', toggleCalendarView); | |
| newEntryBtn.addEventListener('click', openModal); | |
| emptyStateBtn.addEventListener('click', openModal); | |
| closeModal.addEventListener('click', closeModalFunc); | |
| cancelBtn.addEventListener('click', closeModalFunc); | |
| applicationForm.addEventListener('submit', handleFormSubmit); | |
| filterBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| currentFilterValue = btn.dataset.filter; | |
| filterBtns.forEach(b => b.classList.remove('bg-gray-100')); | |
| btn.classList.add('bg-gray-100'); | |
| // Update UI | |
| const filterText = { | |
| 'all': 'All Applications', | |
| '1st': '1st Round Interviews', | |
| '2nd': '2nd Round Interviews', | |
| 'offer': 'Offers Received', | |
| 'rejected': 'Rejected Applications' | |
| }[currentFilterValue]; | |
| currentFilter.textContent = filterText; | |
| renderApplications(); | |
| }); | |
| }); | |
| // Functions | |
| function openModal() { | |
| applicationModal.classList.remove('opacity-0', 'pointer-events-none'); | |
| document.getElementById('modalTitle').textContent = 'Add New Application'; | |
| applicationForm.reset(); | |
| editingId = null; | |
| document.getElementById('applicationId').value = ''; | |
| document.getElementById('historyContainer').innerHTML = ''; | |
| } | |
| function closeModalFunc() { | |
| applicationModal.classList.add('opacity-0', 'pointer-events-none'); | |
| } | |
| function handleFormSubmit(e) { | |
| e.preventDefault(); | |
| const app = editingId ? applications.find(a => a.id === editingId) : null; | |
| const formData = { | |
| id: editingId || Date.now().toString(), | |
| companyName: document.getElementById('companyName').value, | |
| history: app?.history || [], | |
| jobTitle: document.getElementById('jobTitle').value, | |
| salary: document.getElementById('salary').value, | |
| status: document.getElementById('status').value, | |
| jobDescription: document.getElementById('jobDescription').value, | |
| interviewNotes: document.getElementById('interviewNotes').value, | |
| applicationDate: document.getElementById('applicationDate').value, | |
| createdAt: editingId ? applications.find(app => app.id === editingId).createdAt : new Date().toISOString(), | |
| updatedAt: new Date().toISOString() | |
| }; | |
| if (editingId) { | |
| // Update existing application | |
| applications = applications.map(app => | |
| app.id === editingId ? formData : app | |
| ); | |
| } else { | |
| // Add new application | |
| applications.push(formData); | |
| } | |
| saveApplications(); | |
| closeModalFunc(); | |
| renderApplications(); | |
| } | |
| function saveApplications() { | |
| localStorage.setItem('jobApplications', JSON.stringify(applications)); | |
| updateStats(); | |
| } | |
| function updateStats() { | |
| totalCount.textContent = applications.length; | |
| const activeApps = applications.filter(app => | |
| app.status === '1st' || app.status === '2nd' | |
| ).length; | |
| activeCount.textContent = activeApps; | |
| const offers = applications.filter(app => app.status === 'offer').length; | |
| const rate = applications.length > 0 | |
| ? Math.round((offers / applications.length) * 100) | |
| : 0; | |
| offerRate.textContent = `${rate}%`; | |
| } | |
| function renderApplications() { | |
| const filteredApps = applications.filter(app => { | |
| if (currentFilterValue === 'all') return true; | |
| return app.status === currentFilterValue; | |
| }); | |
| filterCount.textContent = `${filteredApps.length} ${filteredApps.length === 1 ? 'application' : 'applications'}`; | |
| if (filteredApps.length === 0) { | |
| emptyState.classList.remove('hidden'); | |
| applicationsContainer.innerHTML = ''; | |
| applicationsContainer.appendChild(emptyState); | |
| return; | |
| } | |
| emptyState.classList.add('hidden'); | |
| applicationsContainer.innerHTML = filteredApps.map(app => ` | |
| <div class="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm card-hover transition-all duration-200"> | |
| <div class="p-5"> | |
| <div class="flex justify-between items-start mb-2"> | |
| <h3 class="text-lg font-semibold text-gray-800 truncate">${app.companyName}</h3> | |
| <span class="status-${app.status} text-xs font-medium px-2.5 py-0.5 rounded-full"> | |
| ${getStatusText(app.status)} | |
| </span> | |
| </div> | |
| <p class="text-gray-600 font-medium mb-1">${app.jobTitle}</p> | |
| ${app.salary ? `<p class="text-gray-700 mb-3">$${numberWithCommas(app.salary)}</p>` : ''} | |
| <div class="flex justify-between text-sm text-gray-500 mb-4"> | |
| <span>Applied: ${formatDate(app.applicationDate)}</span> | |
| <span>Last updated: ${formatDate(app.updatedAt)}</span> | |
| </div> | |
| ${app.jobDescription ? ` | |
| <div class="mb-3"> | |
| <p class="text-sm text-gray-500 line-clamp-2">${app.jobDescription}</p> | |
| </div> | |
| ` : ''} | |
| <div class="flex justify-between pt-3 border-t border-gray-100"> | |
| <button class="edit-btn px-3 py-1 text-sm text-blue-600 hover:text-blue-800" data-id="${app.id}"> | |
| <i class="fas fa-edit mr-1"></i> Edit | |
| </button> | |
| <button class="delete-btn px-3 py-1 text-sm text-red-600 hover:text-red-800" data-id="${app.id}"> | |
| <i class="fas fa-trash-alt mr-1"></i> Delete | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| // Add event listeners to edit and delete buttons | |
| document.querySelectorAll('.edit-btn').forEach(btn => { | |
| btn.addEventListener('click', () => editApplication(btn.dataset.id)); | |
| }); | |
| document.querySelectorAll('.delete-btn').forEach(btn => { | |
| btn.addEventListener('click', () => deleteApplication(btn.dataset.id)); | |
| }); | |
| } | |
| function renderHistory(history) { | |
| const container = document.getElementById('historyContainer'); | |
| container.innerHTML = (history || []).map(item => ` | |
| <div class="flex items-start"> | |
| <div class="flex-shrink-0 mt-1 mr-2"> | |
| <div class="w-2 h-2 rounded-full bg-blue-500"></div> | |
| </div> | |
| <div class="flex-1 bg-gray-50 p-2 rounded"> | |
| <p class="text-sm">${item.note}</p> | |
| <p class="text-xs text-gray-500 mt-1">${formatDate(item.date)}</p> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| function addHistoryNote() { | |
| const noteInput = document.getElementById('newHistoryNote'); | |
| const note = noteInput.value.trim(); | |
| if (!note) return; | |
| const newHistoryItem = { | |
| note, | |
| date: new Date().toISOString() | |
| }; | |
| if (editingId) { | |
| const app = applications.find(a => a.id === editingId); | |
| if (!app.history) app.history = []; | |
| app.history.push(newHistoryItem); | |
| renderHistory(app.history); | |
| noteInput.value = ''; | |
| } | |
| } | |
| function editApplication(id) { | |
| const app = applications.find(a => a.id === id); | |
| if (!app) return; | |
| editingId = id; | |
| document.getElementById('applicationId').value = id; | |
| document.getElementById('companyName').value = app.companyName; | |
| document.getElementById('jobTitle').value = app.jobTitle; | |
| document.getElementById('salary').value = app.salary; | |
| document.getElementById('status').value = app.status; | |
| document.getElementById('jobDescription').value = app.jobDescription; | |
| document.getElementById('interviewNotes').value = app.interviewNotes; | |
| document.getElementById('applicationDate').value = app.applicationDate; | |
| document.getElementById('modalTitle').textContent = 'Edit Application'; | |
| renderHistory(app.history); | |
| applicationModal.classList.remove('opacity-0', 'pointer-events-none'); | |
| } | |
| function deleteApplication(id) { | |
| if (confirm('Are you sure you want to delete this application?')) { | |
| applications = applications.filter(app => app.id !== id); | |
| saveApplications(); | |
| renderApplications(); | |
| } | |
| } | |
| function getStatusText(status) { | |
| const statusMap = { | |
| '1st': '1st Round', | |
| '2nd': '2nd Round', | |
| 'offer': 'Offer Received', | |
| 'rejected': 'Rejected (General)', | |
| 'rejected-1st': 'Rejected (1st Round)', | |
| 'rejected-2nd': 'Rejected (2nd Round)', | |
| 'rejected-culture': 'Rejected (Culture Fit)', | |
| 'rejected-exp': 'Rejected (Experience)' | |
| }; | |
| return statusMap[status] || status; | |
| } | |
| function formatDate(dateString) { | |
| const date = new Date(dateString); | |
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); | |
| } | |
| function numberWithCommas(x) { | |
| return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); | |
| } | |
| function toggleCalendarView() { | |
| const calendarView = document.getElementById('calendarView'); | |
| const appsContainer = document.getElementById('applicationsContainer'); | |
| if (calendarView.classList.contains('hidden')) { | |
| calendarView.classList.remove('hidden'); | |
| appsContainer.classList.add('hidden'); | |
| renderTimeline(); | |
| } else { | |
| calendarView.classList.add('hidden'); | |
| appsContainer.classList.remove('hidden'); | |
| } | |
| } | |
| function renderTimeline() { | |
| const timelineContainer = document.getElementById('timelineContainer'); | |
| const allEvents = []; | |
| // Collect all application and history events | |
| applications.forEach(app => { | |
| allEvents.push({ | |
| type: 'application', | |
| date: app.applicationDate, | |
| title: `Applied to ${app.companyName}`, | |
| description: `Position: ${app.jobTitle}`, | |
| status: app.status | |
| }); | |
| if (app.history && app.history.length) { | |
| app.history.forEach(history => { | |
| allEvents.push({ | |
| type: 'history', | |
| date: history.date, | |
| title: `${app.companyName} update`, | |
| description: history.note, | |
| status: app.status | |
| }); | |
| }); | |
| } | |
| }); | |
| // Sort events by date (newest first) | |
| allEvents.sort((a, b) => new Date(b.date) - new Date(a.date)); | |
| // Group events by month | |
| const groupedEvents = {}; | |
| allEvents.forEach(event => { | |
| const date = new Date(event.date); | |
| const monthYear = date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); | |
| if (!groupedEvents[monthYear]) { | |
| groupedEvents[monthYear] = []; | |
| } | |
| groupedEvents[monthYear].push(event); | |
| }); | |
| // Render timeline | |
| timelineContainer.innerHTML = ''; | |
| for (const [monthYear, events] of Object.entries(groupedEvents)) { | |
| const monthHeader = document.createElement('div'); | |
| monthHeader.className = 'font-semibold text-gray-700 mb-2'; | |
| monthHeader.textContent = monthYear; | |
| timelineContainer.appendChild(monthHeader); | |
| events.forEach(event => { | |
| const eventEl = document.createElement('div'); | |
| eventEl.className = 'pl-4 border-l-2 border-gray-200 pb-4 relative'; | |
| const statusColor = { | |
| '1st': 'bg-green-100 text-green-800', | |
| '2nd': 'bg-blue-100 text-blue-800', | |
| 'offer': 'bg-red-100 text-red-800', | |
| 'rejected': 'bg-gray-100 text-gray-800', | |
| 'rejected-1st': 'bg-gray-100 text-gray-800', | |
| 'rejected-2nd': 'bg-gray-100 text-gray-800', | |
| 'rejected-culture': 'bg-gray-100 text-gray-800', | |
| 'rejected-exp': 'bg-gray-100 text-gray-800' | |
| }[event.status] || 'bg-gray-100 text-gray-800'; | |
| eventEl.innerHTML = ` | |
| <div class="absolute -left-1.5 mt-1.5 w-3 h-3 rounded-full border-2 border-white ${statusColor}"></div> | |
| <div class="bg-white p-3 rounded-lg shadow-xs"> | |
| <h4 class="font-medium">${event.title}</h4> | |
| <p class="text-sm text-gray-600 mt-1">${event.description}</p> | |
| <p class="text-xs text-gray-500 mt-2">${formatDate(event.date)}</p> | |
| </div> | |
| `; | |
| timelineContainer.appendChild(eventEl); | |
| }); | |
| } | |
| } | |
| }); | |
| </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=namgyu-youn/job-tracker" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |