1
0
Fork 0

Import existing project

This commit is contained in:
Caileb 2025-05-26 12:42:36 -05:00
parent 7887817595
commit 80b0cc4939
125 changed files with 16980 additions and 0 deletions

858
develop/js/cc.js Normal file
View file

@ -0,0 +1,858 @@
/**
* Credit Card Tracker
* Tracks reward categories, spending limits, and payment history for credit cards
*/
document.addEventListener('DOMContentLoaded', () => {
// DOM Elements
const cardsContainer = document.getElementById('cards-container');
const emptyState = document.getElementById('empty-state');
const searchInput = document.getElementById('search-input');
const addCardBtn = document.getElementById('add-card-btn');
const addFirstCardBtn = document.getElementById('add-first-card-btn');
const cardModal = document.getElementById('card-modal');
const closeModalBtn = document.getElementById('close-modal');
const cardForm = document.getElementById('card-form');
const modalTitle = document.getElementById('modal-title');
const cardIdInput = document.getElementById('card-id');
const addCategoryBtn = document.getElementById('add-category-btn');
const categoriesContainer = document.getElementById('categories-container');
// Payment Modal Elements
const paymentModal = document.getElementById('payment-modal');
const closePaymentModalBtn = document.getElementById('close-payment-modal');
const paymentForm = document.getElementById('payment-form');
const paymentCardId = document.getElementById('payment-card-id');
const paymentCardName = document.getElementById('payment-card-name');
const paymentCategory = document.getElementById('payment-category');
const paymentAmount = document.getElementById('payment-amount');
const paymentDate = document.getElementById('payment-date');
// Create Payment History Modal - Will be added to DOM later
const paymentHistoryModal = document.createElement('div');
paymentHistoryModal.className = 'modal';
paymentHistoryModal.id = 'payment-history-modal';
// Initialize cards from localStorage
let cards = loadCards();
// Display cards or empty state
renderCards();
// Set today's date as default for payment date
paymentDate.valueAsDate = new Date();
// Event Listeners
addCardBtn.addEventListener('click', () => openAddCardModal());
addFirstCardBtn.addEventListener('click', () => openAddCardModal());
closeModalBtn.addEventListener('click', () => closeModal(cardModal));
closePaymentModalBtn.addEventListener('click', () => closeModal(paymentModal));
cardForm.addEventListener('submit', handleCardFormSubmit);
addCategoryBtn.addEventListener('click', () => addCategoryField('', '', ''));
paymentForm.addEventListener('submit', handlePaymentFormSubmit);
searchInput.addEventListener('input', handleSearch);
// Global event delegation for dynamically added card buttons
cardsContainer.addEventListener('click', handleCardActions);
// Check for monthly resets on page load
checkMonthlyResets();
// Initialize payment history modal
initPaymentHistoryModal();
/**
* Initialize payment history modal
*/
function initPaymentHistoryModal() {
paymentHistoryModal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title">Payment History</h2>
<button class="close-modal" id="close-history-modal">&times;</button>
</div>
<div id="payment-history-container">
<!-- Payment history will be loaded here -->
</div>
</div>
`;
document.body.appendChild(paymentHistoryModal);
// Add event listener to close button
document.getElementById('close-history-modal').addEventListener('click', () => {
closeModal(paymentHistoryModal);
});
}
/**
* Handle card action buttons via event delegation
*/
function handleCardActions(e) {
const target = e.target;
// Find closest card element
const cardElement = target.closest('.credit-card');
if (!cardElement) return;
const cardId = cardElement.dataset.id;
const card = cards.find(c => c.id === cardId);
if (!card) return;
// Payment button
if (target.closest('.payment-btn')) {
e.stopPropagation();
openPaymentModal(card);
return;
}
// Edit button
if (target.closest('.edit-btn')) {
e.stopPropagation();
openEditCardModal(card);
return;
}
// Delete button
if (target.closest('.delete-btn')) {
e.stopPropagation();
deleteCard(cardId);
return;
}
// View payment history (clicking on a category item)
const categoryItem = target.closest('.category-item');
if (categoryItem && categoryItem.dataset.categoryName) {
const categoryName = categoryItem.dataset.categoryName;
const category = card.categories.find(c => c.name === categoryName);
if (category) {
openPaymentHistoryModal(card, category);
}
}
}
/**
* Load cards from localStorage
*/
function loadCards() {
const storedCards = localStorage.getItem('creditCards');
return storedCards ? JSON.parse(storedCards) : [];
}
/**
* Save cards to localStorage
*/
function saveCards() {
localStorage.setItem('creditCards', JSON.stringify(cards));
}
/**
* Render all cards or empty state
*/
function renderCards() {
// Clear cards container except for the empty state
Array.from(cardsContainer.children).forEach(child => {
if (!child.classList.contains('empty-state')) {
child.remove();
}
});
// Show empty state if no cards
if (cards.length === 0) {
emptyState.style.display = 'block';
return;
}
// Hide empty state and render cards
emptyState.style.display = 'none';
// Filter cards if search input has value
let filteredCards = cards;
const searchTerm = searchInput.value.toLowerCase().trim();
if (searchTerm) {
filteredCards = cards.filter(card => {
const nameMatch = card.name.toLowerCase().includes(searchTerm);
const bankMatch = card.bank.toLowerCase().includes(searchTerm);
const categoryMatch = card.categories.some(cat =>
cat.name.toLowerCase().includes(searchTerm)
);
return nameMatch || bankMatch || categoryMatch;
});
}
// Render filtered cards
filteredCards.forEach(card => {
const cardElement = createCardElement(card);
cardsContainer.insertBefore(cardElement, emptyState);
});
}
/**
* Create a card element
*/
function createCardElement(card) {
const cardElement = document.createElement('div');
cardElement.className = 'credit-card';
cardElement.dataset.id = card.id;
// Calculate days until cycle resets
const today = new Date();
const currentDay = today.getDate();
const cycleDay = parseInt(card.statementDate);
let daysUntilReset;
if (currentDay === cycleDay) {
daysUntilReset = 0;
} else if (currentDay < cycleDay) {
daysUntilReset = cycleDay - currentDay;
} else {
// Calculate days until next month's cycle date
const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0).getDate();
daysUntilReset = (lastDayOfMonth - currentDay) + cycleDay;
}
// Create card HTML
cardElement.innerHTML = `
<div class="card-header">
<div class="card-details">
<h2 class="card-title">${card.name}</h2>
<div class="card-bank">${card.bank}${card.lastDigits ? ` •••• ${card.lastDigits}` : ''}</div>
</div>
<div class="card-actions">
<button type="button" class="action-btn payment-btn" title="Record Payment" aria-label="Record Payment">💰</button>
<button type="button" class="action-btn edit-btn" title="Edit Card" aria-label="Edit Card"></button>
<button type="button" class="action-btn delete-btn" title="Delete Card" aria-label="Delete Card">🗑</button>
</div>
</div>
<div class="card-info">
<div class="info-row">
<span class="info-label">Statement Cycle:</span>
<span class="info-value">Day ${card.statementDate}</span>
</div>
<div class="info-row">
<span class="info-label">Cycle Resets:</span>
<span class="info-value">${daysUntilReset === 0 ? 'Today' : `In ${daysUntilReset} day${daysUntilReset !== 1 ? 's' : ''}`}</span>
</div>
${card.expiryDate ? `
<div class="info-row">
<span class="info-label">Expires:</span>
<span class="info-value">${formatExpiryDate(card.expiryDate)}</span>
</div>
` : ''}
</div>
<div class="rewards-categories">
${card.categories.map(category => {
const payments = category.payments; // Assumed to be an array now
const spent = payments.reduce((total, p) => total + parseFloat(p.amount), 0);
const hasLimit = category.limit > 0;
const percentUsed = hasLimit ? (spent / category.limit) * 100 : 0;
const isNearLimit = percentUsed >= 75 && percentUsed < 100;
const isAtLimit = percentUsed >= 100;
const hasPayments = payments.length > 0;
// Calculate cash back amounts
const cashbackEarned = (spent * category.rate / 100).toFixed(2);
const maxCashback = hasLimit ? (category.limit * category.rate / 100).toFixed(2) : 0;
const cashbackDisplay = hasLimit ? ` ($${cashbackEarned}/$${maxCashback})` : ` ($${cashbackEarned})`;
return `
<div class="category-item ${hasPayments ? 'has-payments' : ''}" data-category-name="${category.name}">
<div class="category-header">
<span class="category-tag">${category.name}: ${category.rate}%</span>
${hasLimit ? `
<span class="info-value">$${spent.toFixed(2)} / $${category.limit.toFixed(2)}${cashbackDisplay}</span>
` : `<span class="info-value">$${spent.toFixed(2)}${cashbackDisplay}</span>`}
</div>
${hasLimit ? `
<div class="progress-bar ${isNearLimit ? 'near-limit' : ''} ${isAtLimit ? 'at-limit' : ''}">
<div class="progress-fill" style="width: ${Math.min(percentUsed, 100)}%"></div>
</div>
` : ''}
${hasPayments ? `<div class="view-payments-link">View ${payments.length} payment${payments.length !== 1 ? 's' : ''}</div>` : ''}
</div>
`;
}).join('')}
</div>
`;
return cardElement;
}
/**
* Format expiry date (YYYY-MM to MM/YYYY)
*/
function formatExpiryDate(dateString) {
const [year, month] = dateString.split('-');
return `${month}/${year}`;
}
/**
* Format a date for display (YYYY-MM-DD to MM/DD/YYYY)
*/
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric'
});
}
/**
* Open modal to add a new card
*/
function openAddCardModal() {
// Reset form
cardForm.reset();
cardIdInput.value = '';
modalTitle.textContent = 'Add New Card';
// Clear existing category fields and add one empty one
clearCategoryFields();
// Open modal
openModal(cardModal);
}
/**
* Open modal to edit an existing card
*/
function openEditCardModal(card) {
// Set form values
cardForm.reset();
cardIdInput.value = card.id;
document.getElementById('card-name').value = card.name;
document.getElementById('card-bank').value = card.bank;
document.getElementById('last-digits').value = card.lastDigits || '';
document.getElementById('expiry-date').value = card.expiryDate || '';
document.getElementById('statement-date').value = card.statementDate;
modalTitle.textContent = 'Edit Card';
// Clear existing category fields
clearCategoryFields();
// Add category fields for existing categories
if (card.categories.length === 0) {
} else {
card.categories.forEach(category => {
addCategoryField(category.name, category.rate, category.limit);
});
}
// Open modal
openModal(cardModal);
}
/**
* Open modal to record a payment
*/
function openPaymentModal(card) {
paymentCardId.value = card.id;
paymentCardName.textContent = card.name;
// Clear and populate category dropdown
paymentCategory.innerHTML = '';
card.categories.forEach(category => {
if (!category) return;
const option = document.createElement('option');
option.value = category.name;
option.textContent = `${category.name} (${category.rate}%)`;
// Add info about limit if one exists
if (category.limit > 0) {
const payments = category.payments; // Assumed to be an array now
const spent = payments.reduce((total, p) => total + parseFloat(p.amount), 0);
const remaining = Math.max(0, category.limit - spent);
option.textContent += ` - $${remaining.toFixed(2)} remaining`;
}
paymentCategory.appendChild(option);
});
// Reset other fields
paymentAmount.value = '';
paymentDate.valueAsDate = new Date();
document.getElementById('payment-note').value = '';
// Set modal title and button text for new payment
paymentModal.querySelector('.modal-title').textContent = 'Record Payment';
paymentModal.querySelector('.save-btn').textContent = 'Record Payment';
// Open modal
openModal(paymentModal);
}
/**
* Open payment history modal for a category
*/
function openPaymentHistoryModal(card, category) {
if (!card || !category) return;
const container = document.getElementById('payment-history-container');
const modalTitle = paymentHistoryModal.querySelector('.modal-title');
modalTitle.textContent = `${card.name} - ${category.name} Payments`;
container.innerHTML = ''; // Clear previous content
const payments = category.payments; // Assumed to be an array now
if (payments.length === 0) {
container.innerHTML = `
<div class="empty-state">
<h3>No payment history</h3>
<p>No payments have been recorded for this category yet.</p>
</div>
`;
} else {
const sortedPayments = [...payments].sort((a, b) =>
new Date(b.date) - new Date(a.date)
);
const historyHtml = `
<div class="payment-history-list">
<div class="payment-history-header">
<span>Date</span>
<span>Amount</span>
<span>Note</span>
<span>Actions</span>
</div>
${sortedPayments.map(payment => `
<div class="payment-history-item" data-payment-id="${payment.id}">
<span class="payment-date">${formatDate(payment.date)}</span>
<span class="payment-amount">$${parseFloat(payment.amount).toFixed(2)}</span>
<span class="payment-note">${payment.note || '-'}</span>
<div class="payment-actions">
<button type="button" class="action-btn edit-payment-btn" title="Edit Payment" aria-label="Edit Payment"></button>
<button type="button" class="action-btn delete-payment-btn" title="Delete Payment" aria-label="Delete Payment">🗑</button>
</div>
</div>
`).join('')}
</div>
`;
container.innerHTML = historyHtml;
// Add event listeners for edit and delete payment buttons
container.querySelectorAll('.edit-payment-btn').forEach(btn => {
btn.addEventListener('click', e => {
const paymentId = e.target.closest('.payment-history-item').dataset.paymentId;
const payment = payments.find(p => p.id === paymentId);
if (payment) {
editPayment(card.id, category.name, payment);
}
});
});
container.querySelectorAll('.delete-payment-btn').forEach(btn => {
btn.addEventListener('click', e => {
const paymentId = e.target.closest('.payment-history-item').dataset.paymentId;
const payment = payments.find(p => p.id === paymentId);
if (payment) {
deletePayment(card.id, category.name, payment.id);
}
});
});
}
// Open the modal
openModal(paymentHistoryModal);
}
/**
* Edit a payment
*/
function editPayment(cardId, categoryName, payment) {
// Set up payment modal for editing
paymentCardId.value = cardId;
const card = cards.find(c => c.id === cardId);
if (!card) return;
paymentCardName.textContent = card.name;
// Populate category dropdown (with only the selected category)
paymentCategory.innerHTML = '';
const category = card.categories.find(c => c.name === categoryName);
if (!category) return;
const option = document.createElement('option');
option.value = category.name;
option.textContent = `${category.name} (${category.rate}%)`;
paymentCategory.appendChild(option);
// Set payment details
paymentAmount.value = payment.amount;
paymentDate.value = payment.date;
document.getElementById('payment-note').value = payment.note || '';
// Add payment ID to the form
const paymentIdInput = document.getElementById('payment-id') || document.createElement('input');
paymentIdInput.type = 'hidden';
paymentIdInput.id = 'payment-id';
paymentIdInput.value = payment.id;
if (!document.getElementById('payment-id')) {
paymentForm.appendChild(paymentIdInput);
}
// Change modal title and button text
paymentModal.querySelector('.modal-title').textContent = 'Edit Payment';
paymentModal.querySelector('.save-btn').textContent = 'Update Payment';
// Close history modal and open payment modal with a slight delay for better transition
closeModal(paymentHistoryModal);
setTimeout(() => {
openModal(paymentModal);
}, 300);
}
/**
* Delete a payment
*/
function deletePayment(cardId, categoryName, paymentId) {
if (!confirm('Are you sure you want to delete this payment?')) return;
const cardIndex = cards.findIndex(c => c.id === cardId);
if (cardIndex === -1) return;
const categoryIndex = cards[cardIndex].categories.findIndex(c => c.name === categoryName);
if (categoryIndex === -1) return;
// Remove the payment from the array
const payments = cards[cardIndex].categories[categoryIndex].payments; // Assumed to be array
const paymentIndex = payments.findIndex(p => p.id === paymentId);
if (paymentIndex === -1) return;
// Get payment amount for confirmation
const amount = payments[paymentIndex].amount;
// Remove the payment
payments.splice(paymentIndex, 1);
// Save changes
saveCards();
// Close the history modal
closeModal(paymentHistoryModal);
// Show confirmation toast
showToast(`Payment of $${amount.toFixed(2)} has been deleted`);
// Re-render cards
renderCards();
}
/**
* Open a modal
*/
function openModal(modal) {
modal.classList.add('active');
document.body.style.overflow = 'hidden'; // Prevent background scrolling
// Focus first input in modal for better accessibility
setTimeout(() => {
const firstInput = modal.querySelector('input:not([type="hidden"])');
if (firstInput) firstInput.focus();
}, 100);
}
/**
* Close a modal
*/
function closeModal(modal) {
modal.classList.remove('active');
document.body.style.overflow = ''; // Restore scrolling
}
/**
* Clear all category fields
*/
function clearCategoryFields() {
categoriesContainer.innerHTML = '';
}
/**
* Add a category field to the form
*/
function addCategoryField(name = '', rate = '', limit = '') {
const categoryField = document.createElement('div');
categoryField.className = 'category-inputs';
categoryField.innerHTML = `
<input type="text" class="form-input category-name" placeholder="Category (e.g. Dining)" value="${name}">
<input type="number" class="form-input category-rate" placeholder="Rate %" step="0.1" min="0" value="${rate}">
<input type="number" class="form-input category-limit" placeholder="Limit $" step="0.01" min="0" value="${limit}">
<button type="button" class="action-btn delete-btn remove-category" title="Remove Category" aria-label="Remove Category">×</button>
`;
// Add event listener to remove button
categoryField.querySelector('.remove-category').addEventListener('click', function() {
// Allow removing the last category field
categoryField.remove();
});
categoriesContainer.appendChild(categoryField);
}
/**
* Handle card form submission
*/
function handleCardFormSubmit(e) {
e.preventDefault();
// Get form values
const cardId = cardIdInput.value || generateId();
const isEditing = !!cardIdInput.value;
const name = document.getElementById('card-name').value;
const bank = document.getElementById('card-bank').value;
const lastDigits = document.getElementById('last-digits').value;
const expiryDate = document.getElementById('expiry-date').value;
const statementDate = document.getElementById('statement-date').value;
// Get categories
const categories = [];
const categoryInputs = categoriesContainer.querySelectorAll('.category-inputs');
categoryInputs.forEach(input => {
const catName = input.querySelector('.category-name').value.trim();
const catRate = input.querySelector('.category-rate').value.trim();
const catLimit = input.querySelector('.category-limit').value.trim();
// Only add this category if any reward detail is provided
if (!catName && !catRate && !catLimit) {
return;
}
// Determine payments: Use existing if editing and found, otherwise default to empty array
let payments = [];
const existingCardData = isEditing ? cards.find(c => c.id === cardId) : null;
if (isEditing && existingCardData && existingCardData.categories) {
const existingCategoryData = existingCardData.categories.find(c => c.name === catName);
if (existingCategoryData && Array.isArray(existingCategoryData.payments)) {
payments = existingCategoryData.payments;
}
}
categories.push({
name: catName,
rate: catRate ? parseFloat(catRate) : 0,
limit: catLimit ? parseFloat(catLimit) : null,
payments: payments // Ensured to be an array
});
});
// Create card object
const card = {
id: cardId,
name,
bank,
lastDigits,
expiryDate,
statementDate,
categories, // categories array now guaranteed to have .payments arrays
createdAt: new Date().toISOString()
};
// Update or add card
const existingCardIndex = isEditing ? cards.findIndex(c => c.id === cardId) : -1;
if (existingCardIndex !== -1) {
// Preserve archived payments if editing
card.archivedPayments = cards[existingCardIndex].archivedPayments;
cards[existingCardIndex] = card;
} else {
cards.push(card);
}
// Save and render cards
saveCards();
renderCards();
// Show feedback toast
showToast(isEditing ? 'Card updated successfully' : 'Card added successfully');
// Close modal
closeModal(cardModal);
}
/**
* Handle payment form submission
*/
function handlePaymentFormSubmit(e) {
e.preventDefault();
// Get form values
const cardId = paymentCardId.value;
const categoryName = paymentCategory.value;
const amount = parseFloat(paymentAmount.value);
const date = paymentDate.value;
const note = document.getElementById('payment-note').value;
const paymentId = document.getElementById('payment-id')?.value;
// Find card and category
const cardIndex = cards.findIndex(c => c.id === cardId);
if (cardIndex === -1) return;
const categoryIndex = cards[cardIndex].categories.findIndex(c => c.name === categoryName);
if (categoryIndex === -1) return;
// Check if editing existing payment or adding new one
const payments = cards[cardIndex].categories[categoryIndex].payments; // Assumed to be array
if (paymentId) {
// Find the payment
const paymentIndex = payments.findIndex(p => p.id === paymentId);
if (paymentIndex !== -1) {
// Update payment
payments[paymentIndex] = {
id: paymentId,
amount,
date,
note,
createdAt: payments[paymentIndex].createdAt, // Keep original creation date
updatedAt: new Date().toISOString()
};
// Show toast
showToast('Payment updated successfully');
}
} else {
// Add new payment
const payment = {
id: generateId(),
amount,
date,
note,
createdAt: new Date().toISOString()
};
payments.push(payment);
// Show toast
showToast(`Payment of $${amount.toFixed(2)} recorded`);
}
// Save and render cards
saveCards();
renderCards();
// Remove payment ID if it exists
if (document.getElementById('payment-id')) {
document.getElementById('payment-id').remove();
}
// Reset modal title
paymentModal.querySelector('.modal-title').textContent = 'Record Payment';
// Close modal
closeModal(paymentModal);
}
/**
* Delete a card
*/
function deleteCard(cardId) {
if (!confirm('Are you sure you want to delete this card?')) return;
const cardName = cards.find(card => card.id === cardId)?.name || 'Card';
cards = cards.filter(card => card.id !== cardId);
saveCards();
renderCards();
// Show feedback toast
showToast(`${cardName} has been deleted`);
}
/**
* Show a toast notification
*/
function showToast(message) {
// Remove existing toast if any
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
// Create toast element
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
// Trigger animation
setTimeout(() => toast.classList.add('show'), 10);
// Auto remove after 3 seconds
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
}
/**
* Handle search
*/
function handleSearch() {
renderCards();
}
/**
* Generate a unique ID
*/
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}
/**
* Check for monthly cycle resets
*/
function checkMonthlyResets() {
const today = new Date();
const lastCheck = localStorage.getItem('lastCycleCheck');
// If we haven't checked today
if (!lastCheck || new Date(lastCheck).toDateString() !== today.toDateString()) {
const currentDay = today.getDate();
// Check each card for cycle reset
cards.forEach(card => {
const cycleDay = parseInt(card.statementDate);
// If today is the cycle reset day
if (currentDay === cycleDay) {
// Mark payments as from previous cycle
card.categories.forEach(category => {
// Only archive if there are payments
if (category.payments.length > 0) {
// Create an archive if none exists
if (!card.archivedPayments) {
card.archivedPayments = [];
}
// Archive current cycle payments
const cycleData = {
date: today.toISOString(),
categories: [{
name: category.name,
rate: category.rate,
payments: [...category.payments]
}]
};
// Add to archived payments
card.archivedPayments.push(cycleData);
// Clear current payments
category.payments = [];
}
});
}
});
// Save changes
saveCards();
// Update last check date
localStorage.setItem('lastCycleCheck', today.toISOString());
}
}
});