|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Job Application Tracker Pro</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> |
|
|
.progress-bar { |
|
|
transition: width 0.5s ease-in-out; |
|
|
} |
|
|
.chart-bar { |
|
|
transition: height 0.5s ease-in-out; |
|
|
} |
|
|
.date-picker { |
|
|
position: relative; |
|
|
} |
|
|
.date-picker input[type="date"] { |
|
|
appearance: none; |
|
|
-webkit-appearance: none; |
|
|
} |
|
|
.date-picker::after { |
|
|
content: '\f073'; |
|
|
font-family: 'Font Awesome 6 Free'; |
|
|
font-weight: 900; |
|
|
position: absolute; |
|
|
right: 12px; |
|
|
top: 50%; |
|
|
transform: translateY(-50%); |
|
|
pointer-events: none; |
|
|
color: #6b7280; |
|
|
} |
|
|
.chart-container { |
|
|
height: 180px; |
|
|
} |
|
|
.chart-bar { |
|
|
width: 100%; |
|
|
border-radius: 4px 4px 0 0; |
|
|
position: relative; |
|
|
} |
|
|
.chart-bar::after { |
|
|
content: attr(data-count); |
|
|
position: absolute; |
|
|
top: -22px; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
font-size: 12px; |
|
|
font-weight: bold; |
|
|
color: #4b5563; |
|
|
} |
|
|
.chart-goal-line { |
|
|
position: absolute; |
|
|
width: 100%; |
|
|
height: 2px; |
|
|
background-color: #6b7280; |
|
|
z-index: 10; |
|
|
} |
|
|
.btn-primary { |
|
|
background-color: #4f46e5; |
|
|
color: white; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.btn-primary:hover { |
|
|
background-color: #4338ca; |
|
|
transform: translateY(-1px); |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
} |
|
|
.btn-danger { |
|
|
background-color: #dc2626; |
|
|
color: white; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.btn-danger:hover { |
|
|
background-color: #b91c1c; |
|
|
transform: translateY(-1px); |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
} |
|
|
.btn-success { |
|
|
background-color: #059669; |
|
|
color: white; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.btn-success:hover { |
|
|
background-color: #047857; |
|
|
transform: translateY(-1px); |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
} |
|
|
.btn-secondary { |
|
|
background-color: #4b5563; |
|
|
color: white; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.btn-secondary:hover { |
|
|
background-color: #374151; |
|
|
transform: translateY(-1px); |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
} |
|
|
.count-btn { |
|
|
width: 44px; |
|
|
height: 44px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
border-radius: 50%; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
.count-btn:hover { |
|
|
transform: scale(1.05); |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
} |
|
|
.input-field { |
|
|
transition: all 0.2s; |
|
|
border: 1px solid #d1d5db; |
|
|
} |
|
|
.input-field:focus { |
|
|
border-color: #818cf8; |
|
|
box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.2); |
|
|
} |
|
|
.compact-section { |
|
|
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%); |
|
|
} |
|
|
.edit-input { |
|
|
width: 50px; |
|
|
padding: 2px 5px; |
|
|
border: 1px solid #d1d5db; |
|
|
border-radius: 4px; |
|
|
text-align: center; |
|
|
} |
|
|
.heatmap-day { |
|
|
width: 14px; |
|
|
height: 14px; |
|
|
border-radius: 2px; |
|
|
margin: 1px; |
|
|
position: relative; |
|
|
} |
|
|
.heatmap-day:hover::after { |
|
|
content: attr(data-tooltip); |
|
|
position: absolute; |
|
|
top: -30px; |
|
|
left: 50%; |
|
|
transform: translateX(-50%); |
|
|
background: #333; |
|
|
color: white; |
|
|
padding: 4px 8px; |
|
|
border-radius: 4px; |
|
|
font-size: 12px; |
|
|
white-space: nowrap; |
|
|
z-index: 10; |
|
|
} |
|
|
.timeline-item { |
|
|
position: relative; |
|
|
padding-left: 20px; |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
.timeline-item::before { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
left: 0; |
|
|
top: 5px; |
|
|
width: 10px; |
|
|
height: 10px; |
|
|
border-radius: 50%; |
|
|
background: #4f46e5; |
|
|
} |
|
|
.timeline-item::after { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
left: 4px; |
|
|
top: 15px; |
|
|
bottom: -15px; |
|
|
width: 2px; |
|
|
background: #e5e7eb; |
|
|
} |
|
|
.timeline-item:last-child::after { |
|
|
display: none; |
|
|
} |
|
|
.status-badge { |
|
|
font-size: 10px; |
|
|
padding: 2px 6px; |
|
|
border-radius: 10px; |
|
|
font-weight: 600; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 min-h-screen"> |
|
|
<div class="container mx-auto px-4 py-6 max-w-6xl"> |
|
|
|
|
|
<header class="mb-6"> |
|
|
<h1 class="text-3xl font-bold text-indigo-700 mb-1">Job Application Tracker Pro</h1> |
|
|
<p class="text-sm text-gray-600">Track and visualize your job search progress</p> |
|
|
</header> |
|
|
|
|
|
|
|
|
<div class="compact-section rounded-lg shadow-sm p-4 mb-4 border border-gray-200"> |
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4"> |
|
|
<div> |
|
|
<label for="username" class="block text-xs font-medium text-gray-700 mb-1">Username</label> |
|
|
<div class="flex"> |
|
|
<input type="text" id="username" placeholder="Your name" |
|
|
class="flex-1 px-3 py-2 text-sm input-field rounded-l-md focus:outline-none focus:ring-1 focus:ring-indigo-500"> |
|
|
<button id="saveUserBtn" class="btn-primary px-3 py-2 text-sm rounded-r-md"> |
|
|
<i class="fas fa-check"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<label for="dailyGoal" class="block text-xs font-medium text-gray-700 mb-1">Daily Goal</label> |
|
|
<div class="flex"> |
|
|
<input type="number" id="dailyGoal" min="1" value="30" |
|
|
class="flex-1 px-3 py-2 text-sm input-field rounded-l-md focus:outline-none focus:ring-1 focus:ring-indigo-500"> |
|
|
<button id="saveGoalBtn" class="btn-primary px-3 py-2 text-sm rounded-r-md"> |
|
|
<i class="fas fa-bullseye"></i> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<label for="selectedDate" class="block text-xs font-medium text-gray-700 mb-1">Select Date</label> |
|
|
<div class="date-picker relative"> |
|
|
<input type="date" id="selectedDate" class="w-full px-3 py-2 text-sm input-field rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500"> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-xs font-medium text-gray-700 mb-1">Quick Actions</label> |
|
|
<div class="flex gap-2"> |
|
|
<button id="todayBtn" class="btn-secondary px-3 py-2 text-xs rounded-md"> |
|
|
<i class="fas fa-calendar-day mr-1"></i> Today |
|
|
</button> |
|
|
<button id="yesterdayBtn" class="btn-secondary px-3 py-2 text-xs rounded-md"> |
|
|
<i class="fas fa-arrow-left mr-1"></i> Yesterday |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div id="currentUser" class="mt-3 text-xs text-gray-600 hidden flex items-center justify-between"> |
|
|
<div> |
|
|
<span class="font-medium">User:</span> <span id="userDisplay"></span> | |
|
|
<span class="font-medium">Goal:</span> <span id="goalDisplay"></span> | |
|
|
<span class="font-medium">Streak:</span> <span id="streakDisplay">0 days</span> |
|
|
</div> |
|
|
<div id="dateDisplay" class="text-gray-700"></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-5 mb-6 border border-gray-100"> |
|
|
<h2 class="text-lg font-semibold mb-3 text-gray-800" id="dailyProgressTitle">Today's Progress</h2> |
|
|
<div class="mb-4"> |
|
|
<div class="flex justify-between mb-1"> |
|
|
<span class="text-xs font-medium text-gray-700">Applications</span> |
|
|
<span id="progressText" class="text-xs font-medium text-gray-700">0/30</span> |
|
|
</div> |
|
|
<div class="w-full bg-gray-200 rounded-full h-2"> |
|
|
<div id="progressBar" class="progress-bar bg-indigo-600 h-2 rounded-full" style="width: 0%"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="flex items-center justify-center gap-4 mb-4"> |
|
|
<button id="decreaseBtn" class="count-btn bg-red-500 text-white disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
|
|
<i class="fas fa-minus"></i> |
|
|
</button> |
|
|
<div class="text-3xl font-bold w-16 text-center text-gray-800" id="countDisplay">0</div> |
|
|
<button id="increaseBtn" class="count-btn bg-green-500 text-white"> |
|
|
<i class="fas fa-plus"></i> |
|
|
</button> |
|
|
</div> |
|
|
<div class="flex gap-3"> |
|
|
<button id="saveBtn" class="btn-primary flex-1 py-2 px-4 text-sm rounded-md"> |
|
|
<i class="fas fa-save mr-1"></i> Save |
|
|
</button> |
|
|
<button id="deleteBtn" class="btn-danger flex-1 py-2 px-4 text-sm rounded-md hidden"> |
|
|
<i class="fas fa-trash mr-1"></i> Delete |
|
|
</button> |
|
|
<button id="addNoteBtn" class="btn-secondary flex-1 py-2 px-4 text-sm rounded-md"> |
|
|
<i class="fas fa-sticky-note mr-1"></i> Add Note |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6"> |
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-5 border border-gray-100"> |
|
|
<h2 class="text-lg font-semibold mb-3 text-gray-800">Weekly Summary</h2> |
|
|
<div class="chart-container flex justify-between items-end mb-5 border-b border-gray-200 pb-4"> |
|
|
|
|
|
</div> |
|
|
<div class="grid grid-cols-3 gap-3"> |
|
|
<div class="bg-indigo-50 p-3 rounded-lg border border-indigo-100"> |
|
|
<div class="text-xs font-medium text-indigo-700">Today</div> |
|
|
<div class="text-xl font-bold text-indigo-900" id="todayCount">0</div> |
|
|
</div> |
|
|
<div class="bg-purple-50 p-3 rounded-lg border border-purple-100"> |
|
|
<div class="text-xs font-medium text-purple-700">This Week</div> |
|
|
<div class="text-xl font-bold text-purple-900" id="weeklyCount">0</div> |
|
|
</div> |
|
|
<div class="bg-green-50 p-3 rounded-lg border border-green-100"> |
|
|
<div class="text-xs font-medium text-green-700">Total</div> |
|
|
<div class="text-xl font-bold text-green-900" id="totalCount">0</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-5 border border-gray-100"> |
|
|
<h2 class="text-lg font-semibold mb-3 text-gray-800">Activity Heatmap</h2> |
|
|
<div class="flex justify-between mb-2"> |
|
|
<span class="text-xs text-gray-500">Last 3 Months</span> |
|
|
<div class="flex items-center"> |
|
|
<span class="text-xs text-gray-500 mr-2">Less</span> |
|
|
<div class="flex"> |
|
|
<div class="heatmap-day bg-gray-100"></div> |
|
|
<div class="heatmap-day bg-green-100"></div> |
|
|
<div class="heatmap-day bg-green-300"></div> |
|
|
<div class="heatmap-day bg-green-500"></div> |
|
|
<div class="heatmap-day bg-green-700"></div> |
|
|
</div> |
|
|
<span class="text-xs text-gray-500 ml-2">More</span> |
|
|
</div> |
|
|
</div> |
|
|
<div id="heatmap" class="flex flex-wrap"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-5 border border-gray-100"> |
|
|
<h2 class="text-lg font-semibold mb-3 text-gray-800">Your Stats</h2> |
|
|
<div class="grid grid-cols-2 gap-4 mb-4"> |
|
|
<div class="bg-blue-50 p-3 rounded-lg border border-blue-100"> |
|
|
<div class="text-xs font-medium text-blue-700">Current Streak</div> |
|
|
<div class="text-xl font-bold text-blue-900" id="currentStreak">0 days</div> |
|
|
</div> |
|
|
<div class="bg-yellow-50 p-3 rounded-lg border border-yellow-100"> |
|
|
<div class="text-xs font-medium text-yellow-700">Best Streak</div> |
|
|
<div class="text-xl font-bold text-yellow-900" id="bestStreak">0 days</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-pink-50 p-3 rounded-lg border border-pink-100 mb-4"> |
|
|
<div class="text-xs font-medium text-pink-700">Completion Rate</div> |
|
|
<div class="text-xl font-bold text-pink-900" id="completionRate">0%</div> |
|
|
</div> |
|
|
<div class="text-xs text-gray-500"> |
|
|
<div class="flex justify-between mb-1"> |
|
|
<span>Applications per day:</span> |
|
|
<span id="avgPerDay">0</span> |
|
|
</div> |
|
|
<div class="flex justify-between"> |
|
|
<span>Days applied:</span> |
|
|
<span id="daysApplied">0</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-5 mb-6 border border-gray-100"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h2 class="text-lg font-semibold text-gray-800">Application History</h2> |
|
|
<div class="flex gap-2"> |
|
|
<button id="sortDateBtn" class="btn-secondary px-3 py-1 text-xs rounded-md"> |
|
|
<i class="fas fa-sort-amount-down mr-1"></i> Sort by Date |
|
|
</button> |
|
|
<button id="sortCountBtn" class="btn-secondary px-3 py-1 text-xs rounded-md"> |
|
|
<i class="fas fa-sort-numeric-down mr-1"></i> Sort by Count |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="overflow-x-auto"> |
|
|
<table class="min-w-full divide-y divide-gray-200"> |
|
|
<thead class="bg-gray-50"> |
|
|
<tr> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Applications</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Goal</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Notes</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="historyTable" class="bg-white divide-y divide-gray-200"> |
|
|
|
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-white rounded-lg shadow-md p-5 mb-6 border border-gray-100"> |
|
|
<h2 class="text-lg font-semibold mb-3 text-gray-800">Application Timeline</h2> |
|
|
<div id="timeline" class="space-y-4"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="adminSection" class="bg-white rounded-lg shadow-md p-5 border border-gray-100 hidden"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h2 class="text-lg font-semibold text-gray-800">All Users Progress</h2> |
|
|
<div class="flex gap-2"> |
|
|
<button id="refreshUsersBtn" class="btn-secondary px-3 py-1 text-sm rounded-md"> |
|
|
<i class="fas fa-sync-alt mr-1"></i> Refresh |
|
|
</button> |
|
|
<button id="exportDataBtn" class="btn-success px-3 py-1 text-sm rounded-md"> |
|
|
<i class="fas fa-file-export mr-1"></i> Export Data |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="overflow-x-auto"> |
|
|
<table class="min-w-full divide-y divide-gray-200"> |
|
|
<thead class="bg-gray-50"> |
|
|
<tr> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Today</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Weekly Avg</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Streak</th> |
|
|
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> |
|
|
</tr> |
|
|
</thead> |
|
|
<tbody id="usersTable" class="bg-white divide-y divide-gray-200"> |
|
|
|
|
|
</tbody> |
|
|
</table> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="editModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
|
|
<div class="bg-white rounded-lg p-6 w-full max-w-md"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h3 class="text-lg font-semibold text-gray-800" id="editModalTitle">Edit Entry</h3> |
|
|
<button id="closeEditModal" class="text-gray-500 hover:text-gray-700"> |
|
|
<i class="fas fa-times"></i> |
|
|
</button> |
|
|
</div> |
|
|
<div class="space-y-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Date</label> |
|
|
<div class="date-picker relative"> |
|
|
<input type="date" id="editDate" class="w-full px-3 py-2 text-sm input-field rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500"> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Applications</label> |
|
|
<input type="number" id="editCount" min="0" class="w-full px-3 py-2 text-sm input-field rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500"> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Goal</label> |
|
|
<input type="number" id="editGoal" min="1" class="w-full px-3 py-2 text-sm input-field rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500"> |
|
|
</div> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Notes</label> |
|
|
<textarea id="editNotes" class="w-full px-3 py-2 text-sm input-field rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500" rows="3"></textarea> |
|
|
</div> |
|
|
</div> |
|
|
<div class="mt-6 flex justify-end space-x-3"> |
|
|
<button id="cancelEdit" class="btn-secondary px-4 py-2 text-sm rounded-md"> |
|
|
Cancel |
|
|
</button> |
|
|
<button id="saveEdit" class="btn-primary px-4 py-2 text-sm rounded-md"> |
|
|
Save Changes |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="noteModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
|
|
<div class="bg-white rounded-lg p-6 w-full max-w-md"> |
|
|
<div class="flex justify-between items-center mb-4"> |
|
|
<h3 class="text-lg font-semibold text-gray-800" id="noteModalTitle">Add Note</h3> |
|
|
<button id="closeNoteModal" class="text-gray-500 hover:text-gray-700"> |
|
|
<i class="fas fa-times"></i> |
|
|
</button> |
|
|
</div> |
|
|
<div class="space-y-4"> |
|
|
<div> |
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Note</label> |
|
|
<textarea id="noteContent" class="w-full px-3 py-2 text-sm input-field rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500" rows="5" placeholder="Add details about your applications today..."></textarea> |
|
|
</div> |
|
|
</div> |
|
|
<div class="mt-6 flex justify-end space-x-3"> |
|
|
<button id="cancelNote" class="btn-secondary px-4 py-2 text-sm rounded-md"> |
|
|
Cancel |
|
|
</button> |
|
|
<button id="saveNote" class="btn-primary px-4 py-2 text-sm rounded-md"> |
|
|
Save Note |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
const usernameInput = document.getElementById('username'); |
|
|
const saveUserBtn = document.getElementById('saveUserBtn'); |
|
|
const dailyGoalInput = document.getElementById('dailyGoal'); |
|
|
const saveGoalBtn = document.getElementById('saveGoalBtn'); |
|
|
const currentUserDiv = document.getElementById('currentUser'); |
|
|
const userDisplay = document.getElementById('userDisplay'); |
|
|
const goalDisplay = document.getElementById('goalDisplay'); |
|
|
const dateDisplay = document.getElementById('dateDisplay'); |
|
|
const streakDisplay = document.getElementById('streakDisplay'); |
|
|
const decreaseBtn = document.getElementById('decreaseBtn'); |
|
|
const increaseBtn = document.getElementById('increaseBtn'); |
|
|
const countDisplay = document.getElementById('countDisplay'); |
|
|
const progressText = document.getElementById('progressText'); |
|
|
const progressBar = document.getElementById('progressBar'); |
|
|
const saveBtn = document.getElementById('saveBtn'); |
|
|
const deleteBtn = document.getElementById('deleteBtn'); |
|
|
const addNoteBtn = document.getElementById('addNoteBtn'); |
|
|
const historyTable = document.getElementById('historyTable'); |
|
|
const adminSection = document.getElementById('adminSection'); |
|
|
const usersTable = document.getElementById('usersTable'); |
|
|
const selectedDateInput = document.getElementById('selectedDate'); |
|
|
const dailyProgressTitle = document.getElementById('dailyProgressTitle'); |
|
|
const todayCountEl = document.getElementById('todayCount'); |
|
|
const weeklyCountEl = document.getElementById('weeklyCount'); |
|
|
const totalCountEl = document.getElementById('totalCount'); |
|
|
const refreshUsersBtn = document.getElementById('refreshUsersBtn'); |
|
|
const exportDataBtn = document.getElementById('exportDataBtn'); |
|
|
const editModal = document.getElementById('editModal'); |
|
|
const closeEditModal = document.getElementById('closeEditModal'); |
|
|
const editModalTitle = document.getElementById('editModalTitle'); |
|
|
const editDate = document.getElementById('editDate'); |
|
|
const editCount = document.getElementById('editCount'); |
|
|
const editGoal = document.getElementById('editGoal'); |
|
|
const editNotes = document.getElementById('editNotes'); |
|
|
const cancelEdit = document.getElementById('cancelEdit'); |
|
|
const saveEdit = document.getElementById('saveEdit'); |
|
|
const noteModal = document.getElementById('noteModal'); |
|
|
const closeNoteModal = document.getElementById('closeNoteModal'); |
|
|
const noteModalTitle = document.getElementById('noteModalTitle'); |
|
|
const noteContent = document.getElementById('noteContent'); |
|
|
const cancelNote = document.getElementById('cancelNote'); |
|
|
const saveNote = document.getElementById('saveNote'); |
|
|
const todayBtn = document.getElementById('todayBtn'); |
|
|
const yesterdayBtn = document.getElementById('yesterdayBtn'); |
|
|
const sortDateBtn = document.getElementById('sortDateBtn'); |
|
|
const sortCountBtn = document.getElementById('sortCountBtn'); |
|
|
const heatmap = document.getElementById('heatmap'); |
|
|
const timeline = document.getElementById('timeline'); |
|
|
const currentStreakEl = document.getElementById('currentStreak'); |
|
|
const bestStreakEl = document.getElementById('bestStreak'); |
|
|
const completionRateEl = document.getElementById('completionRate'); |
|
|
const avgPerDayEl = document.getElementById('avgPerDay'); |
|
|
const daysAppliedEl = document.getElementById('daysApplied'); |
|
|
|
|
|
|
|
|
let currentUser = localStorage.getItem('currentUser') || ''; |
|
|
let dailyGoal = parseInt(localStorage.getItem(`${currentUser}_dailyGoal`)) || 30; |
|
|
let currentCount = 0; |
|
|
let selectedDate = new Date().toISOString().split('T')[0]; |
|
|
let applicationsData = JSON.parse(localStorage.getItem('applicationsData')) || {}; |
|
|
let allUsersData = JSON.parse(localStorage.getItem('allUsersData')) || {}; |
|
|
let currentEditEntry = null; |
|
|
let sortOrder = 'dateDesc'; |
|
|
|
|
|
|
|
|
function init() { |
|
|
|
|
|
selectedDateInput.value = selectedDate; |
|
|
selectedDateInput.max = selectedDate; |
|
|
|
|
|
if (currentUser) { |
|
|
usernameInput.value = currentUser; |
|
|
dailyGoalInput.value = dailyGoal; |
|
|
showCurrentUser(); |
|
|
loadDateData(); |
|
|
loadHistory(); |
|
|
loadAllUsersData(); |
|
|
updateStats(); |
|
|
} |
|
|
|
|
|
|
|
|
if (currentUser === 'loliloli') { |
|
|
adminSection.classList.remove('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function showCurrentUser() { |
|
|
currentUserDiv.classList.remove('hidden'); |
|
|
userDisplay.textContent = currentUser; |
|
|
goalDisplay.textContent = dailyGoal; |
|
|
progressText.textContent = `0/${dailyGoal}`; |
|
|
updateDateDisplay(); |
|
|
} |
|
|
|
|
|
|
|
|
function updateDateDisplay() { |
|
|
const dateObj = new Date(selectedDate); |
|
|
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', |
|
|
'July', 'August', 'September', 'October', 'November', 'December']; |
|
|
const day = dateObj.getDate(); |
|
|
const month = monthNames[dateObj.getMonth()]; |
|
|
const dayOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][dateObj.getDay()]; |
|
|
|
|
|
|
|
|
let suffix = 'th'; |
|
|
if (day % 10 === 1 && day !== 11) suffix = 'st'; |
|
|
else if (day % 10 === 2 && day !== 12) suffix = 'nd'; |
|
|
else if (day % 10 === 3 && day !== 13) suffix = 'rd'; |
|
|
|
|
|
dateDisplay.textContent = `${dayOfWeek}, ${day}${suffix} ${month}`; |
|
|
} |
|
|
|
|
|
|
|
|
function loadDateData() { |
|
|
if (!currentUser || !applicationsData[currentUser]) { |
|
|
currentCount = 0; |
|
|
updateCountDisplay(); |
|
|
deleteBtn.classList.add('hidden'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const dateData = applicationsData[currentUser][selectedDate]; |
|
|
|
|
|
if (dateData) { |
|
|
currentCount = dateData.count; |
|
|
deleteBtn.classList.remove('hidden'); |
|
|
} else { |
|
|
currentCount = 0; |
|
|
deleteBtn.classList.add('hidden'); |
|
|
} |
|
|
|
|
|
updateCountDisplay(); |
|
|
updateTitle(); |
|
|
updateDateDisplay(); |
|
|
} |
|
|
|
|
|
|
|
|
function updateTitle() { |
|
|
const today = new Date().toISOString().split('T')[0]; |
|
|
const yesterday = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split('T')[0]; |
|
|
|
|
|
if (selectedDate === today) { |
|
|
dailyProgressTitle.textContent = "Today's Progress"; |
|
|
} else if (selectedDate === yesterday) { |
|
|
dailyProgressTitle.textContent = "Yesterday's Progress"; |
|
|
} else { |
|
|
const dateObj = new Date(selectedDate); |
|
|
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', |
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; |
|
|
const day = dateObj.getDate(); |
|
|
const month = monthNames[dateObj.getMonth()]; |
|
|
|
|
|
dailyProgressTitle.textContent = `${day} ${month} Progress`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateCountDisplay() { |
|
|
countDisplay.textContent = currentCount; |
|
|
progressText.textContent = `${currentCount}/${dailyGoal}`; |
|
|
const progressPercentage = Math.min((currentCount / dailyGoal) * 100, 100); |
|
|
progressBar.style.width = `${progressPercentage}%`; |
|
|
|
|
|
|
|
|
if (progressPercentage >= 100) { |
|
|
progressBar.classList.remove('bg-indigo-600', 'bg-yellow-500', 'bg-red-500'); |
|
|
progressBar.classList.add('bg-green-500'); |
|
|
} else if (progressPercentage >= 75) { |
|
|
progressBar.classList.remove('bg-green-500', 'bg-yellow-500', 'bg-red-500'); |
|
|
progressBar.classList.add('bg-indigo-600'); |
|
|
} else if (progressPercentage >= 50) { |
|
|
progressBar.classList.remove('bg-indigo-600', 'bg-green-500', 'bg-red-500'); |
|
|
progressBar.classList.add('bg-yellow-500'); |
|
|
} else { |
|
|
progressBar.classList.remove('bg-indigo-600', 'bg-yellow-500', 'bg-green-500'); |
|
|
progressBar.classList.add('bg-red-500'); |
|
|
} |
|
|
|
|
|
|
|
|
decreaseBtn.disabled = currentCount <= 0; |
|
|
} |
|
|
|
|
|
|
|
|
function loadHistory() { |
|
|
if (!currentUser || !applicationsData[currentUser]) { |
|
|
historyTable.innerHTML = '<tr><td colspan="6" class="px-4 py-3 text-center text-xs text-gray-500">No data available</td></tr>'; |
|
|
todayCountEl.textContent = '0'; |
|
|
weeklyCountEl.textContent = '0'; |
|
|
totalCountEl.textContent = '0'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
historyTable.innerHTML = ''; |
|
|
|
|
|
|
|
|
const userData = applicationsData[currentUser]; |
|
|
let dates = Object.keys(userData); |
|
|
|
|
|
if (sortOrder === 'dateDesc') { |
|
|
dates.sort((a, b) => new Date(b) - new Date(a)); |
|
|
} else if (sortOrder === 'dateAsc') { |
|
|
dates.sort((a, b) => new Date(a) - new Date(b)); |
|
|
} else if (sortOrder === 'countDesc') { |
|
|
dates.sort((a, b) => userData[b].count - userData[a].count); |
|
|
} else if (sortOrder === 'countAsc') { |
|
|
dates.sort((a, b) => userData[a].count - userData[b].count); |
|
|
} |
|
|
|
|
|
|
|
|
const today = new Date().toISOString().split('T')[0]; |
|
|
const todayCount = userData[today] ? userData[today].count : 0; |
|
|
|
|
|
|
|
|
const weeklyDates = dates.filter(date => { |
|
|
const dateObj = new Date(date); |
|
|
const todayObj = new Date(today); |
|
|
return dateObj >= new Date(todayObj.setDate(todayObj.getDate() - 7)); |
|
|
}); |
|
|
const weeklyCount = weeklyDates.reduce((sum, date) => sum + userData[date].count, 0); |
|
|
|
|
|
|
|
|
const totalCount = dates.reduce((sum, date) => sum + userData[date].count, 0); |
|
|
|
|
|
|
|
|
todayCountEl.textContent = todayCount; |
|
|
weeklyCountEl.textContent = weeklyCount; |
|
|
totalCountEl.textContent = totalCount; |
|
|
|
|
|
|
|
|
createWeeklyChart(dates.slice(0, 7).reverse()); |
|
|
|
|
|
|
|
|
createHeatmap(); |
|
|
|
|
|
|
|
|
createTimeline(dates); |
|
|
|
|
|
|
|
|
dates.forEach(date => { |
|
|
const entry = userData[date]; |
|
|
const row = document.createElement('tr'); |
|
|
|
|
|
const dateCell = document.createElement('td'); |
|
|
dateCell.className = 'px-4 py-3 whitespace-nowrap text-xs text-gray-500'; |
|
|
dateCell.textContent = formatDate(date); |
|
|
|
|
|
const countCell = document.createElement('td'); |
|
|
countCell.className = 'px-4 py-3 whitespace-nowrap text-xs text-gray-500'; |
|
|
countCell.textContent = entry.count; |
|
|
|
|
|
const goalCell = document.createElement('td'); |
|
|
goalCell.className = 'px-4 py-3 whitespace-nowrap text-xs text-gray-500'; |
|
|
goalCell.textContent = entry.goal; |
|
|
|
|
|
const statusCell = document.createElement('td'); |
|
|
statusCell.className = 'px-4 py-3 whitespace-nowrap text-xs'; |
|
|
|
|
|
const statusSpan = document.createElement('span'); |
|
|
statusSpan.className = entry.count >= entry.goal ? |
|
|
'status-badge bg-green-100 text-green-800' : |
|
|
'status-badge bg-red-100 text-red-800'; |
|
|
statusSpan.textContent = entry.count >= entry.goal ? 'Completed' : 'Pending'; |
|
|
|
|
|
statusCell.appendChild(statusSpan); |
|
|
|
|
|
const notesCell = document.createElement('td'); |
|
|
notesCell.className = 'px-4 py-3 text-xs text-gray-500 max-w-xs truncate'; |
|
|
notesCell.textContent = entry.notes || '—'; |
|
|
notesCell.title = entry.notes || ''; |
|
|
|
|
|
const actionCell = document.createElement('td'); |
|
|
actionCell.className = 'px-4 py-3 whitespace-nowrap text-xs text-gray-500 flex gap-2'; |
|
|
|
|
|
const editButton = document.createElement('button'); |
|
|
editButton.className = 'text-indigo-600 hover:text-indigo-900'; |
|
|
editButton.innerHTML = '<i class="fas fa-edit"></i>'; |
|
|
editButton.title = 'Edit this entry'; |
|
|
editButton.addEventListener('click', () => { |
|
|
openEditModal(date, entry); |
|
|
}); |
|
|
|
|
|
const deleteButton = document.createElement('button'); |
|
|
deleteButton.className = 'text-red-600 hover:text-red-900'; |
|
|
deleteButton.innerHTML = '<i class="fas fa-trash"></i>'; |
|
|
deleteButton.title = 'Delete this entry'; |
|
|
deleteButton.addEventListener('click', () => { |
|
|
if (confirm(`Are you sure you want to delete the entry for ${formatDate(date)}?`)) { |
|
|
delete applicationsData[currentUser][date]; |
|
|
localStorage.setItem('applicationsData', JSON.stringify(applicationsData)); |
|
|
|
|
|
if (allUsersData[currentUser] && allUsersData[currentUser].applications[date]) { |
|
|
delete allUsersData[currentUser].applications[date]; |
|
|
localStorage.setItem('allUsersData', JSON.stringify(allUsersData)); |
|
|
} |
|
|
|
|
|
loadHistory(); |
|
|
updateStats(); |
|
|
|
|
|
|
|
|
if (selectedDate === date) { |
|
|
currentCount = 0; |
|
|
updateCountDisplay(); |
|
|
deleteBtn.classList.add('hidden'); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
actionCell.appendChild(editButton); |
|
|
actionCell.appendChild(deleteButton); |
|
|
|
|
|
row.appendChild(dateCell); |
|
|
row.appendChild(countCell); |
|
|
row.appendChild(goalCell); |
|
|
row.appendChild(statusCell); |
|
|
row.appendChild(notesCell); |
|
|
row.appendChild(actionCell); |
|
|
|
|
|
historyTable.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function createWeeklyChart(dates) { |
|
|
const chartContainer = document.querySelector('.chart-container'); |
|
|
chartContainer.innerHTML = ''; |
|
|
|
|
|
if (!dates.length) return; |
|
|
|
|
|
|
|
|
const maxValue = Math.max(...dates.map(date => { |
|
|
return applicationsData[currentUser][date].count; |
|
|
}), dailyGoal); |
|
|
|
|
|
dates.forEach(date => { |
|
|
const entry = applicationsData[currentUser][date]; |
|
|
const day = new Date(date).getDate(); |
|
|
const dayOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][new Date(date).getDay()]; |
|
|
const count = entry.count; |
|
|
const goal = entry.goal; |
|
|
|
|
|
const barContainer = document.createElement('div'); |
|
|
barContainer.className = 'flex flex-col items-center w-full'; |
|
|
|
|
|
const barWrapper = document.createElement('div'); |
|
|
barWrapper.className = 'relative flex-1 w-full flex items-end'; |
|
|
barWrapper.style.height = '100%'; |
|
|
|
|
|
|
|
|
const countBar = document.createElement('div'); |
|
|
countBar.className = 'chart-bar'; |
|
|
|
|
|
|
|
|
if (count === 0) { |
|
|
countBar.classList.add('bg-gray-200'); |
|
|
} else if (count < 5) { |
|
|
countBar.classList.add('bg-green-100'); |
|
|
} else if (count < 10) { |
|
|
countBar.classList.add('bg-green-300'); |
|
|
} else if (count < 20) { |
|
|
countBar.classList.add('bg-green-500'); |
|
|
} else { |
|
|
countBar.classList.add('bg-green-700'); |
|
|
} |
|
|
|
|
|
countBar.style.height = `${(count / maxValue) * 100}%`; |
|
|
countBar.setAttribute('data-count', count); |
|
|
|
|
|
|
|
|
const goalLine = document.createElement('div'); |
|
|
goalLine.className = 'chart-goal-line'; |
|
|
goalLine.style.bottom = `${(goal / maxValue) * 100}%`; |
|
|
|
|
|
|
|
|
const label = document.createElement('div'); |
|
|
label.className = 'text-xs text-gray-500 mt-1'; |
|
|
label.textContent = dayOfWeek; |
|
|
|
|
|
barWrapper.appendChild(countBar); |
|
|
barWrapper.appendChild(goalLine); |
|
|
barContainer.appendChild(barWrapper); |
|
|
barContainer.appendChild(label); |
|
|
|
|
|
chartContainer.appendChild(barContainer); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function createHeatmap() { |
|
|
heatmap.innerHTML = ''; |
|
|
|
|
|
if (!currentUser || !applicationsData[currentUser]) return; |
|
|
|
|
|
const today = new Date(); |
|
|
const startDate = new Date(today); |
|
|
startDate.setDate(today.getDate() - 90); |
|
|
|
|
|
|
|
|
const dateMap = {}; |
|
|
for (let d = new Date(startDate); d <= today; d.setDate(d.getDate() + 1)) { |
|
|
const dateStr = d.toISOString().split('T')[0]; |
|
|
dateMap[dateStr] = applicationsData[currentUser][dateStr] ? applicationsData[currentUser][dateStr].count : 0; |
|
|
} |
|
|
|
|
|
|
|
|
const weeks = []; |
|
|
let currentWeek = []; |
|
|
let currentDay = new Date(startDate); |
|
|
|
|
|
while (currentDay <= today) { |
|
|
currentWeek.push({ |
|
|
date: currentDay.toISOString().split('T')[0], |
|
|
count: dateMap[currentDay.toISOString().split('T')[0]] || 0 |
|
|
}); |
|
|
|
|
|
if (currentDay.getDay() === 6 || currentDay.toISOString().split('T')[0] === today.toISOString().split('T')[0]) { |
|
|
weeks.push(currentWeek); |
|
|
currentWeek = []; |
|
|
} |
|
|
|
|
|
currentDay = new Date(currentDay); |
|
|
currentDay.setDate(currentDay.getDate() + 1); |
|
|
} |
|
|
|
|
|
|
|
|
weeks.forEach(week => { |
|
|
const weekContainer = document.createElement('div'); |
|
|
weekContainer.className = 'flex flex-col'; |
|
|
|
|
|
week.forEach(day => { |
|
|
const dayElement = document.createElement('div'); |
|
|
dayElement.className = 'heatmap-day'; |
|
|
|
|
|
|
|
|
if (day.count === 0) { |
|
|
dayElement.classList.add('bg-gray-100'); |
|
|
} else if (day.count < 5) { |
|
|
dayElement.classList.add('bg-green-100'); |
|
|
} else if (day.count < 10) { |
|
|
dayElement.classList.add('bg-green-300'); |
|
|
} else if (day.count < 20) { |
|
|
dayElement.classList.add('bg-green-500'); |
|
|
} else { |
|
|
dayElement.classList.add('bg-green-700'); |
|
|
} |
|
|
|
|
|
dayElement.setAttribute('data-tooltip', `${formatDate(day.date)}: ${day.count} applications`); |
|
|
|
|
|
weekContainer.appendChild(dayElement); |
|
|
}); |
|
|
|
|
|
heatmap.appendChild(weekContainer); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function createTimeline(dates) { |
|
|
timeline.innerHTML = ''; |
|
|
|
|
|
if (!dates.length) { |
|
|
timeline.innerHTML = '<p class="text-sm text-gray-500">No applications yet. Start applying to see your timeline!</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const recentDates = dates.slice(0, 10); |
|
|
|
|
|
recentDates.forEach(date => { |
|
|
const entry = applicationsData[currentUser][date]; |
|
|
const timelineItem = document.createElement('div'); |
|
|
timelineItem.className = 'timeline-item'; |
|
|
|
|
|
const content = document.createElement('div'); |
|
|
content.className = 'text-sm'; |
|
|
|
|
|
const dateSpan = document.createElement('span'); |
|
|
dateSpan.className = 'font-medium text-gray-900'; |
|
|
dateSpan.textContent = formatDate(date); |
|
|
|
|
|
const countSpan = document.createElement('span'); |
|
|
countSpan.className = 'text-gray-600 ml-2'; |
|
|
countSpan.textContent = `${entry.count} applications`; |
|
|
|
|
|
const statusSpan = document.createElement('span'); |
|
|
statusSpan.className = entry.count >= entry.goal ? |
|
|
'status-badge bg-green-100 text-green-800 ml-2' : |
|
|
'status-badge bg-red-100 text-red-800 ml-2'; |
|
|
statusSpan.textContent = entry.count >= entry.goal ? 'Goal met' : 'Goal not met'; |
|
|
|
|
|
content.appendChild(dateSpan); |
|
|
content.appendChild(countSpan); |
|
|
content.appendChild(statusSpan); |
|
|
|
|
|
if (entry.notes) { |
|
|
const notesDiv = document.createElement('div'); |
|
|
notesDiv.className = 'text-xs text-gray-500 mt-1'; |
|
|
notesDiv.textContent = entry.notes; |
|
|
content.appendChild(notesDiv); |
|
|
} |
|
|
|
|
|
timelineItem.appendChild(content); |
|
|
timeline.appendChild(timelineItem); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function updateStats() { |
|
|
if (!currentUser || !applicationsData[currentUser]) return; |
|
|
|
|
|
const userData = applicationsData[currentUser]; |
|
|
const dates = Object.keys(userData).sort((a, b) => new Date(a) - new Date(b)); |
|
|
const today = new Date().toISOString().split('T')[0]; |
|
|
|
|
|
|
|
|
let currentStreak = 0; |
|
|
let bestStreak = 0; |
|
|
let tempStreak = 0; |
|
|
|
|
|
|
|
|
const todayHasData = userData[today] !== undefined; |
|
|
|
|
|
|
|
|
let checkDate = todayHasData ? today : new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split('T')[0]; |
|
|
|
|
|
while (userData[checkDate] !== undefined) { |
|
|
currentStreak++; |
|
|
tempStreak++; |
|
|
|
|
|
bestStreak = Math.max(bestStreak, tempStreak); |
|
|
|
|
|
|
|
|
checkDate = new Date(new Date(checkDate).setDate(new Date(checkDate).getDate() - 1)).toISOString().split('T')[0]; |
|
|
} |
|
|
|
|
|
|
|
|
const totalEntries = dates.length; |
|
|
const completedEntries = dates.filter(date => userData[date].count >= userData[date].goal).length; |
|
|
const completionRate = totalEntries > 0 ? Math.round((completedEntries / totalEntries) * 100) : 0; |
|
|
|
|
|
|
|
|
const totalApplications = dates.reduce((sum, date) => sum + userData[date].count, 0); |
|
|
const avgPerDay = totalEntries > 0 ? (totalApplications / totalEntries).toFixed(1) : 0; |
|
|
|
|
|
|
|
|
currentStreakEl.textContent = `${currentStreak} day${currentStreak !== 1 ? 's' : ''}`; |
|
|
bestStreakEl.textContent = `${bestStreak} day${bestStreak !== 1 ? 's' : ''}`; |
|
|
completionRateEl.textContent = `${completionRate}%`; |
|
|
avgPerDayEl.textContent = avgPerDay; |
|
|
daysAppliedEl.textContent = totalEntries; |
|
|
streakDisplay.textContent = `${currentStreak} day${currentStreak !== 1 ? 's' : ''}`; |
|
|
} |
|
|
|
|
|
|
|
|
function openEditModal(date, entry) { |
|
|
currentEditEntry = { date, entry }; |
|
|
editModalTitle.textContent = `Edit Entry for ${formatDate(date)}`; |
|
|
editDate.value = date; |
|
|
editCount.value = entry.count; |
|
|
editGoal.value = entry.goal; |
|
|
editNotes.value = entry.notes || ''; |
|
|
editModal.classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
|
|
|
function closeEditModalHandler() { |
|
|
editModal.classList.add('hidden'); |
|
|
currentEditEntry = null; |
|
|
} |
|
|
|
|
|
|
|
|
function saveEditHandler() { |
|
|
const newCount = parseInt(editCount.value); |
|
|
const newGoal = parseInt(editGoal.value); |
|
|
const newDate = editDate.value; |
|
|
const newNotes = editNotes.value; |
|
|
|
|
|
if (isNaN(newCount) || newCount < 0) { |
|
|
alert('Please enter a valid application count'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (isNaN(newGoal) || newGoal < 1) { |
|
|
alert('Please enter a valid goal (minimum 1)'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const { date: oldDate, entry } = currentEditEntry; |
|
|
|
|
|
|
|
|
if (newDate !== oldDate) { |
|
|
delete applicationsData[currentUser][oldDate]; |
|
|
|
|
|
if (allUsersData[currentUser] && allUsersData[currentUser].applications[oldDate]) { |
|
|
delete allUsersData[currentUser].applications[oldDate]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
applicationsData[currentUser][newDate] = { |
|
|
count: newCount, |
|
|
goal: newGoal, |
|
|
notes: newNotes |
|
|
}; |
|
|
localStorage.setItem('applicationsData', JSON.stringify(applicationsData)); |
|
|
|
|
|
|
|
|
if (!allUsersData[currentUser]) { |
|
|
allUsersData[currentUser] = { |
|
|
goal: dailyGoal, |
|
|
applications: {} |
|
|
}; |
|
|
} |
|
|
allUsersData[currentUser].applications[newDate] = { |
|
|
count: newCount, |
|
|
goal: newGoal, |
|
|
notes: newNotes |
|
|
}; |
|
|
localStorage.setItem('allUsersData', JSON.stringify(allUsersData)); |
|
|
|
|
|
|
|
|
loadHistory(); |
|
|
updateStats(); |
|
|
|
|
|
|
|
|
if (selectedDate === newDate || selectedDate === oldDate) { |
|
|
if (selectedDate === newDate) { |
|
|
currentCount = newCount; |
|
|
} else { |
|
|
currentCount = 0; |
|
|
} |
|
|
updateCountDisplay(); |
|
|
|
|
|
if (selectedDate === newDate && newCount > 0) { |
|
|
deleteBtn.classList.remove('hidden'); |
|
|
} else if (selectedDate === oldDate) { |
|
|
deleteBtn.classList.add('hidden'); |
|
|
} |
|
|
} |
|
|
|
|
|
closeEditModalHandler(); |
|
|
} |
|
|
|
|
|
|
|
|
function openNoteModal() { |
|
|
noteModalTitle.textContent = `Add Note for ${formatDate(selectedDate)}`; |
|
|
noteContent.value = ''; |
|
|
noteModal.classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
|
|
|
function closeNoteModalHandler() { |
|
|
noteModal.classList.add('hidden'); |
|
|
} |
|
|
|
|
|
|
|
|
function saveNoteHandler() { |
|
|
const note = noteContent.value.trim(); |
|
|
|
|
|
|
|
|
if (!applicationsData[currentUser]) { |
|
|
applicationsData[currentUser] = {}; |
|
|
} |
|
|
|
|
|
if (!applicationsData[currentUser][selectedDate]) { |
|
|
applicationsData[currentUser][selectedDate] = { |
|
|
count: currentCount, |
|
|
goal: dailyGoal, |
|
|
notes: note |
|
|
}; |
|
|
} else { |
|
|
applicationsData[currentUser][selectedDate].notes = note; |
|
|
} |
|
|
|
|
|
|
|
|
if (!allUsersData[currentUser]) { |
|
|
allUsersData[currentUser] = { |
|
|
goal: dailyGoal, |
|
|
applications: {} |
|
|
}; |
|
|
} |
|
|
|
|
|
if (!allUsersData[currentUser].applications[selectedDate]) { |
|
|
allUsersData[currentUser].applications[selectedDate] = { |
|
|
count: currentCount, |
|
|
goal: dailyGoal, |
|
|
notes: note |
|
|
}; |
|
|
} else { |
|
|
allUsersData[currentUser].applications[selectedDate].notes = note; |
|
|
} |
|
|
|
|
|
localStorage.setItem('applicationsData', JSON.stringify(applicationsData)); |
|
|
localStorage.setItem('allUsersData', JSON.stringify(allUsersData)); |
|
|
|
|
|
|
|
|
loadHistory(); |
|
|
|
|
|
closeNoteModalHandler(); |
|
|
} |
|
|
|
|
|
|
|
|
function loadAllUsersData() { |
|
|
if (currentUser !== 'loliloli') return; |
|
|
|
|
|
usersTable.innerHTML = ''; |
|
|
|
|
|
const users = Object.keys(allUsersData); |
|
|
if (users.length === 0) { |
|
|
usersTable.innerHTML = '<tr><td colspan="6" class="px-4 py-3 text-center text-xs text-gray-500">No user data available</td></tr>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
users.forEach(user => { |
|
|
if (user === 'loliloli') return; |
|
|
|
|
|
const userData = allUsersData[user]; |
|
|
const today = new Date().toISOString().split('T')[0]; |
|
|
const todayCount = userData.applications[today] ? userData.applications[today].count : 0; |
|
|
|
|
|
|
|
|
const dates = Object.keys(userData.applications); |
|
|
const last7Days = dates |
|
|
.filter(date => { |
|
|
const dateObj = new Date(date); |
|
|
const todayObj = new Date(today); |
|
|
return dateObj >= new Date(todayObj.setDate(todayObj.getDate() - 7)); |
|
|
}) |
|
|
.map(date => userData.applications[date].count); |
|
|
|
|
|
const weeklyAvg = last7Days.length > 0 ? |
|
|
Math.round(last7Days.reduce((a, b) => a + b, 0) / last7Days.length) : 0; |
|
|
|
|
|
|
|
|
const total = dates.reduce((sum, date) => sum + userData.applications[date].count, 0); |
|
|
|
|
|
|
|
|
let currentStreak = 0; |
|
|
let checkDate = today; |
|
|
|
|
|
while (userData.applications[checkDate] !== undefined) { |
|
|
currentStreak++; |
|
|
checkDate = new Date(new Date(checkDate).setDate(new Date(checkDate).getDate() - 1)).toISOString().split('T')[0]; |
|
|
} |
|
|
|
|
|
const row = document.createElement('tr'); |
|
|
|
|
|
const userCell = document.createElement('td'); |
|
|
userCell.className = 'px-4 py-3 whitespace-nowrap text-xs font-medium text-gray-900'; |
|
|
userCell.textContent = user; |
|
|
|
|
|
const todayCell = document.createElement('td'); |
|
|
todayCell.className = 'px-4 py-3 whitespace-nowrap text-xs text-gray-500'; |
|
|
todayCell.textContent = todayCount; |
|
|
|
|
|
const weeklyCell = document.createElement('td'); |
|
|
weeklyCell.className = 'px-4 py-3 whitespace-nowrap text-xs text-gray-500'; |
|
|
weeklyCell.textContent = weeklyAvg; |
|
|
|
|
|
const totalCell = document.createElement('td'); |
|
|
totalCell.className = 'px-4 py-3 whitespace-nowrap text-xs text-gray-500'; |
|
|
totalCell.textContent = total; |
|
|
|
|
|
const streakCell = document.createElement('td'); |
|
|
streakCell.className = 'px-4 py-3 whitespace-nowrap text-xs text-gray-500'; |
|
|
streakCell.textContent = `${currentStreak} day${currentStreak !== 1 ? 's' : ''}`; |
|
|
|
|
|
const actionCell = document.createElement('td'); |
|
|
actionCell.className = 'px-4 py-3 whitespace-nowrap text-xs text-gray-500'; |
|
|
|
|
|
const viewButton = document.createElement('button'); |
|
|
viewButton.className = 'text-indigo-600 hover:text-indigo-900'; |
|
|
viewButton.innerHTML = '<i class="fas fa-eye"></i>'; |
|
|
viewButton.title = 'View user details'; |
|
|
viewButton.addEventListener('click', () => { |
|
|
alert(`Viewing details for ${user}\nToday: ${todayCount}\nWeekly Avg: ${weeklyAvg}\nTotal: ${total}\nCurrent Streak: ${currentStreak} days`); |
|
|
}); |
|
|
|
|
|
actionCell.appendChild(viewButton); |
|
|
|
|
|
row.appendChild(userCell); |
|
|
row.appendChild(todayCell); |
|
|
row.appendChild(weeklyCell); |
|
|
row.appendChild(totalCell); |
|
|
row.appendChild(streakCell); |
|
|
row.appendChild(actionCell); |
|
|
|
|
|
usersTable.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function exportData() { |
|
|
const dataStr = JSON.stringify(allUsersData, null, 2); |
|
|
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); |
|
|
|
|
|
const exportFileDefaultName = 'job-application-tracker-data.json'; |
|
|
|
|
|
const linkElement = document.createElement('a'); |
|
|
linkElement.setAttribute('href', dataUri); |
|
|
linkElement.setAttribute('download', exportFileDefaultName); |
|
|
linkElement.click(); |
|
|
} |
|
|
|
|
|
|
|
|
function formatDate(dateString) { |
|
|
const dateObj = new Date(dateString); |
|
|
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', |
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; |
|
|
const day = dateObj.getDate(); |
|
|
const month = monthNames[dateObj.getMonth()]; |
|
|
const year = dateObj.getFullYear(); |
|
|
|
|
|
return `${day} ${month} ${year}`; |
|
|
} |
|
|
|
|
|
|
|
|
saveUserBtn.addEventListener('click', () => { |
|
|
const newUser = usernameInput.value.trim(); |
|
|
if (!newUser) { |
|
|
alert('Please enter a username'); |
|
|
return; |
|
|
} |
|
|
|
|
|
currentUser = newUser; |
|
|
localStorage.setItem('currentUser', currentUser); |
|
|
|
|
|
|
|
|
if (!applicationsData[currentUser]) { |
|
|
applicationsData[currentUser] = {}; |
|
|
} |
|
|
|
|
|
|
|
|
if (!allUsersData[currentUser]) { |
|
|
allUsersData[currentUser] = { |
|
|
goal: dailyGoal, |
|
|
applications: {} |
|
|
}; |
|
|
localStorage.setItem('allUsersData', JSON.stringify(allUsersData)); |
|
|
} |
|
|
|
|
|
|
|
|
dailyGoal = allUsersData[currentUser].goal || 30; |
|
|
dailyGoalInput.value = dailyGoal; |
|
|
|
|
|
showCurrentUser(); |
|
|
loadDateData(); |
|
|
loadHistory(); |
|
|
updateStats(); |
|
|
|
|
|
|
|
|
if (currentUser === 'loliloli') { |
|
|
adminSection.classList.remove('hidden'); |
|
|
loadAllUsersData(); |
|
|
} else { |
|
|
adminSection.classList.add('hidden'); |
|
|
} |
|
|
}); |
|
|
|
|
|
saveGoalBtn.addEventListener('click', () => { |
|
|
const newGoal = parseInt(dailyGoalInput.value); |
|
|
if (isNaN(newGoal) || newGoal < 1) { |
|
|
alert('Please enter a valid goal (minimum 1)'); |
|
|
return; |
|
|
} |
|
|
|
|
|
dailyGoal = newGoal; |
|
|
goalDisplay.textContent = dailyGoal; |
|
|
progressText.textContent = `${currentCount}/${dailyGoal}`; |
|
|
|
|
|
|
|
|
if (currentUser) { |
|
|
if (!allUsersData[currentUser]) { |
|
|
allUsersData[currentUser] = { |
|
|
goal: dailyGoal, |
|
|
applications: {} |
|
|
}; |
|
|
} else { |
|
|
allUsersData[currentUser].goal = dailyGoal; |
|
|
} |
|
|
|
|
|
localStorage.setItem('allUsersData', JSON.stringify(allUsersData)); |
|
|
localStorage.setItem(`${currentUser}_dailyGoal`, dailyGoal); |
|
|
} |
|
|
|
|
|
updateCountDisplay(); |
|
|
}); |
|
|
|
|
|
increaseBtn.addEventListener('click', () => { |
|
|
currentCount++; |
|
|
updateCountDisplay(); |
|
|
}); |
|
|
|
|
|
decreaseBtn.addEventListener('click', () => { |
|
|
if (currentCount > 0) { |
|
|
currentCount--; |
|
|
updateCountDisplay(); |
|
|
} |
|
|
}); |
|
|
|
|
|
saveBtn.addEventListener('click', () => { |
|
|
if (!currentUser) { |
|
|
alert('Please set a username first'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (!applicationsData[currentUser]) { |
|
|
applicationsData[currentUser] = {}; |
|
|
} |
|
|
|
|
|
applicationsData[currentUser][selectedDate] = { |
|
|
count: currentCount, |
|
|
goal: dailyGoal |
|
|
}; |
|
|
localStorage.setItem('applicationsData', JSON.stringify(applicationsData)); |
|
|
|
|
|
|
|
|
if (!allUsersData[currentUser]) { |
|
|
allUsersData[currentUser] = { |
|
|
goal: dailyGoal, |
|
|
applications: {} |
|
|
}; |
|
|
} |
|
|
allUsersData[currentUser].applications[selectedDate] = { |
|
|
count: currentCount, |
|
|
goal: dailyGoal |
|
|
}; |
|
|
localStorage.setItem('allUsersData', JSON.stringify(allUsersData)); |
|
|
|
|
|
|
|
|
loadHistory(); |
|
|
updateStats(); |
|
|
|
|
|
|
|
|
if (currentUser === 'loliloli') { |
|
|
loadAllUsersData(); |
|
|
} |
|
|
|
|
|
|
|
|
deleteBtn.classList.remove('hidden'); |
|
|
|
|
|
alert('Applications saved successfully for ' + formatDate(selectedDate) + '!'); |
|
|
}); |
|
|
|
|
|
deleteBtn.addEventListener('click', () => { |
|
|
if (!confirm('Are you sure you want to delete this entry for ' + formatDate(selectedDate) + '?')) { |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (applicationsData[currentUser] && applicationsData[currentUser][selectedDate]) { |
|
|
delete applicationsData[currentUser][selectedDate]; |
|
|
localStorage.setItem('applicationsData', JSON.stringify(applicationsData)); |
|
|
} |
|
|
|
|
|
|
|
|
if (allUsersData[currentUser] && allUsersData[currentUser].applications[selectedDate]) { |
|
|
delete allUsersData[currentUser].applications[selectedDate]; |
|
|
localStorage.setItem('allUsersData', JSON.stringify(allUsersData)); |
|
|
} |
|
|
|
|
|
|
|
|
currentCount = 0; |
|
|
updateCountDisplay(); |
|
|
deleteBtn.classList.add('hidden'); |
|
|
|
|
|
|
|
|
loadHistory(); |
|
|
updateStats(); |
|
|
|
|
|
|
|
|
if (currentUser === 'loliloli') { |
|
|
loadAllUsersData(); |
|
|
} |
|
|
|
|
|
alert('Entry deleted successfully for ' + formatDate(selectedDate) + '!'); |
|
|
}); |
|
|
|
|
|
|
|
|
selectedDateInput.addEventListener('change', () => { |
|
|
selectedDate = selectedDateInput.value; |
|
|
loadDateData(); |
|
|
}); |
|
|
|
|
|
|
|
|
todayBtn.addEventListener('click', () => { |
|
|
selectedDate = new Date().toISOString().split('T')[0]; |
|
|
selectedDateInput.value = selectedDate; |
|
|
loadDateData(); |
|
|
}); |
|
|
|
|
|
yesterdayBtn.addEventListener('click', () => { |
|
|
selectedDate = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString().split('T')[0]; |
|
|
selectedDateInput.value = selectedDate; |
|
|
loadDateData(); |
|
|
}); |
|
|
|
|
|
|
|
|
sortDateBtn.addEventListener('click', () => { |
|
|
sortOrder = sortOrder === 'dateDesc' ? 'dateAsc' : 'dateDesc'; |
|
|
loadHistory(); |
|
|
}); |
|
|
|
|
|
sortCountBtn.addEventListener('click', () => { |
|
|
sortOrder = sortOrder === 'countDesc' ? 'countAsc' : 'countDesc'; |
|
|
loadHistory(); |
|
|
}); |
|
|
|
|
|
|
|
|
closeEditModal.addEventListener('click', closeEditModalHandler); |
|
|
cancelEdit.addEventListener('click', closeEditModalHandler); |
|
|
saveEdit.addEventListener('click', saveEditHandler); |
|
|
|
|
|
|
|
|
addNoteBtn.addEventListener('click', openNoteModal); |
|
|
closeNoteModal.addEventListener('click', closeNoteModalHandler); |
|
|
cancelNote.addEventListener('click', closeNoteModalHandler); |
|
|
saveNote.addEventListener('click', saveNoteHandler); |
|
|
|
|
|
|
|
|
refreshUsersBtn.addEventListener('click', loadAllUsersData); |
|
|
|
|
|
|
|
|
exportDataBtn.addEventListener('click', exportData); |
|
|
|
|
|
|
|
|
init(); |
|
|
</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=kasramojallal/application-tracker" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |