job-tracker / index.html
namgyu-youn's picture
I want to add history (calendar) form viewer. There should be only viewer option, not edit or delete option. - Initial Deployment
81224c0 verified
<!DOCTYPE html>
<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>