/**
* @fileoverview Manages loading, rendering, and user interactions for job application cards.
*/
import '../components/job-card.js';
import { deleteApplication } from '../controllers/deleteApplication.js';
/**
* @description Initialize application data and render cards on first visit.
* @listens DOMContentLoaded
*/
document.addEventListener('DOMContentLoaded', async () => {
// Seed localStorage from JSON file if empty
if (!localStorage.getItem('applications')) {
const jobs = await fetchApplications();
localStorage.setItem('applications', JSON.stringify(jobs));
}
// Retrieve and render stored applications
const jobs = JSON.parse(localStorage.getItem('applications'));
renderCards(jobs);
setupModal();
// Listen for new applications being created
window.addEventListener('applicationCreated', () => {
const updatedJobs = JSON.parse(localStorage.getItem('applications')) || [];
renderCards(updatedJobs);
});
});
/**
* @description Fetches initial job applications from a JSON data file.
* @async
* @returns {Promise<Object[]>} Array of job application objects (empty array on error).
*/
async function fetchApplications() {
try {
// Try absolute path first (for Netlify), then relative path (for local/test)
let response = await fetch('/data/applications.json');
if (!response.ok) {
response = await fetch('../data/applications.json');
}
if (!response.ok) throw new Error('Failed to load applications.json');
return await response.json();
} catch (error) {
console.error('Error loading applications:', error);
return [];
}
}
/**
* @description Render job application cards based on provided jobs and current filter.
* @param {Object[]} [jobs] - Array of job objects to render; defaults to localStorage data.
*/
function renderCards(jobs) {
// Fallback to local storage if no jobs are passed in
if (!jobs) {
jobs = JSON.parse(localStorage.getItem('applications')) || [];
}
const container = document.getElementById('applicationCardsContainer');
container.innerHTML = '';
// Determine current filter and total count for header
const currentFilter = localStorage.getItem('filterPreference') || 'All';
const totalApplications = JSON.parse(localStorage.getItem('applications')) || [];
const header = document.querySelector('.main-header h1');
if (header) {
if (currentFilter === 'All') {
header.textContent = `All Applications (${jobs.length})`;
} else {
header.textContent = `${currentFilter} Applications (${jobs.length} of ${totalApplications.length})`;
}
}
// Show empty state if there are no jobs
if (jobs.length === 0) {
container.innerHTML = `
<div class="empty-state">
<span class="material-symbols-outlined">work_off</span>
<h3>No applications yet</h3>
<p>Start tracking your job applications by adding your first one!</p>
<a href="add_application.html" class="add-btn">Add Your First Application</a>
</div>
`;
return;
}
// Create and append a card for each job
for (const job of jobs) {
const wrapper = document.createElement('div');
wrapper.classList.add('application-wrapper');
wrapper.dataset.id = job.id;
const cardElem = document.createElement('job-app-card');
cardElem.data = job;
cardElem.dataset.id = job.id;
// Open details modal when card clicked (unless delete button)
cardElem.addEventListener('click', (e) => {
if (e.composedPath().some(el => el.classList?.contains('delete-btn'))) return;
openModal(job);
});
// Handle delete events bubbled from the custom element
cardElem.addEventListener('delete-card', (e) => {
const appId = e.detail.id;
if (confirm('Are you sure you want to delete this application?')) {
deleteApplication(appId);
// Re-render after deletion animation delay
setTimeout(() => {
const updated = JSON.parse(localStorage.getItem('applications')) || [];
renderCards(updated);
}, 310);
}
});
wrapper.appendChild(cardElem);
container.appendChild(wrapper);
}
}
/**
* @description Configure modal open/close behaviors (click outside, close button, Escape key).
*/
function setupModal() {
const modal = document.getElementById('appDetailsModal');
const closeBtn = modal.querySelector('.close-btn');
closeBtn.addEventListener('click', () => {
modal.classList.remove('show');
});
// Close when clicking the backdrop
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('show');
}
});
// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
modal.classList.remove('show');
}
});
}
/**
* @description Populate and display the details modal for a job application.
* @param {Object} data - The job application data to show.
*/
function openModal(data) {
window.currentlyViewingJob = data;
const modal = document.getElementById('appDetailsModal');
modal.classList.add('show');
// Fill modal fields with job data
document.getElementById('modal-title').textContent = data.jobPosition;
document.getElementById('modal-company').textContent = data.company;
document.getElementById('modal-type').textContent = data.positionType;
document.getElementById('modal-salary').textContent = `$${data.salary?.toLocaleString() || '-'}`;
document.getElementById('modal-location').textContent = data.location;
document.getElementById('modal-date').textContent = data.dateApplied;
document.getElementById('modal-status').textContent = data.status;
document.getElementById('modal-contact').textContent = data.contact?.email || '';
document.getElementById('modal-phone').textContent = data.contact?.phoneNumber || '';
document.getElementById('modal-notes').textContent = data.notes || '';
// Populate list of important dates if present
const ul = document.getElementById('modal-important-dates');
ul.innerHTML = '';
if (data.importantDates) {
for (const [label, value] of Object.entries(data.importantDates)) {
const li = document.createElement('li');
li.textContent = `${label}: ${value}`;
ul.appendChild(li);
}
}
}
// Store current job in localStorage and navigate to edit form
document.getElementById('editApplicationBtn').addEventListener('click', () => {
if (!window.currentlyViewingJob) return;
localStorage.setItem('editJobData', JSON.stringify(window.currentlyViewingJob));
window.location.href = 'add_application.html';
});
/**
* @description Update a specific job-app-card element in the DOM after an application update.
* @param {string} applicationId - The ID of the updated application.
*/
export function updateCardInDOM(applicationId) {
const cardElement = document.querySelector(`job-app-card[data-id="${applicationId}"]`);
if (!cardElement) return;
const cards = JSON.parse(localStorage.getItem('applications')) || [];
const updatedCard = cards.find(card => card.id === applicationId);
if (!updatedCard) return;
// Apply new data and highlight the updated card
cardElement.data = updatedCard;
const wrapper = cardElement.closest('.application-wrapper');
if (wrapper) {
wrapper.style.transition = 'background-color 0.3s ease';
wrapper.style.backgroundColor = 'rgba(100, 255, 100, 0.2)';
setTimeout(() => {
wrapper.style.backgroundColor = '';
}, 300);
}
}
// Expose renderCards globally for external usage
window.renderCards = renderCards;
// Mock data for testing and demonstration
const mockApplications = [
{
id: 'mock-1',
company: 'Apple',
jobPosition: 'Machine Learning Engineer',
jobType: 'Full-Time',
salary: 265000,
location: 'San Francisco, CA',
contact: {
name: 'Mark Spears',
email: 'mark.spears@apple.com',
phoneNumber: '417-525-2998'
},
notes: 'Waiting to hear back from the recruiter screen I did last week',
dateApplied: '2025-03-19',
importantDates: {
'Phone Interview': '2025-04-05',
'Technical Interview': '2025-04-12'
},
status: 'Applied',
bookmarked: true,
logo: 'https://upload.wikimedia.org/wikipedia/commons/f/fa/Apple_logo_black.svg'
},
{
id: 'mock-2',
company: 'Google',
jobPosition: 'Software Developer',
jobType: 'Full-Time',
salary: 120000,
location: 'San Diego, CA',
contact: {
name: 'Alex Jobs',
email: 'alex.jobs@google.com',
phoneNumber: '999-999-9999'
},
notes: 'Have finished the coding challenge, waiting for results',
dateApplied: '2025-04-19',
importantDates: {
'Challenge Submission': '2025-04-24'
},
status: 'Interviewing',
bookmarked: false,
logo: 'https://upload.wikimedia.org/wikipedia/commons/2/2f/Google_2015_logo.svg'
},
{
id: 'mock-3',
company: 'Netflix',
jobPosition: 'Product Designer',
jobType: 'Full-Time',
salary: 145000,
location: 'Chicago, IL',
contact: {
name: 'Joe Davis',
email: 'joe.davis@netflix.com',
phoneNumber: '888-888-8888'
},
notes: 'Have received the offer, reviewing benefits and stock options',
dateApplied: '2025-03-29',
importantDates: {
'Offer Deadline': '2025-05-20'
},
status: 'Offer',
bookmarked: true,
logo: 'https://upload.wikimedia.org/wikipedia/commons/0/08/Netflix_2015_logo.svg'
},
{
id: 'mock-4',
company: 'Microsoft',
jobPosition: 'Frontend Developer',
jobType: 'Full-Time',
salary: 135000,
location: 'Seattle, WA',
contact: {
name: 'Sarah Chen',
email: 'sarah.chen@microsoft.com',
phoneNumber: '206-555-0123'
},
notes: 'Completed final round interviews, waiting for decision',
dateApplied: '2025-02-15',
importantDates: {
'Final Interview': '2025-03-10',
'Decision Expected': '2025-03-25'
},
status: 'Screening',
bookmarked: false,
logo: 'https://upload.wikimedia.org/wikipedia/commons/9/96/Microsoft_logo_%282012%29.svg'
},
{
id: 'mock-5',
company: 'Amazon',
jobPosition: 'DevOps Engineer',
jobType: 'Full-Time',
salary: 155000,
location: 'Austin, TX',
contact: {
name: 'Mike Rodriguez',
email: 'mike.rodriguez@amazon.com',
phoneNumber: '512-555-0456'
},
notes: 'Added to wishlist, planning to apply next month',
dateApplied: '2025-05-01',
importantDates: {},
status: 'Wishlist',
bookmarked: true,
logo: 'https://upload.wikimedia.org/wikipedia/commons/a/a9/Amazon_logo.svg'
},
{
id: 'mock-6',
company: 'Meta',
jobPosition: 'Data Scientist',
jobType: 'Full-Time',
salary: 180000,
location: 'Menlo Park, CA',
contact: {
name: 'Emily Wang',
email: 'emily.wang@meta.com',
phoneNumber: '650-555-0789'
},
notes: 'Applied but decided to withdraw due to company changes',
dateApplied: '2025-01-20',
importantDates: {
'Withdrawal Date': '2025-02-05'
},
status: 'Withdrawn',
bookmarked: false,
logo: 'https://upload.wikimedia.org/wikipedia/commons/7/7b/Meta_Platforms_Inc._logo.svg'
},
{
id: 'mock-7',
company: 'Tesla',
jobPosition: 'Software Engineer',
jobType: 'Full-Time',
salary: 140000,
location: 'Palo Alto, CA',
contact: {
name: 'David Liu',
email: 'david.liu@tesla.com',
phoneNumber: '650-555-0321'
},
notes: 'Application was rejected after technical interview',
dateApplied: '2025-01-10',
importantDates: {
'Technical Interview': '2025-02-01',
'Rejection Date': '2025-02-10'
},
status: 'Rejected',
bookmarked: false,
logo: 'https://upload.wikimedia.org/wikipedia/commons/b/bb/Tesla_T_symbol.svg'
},
{
id: 'mock-8',
company: 'Spotify',
jobPosition: 'Backend Engineer',
jobType: 'Full-Time',
salary: 125000,
location: 'Remote',
contact: {
name: 'Anna Johnson',
email: 'anna.johnson@spotify.com',
phoneNumber: '555-123-4567'
},
notes: 'Applied 3 months ago, no response from recruiter',
dateApplied: '2024-12-15',
importantDates: {},
status: 'Ghosted',
bookmarked: false,
logo: 'https://upload.wikimedia.org/wikipedia/commons/1/19/Spotify_logo_without_text.svg'
}
];
/**
* @description Loads mock application data into localStorage for testing purposes.
* This function will add mock applications to existing data instead of replacing it.
*/
function loadMockData() {
console.log('Adding mock data to existing applications...');
// Get existing applications
const existingApplications = JSON.parse(localStorage.getItem('applications')) || [];
// Filter out mock applications that already exist to avoid duplicates
const existingIds = existingApplications.map(app => app.id);
const newMockApplications = mockApplications.filter(app => !existingIds.includes(app.id));
// Combine existing and new mock applications
const combinedApplications = [...existingApplications, ...newMockApplications];
// Save combined data
localStorage.setItem('applications', JSON.stringify(combinedApplications));
console.log(`Added ${newMockApplications.length} new mock applications (${existingApplications.length} existing + ${newMockApplications.length} new = ${combinedApplications.length} total)`);
// Re-render the cards immediately with the combined data
renderCards(combinedApplications);
// Show success message
if (newMockApplications.length > 0) {
alert(`Successfully added ${newMockApplications.length} new mock applications! Total applications: ${combinedApplications.length}`);
} else {
alert(`All mock applications already exist. No new applications added. Total applications: ${combinedApplications.length}`);
}
}
/**
* @description Clears all application data from localStorage.
*/
function clearAllData() {
if (confirm('Are you sure you want to clear all application data? This cannot be undone.')) {
localStorage.removeItem('applications');
localStorage.removeItem('filterPreference');
localStorage.removeItem('sortPreference');
console.log('All application data cleared');
// Re-render with empty data
renderCards([]);
alert('All application data has been cleared!');
}
}
// Expose functions globally for console access and external usage
window.loadMockData = loadMockData;
window.clearAllData = clearAllData;
window.mockApplications = mockApplications;
window.openModal = openModal;
window.setupModal = setupModal;