crypto-port-logs / index.html
bibbler's picture
make date field editable and add column for ROI with computation calc for profit factor, ie: 2x, 3x and etc. also needs ability to export/download and import portfolio table - Initial Deployment
c1bea79 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crypto Portfolio Tracker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="https://unpkg.com/feather-icons"></script>
<style>
.profit {
background-color: rgba(74, 222, 128, 0.2);
color: #16a34a;
}
.loss {
background-color: rgba(248, 113, 113, 0.2);
color: #dc2626;
}
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="mb-8">
<h1 class="text-3xl font-bold text-gray-800">Crypto Portfolio Tracker</h1>
<p class="text-gray-600">Track your cryptocurrency investments with real-time data</p>
</header>
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<div class="flex flex-col md:flex-row gap-4 mb-6">
<div class="flex-1">
<label for="coinSearch" class="block text-sm font-medium text-gray-700 mb-1">Search Cryptocurrency</label>
<div class="relative">
<input type="text" id="coinSearch" placeholder="Search by name or symbol..."
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<div id="coinDropdown" class="absolute z-10 mt-1 w-full bg-white border border-gray-300 rounded-md shadow-lg hidden max-h-60 overflow-y-auto">
<!-- Coin list will be populated here -->
</div>
</div>
</div>
<div class="flex-1">
<label for="purchaseDate" class="block text-sm font-medium text-gray-700 mb-1">Purchase Date</label>
<input type="date" id="purchaseDate"
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="flex-1">
<label for="quantity" class="block text-sm font-medium text-gray-700 mb-1">Quantity</label>
<input type="number" id="quantity" placeholder="0.00" step="0.00000001"
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="flex-1">
<label for="avgCost" class="block text-sm font-medium text-gray-700 mb-1">Avg. Cost (Optional)</label>
<input type="number" id="avgCost" placeholder="0.00" step="0.01"
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div class="flex items-end">
<button id="addCoin" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center gap-2">
<i data-feather="plus"></i> Add Coin
</button>
</div>
</div>
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold text-gray-800">Your Portfolio</h2>
<div class="flex items-center gap-2">
<span id="apiCounter" class="text-sm text-gray-500">API calls today: 0</span>
<button id="loadFromCache" class="px-3 py-1 text-sm bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors flex items-center gap-1">
<i data-feather="refresh-cw" class="w-4 h-4"></i> Load Saved
</button>
</div>
</div>
<div class="table-container">
<table id="portfolioTable" class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ticker</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Price</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Qty</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Current Cost</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Avg Cost</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Avg Value</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Diff ($)</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Diff (%)</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Days Since</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody id="portfolioBody" class="bg-white divide-y divide-gray-200">
<!-- Portfolio items will be populated here -->
</tbody>
</table>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold text-gray-800 mb-4">Portfolio Summary</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-blue-50 p-4 rounded-lg">
<h3 class="text-sm font-medium text-blue-800">Total Investment</h3>
<p id="totalInvestment" class="text-2xl font-bold text-blue-600">$0.00</p>
</div>
<div class="bg-green-50 p-4 rounded-lg">
<h3 class="text-sm font-medium text-green-800">Current Value</h3>
<p id="currentValue" class="text-2xl font-bold text-green-600">$0.00</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="text-sm font-medium text-gray-800">Profit/Loss</h3>
<p id="profitLoss" class="text-2xl font-bold">$0.00</p>
</div>
</div>
</div>
</div>
<script>
// Initialize feather icons
feather.replace();
AOS.init();
// API configuration
const API_KEY = '48776ed0-aacd-4181-bd9f-e1003791b921';
const API_BASE_URL = 'https://api.livecoinwatch.com';
let apiCallCount = 0;
let lastApiCallDate = new Date().toDateString();
// Sample cryptocurrency data (would be fetched from API in production)
const sampleCoins = [
{ code: 'BTC', name: 'Bitcoin' },
{ code: 'ETH', name: 'Ethereum' },
{ code: 'BNB', name: 'Binance Coin' },
{ code: 'SOL', name: 'Solana' },
{ code: 'XRP', name: 'XRP' },
{ code: 'ADA', name: 'Cardano' },
{ code: 'DOGE', name: 'Dogecoin' },
{ code: 'DOT', name: 'Polkadot' },
{ code: 'SHIB', name: 'Shiba Inu' },
{ code: 'MATIC', name: 'Polygon' },
{ code: 'AVAX', name: 'Avalanche' },
{ code: 'LTC', name: 'Litecoin' },
{ code: 'UNI', name: 'Uniswap' },
{ code: 'LINK', name: 'Chainlink' },
{ code: 'ATOM', name: 'Cosmos' }
];
// DOM elements
const coinSearch = document.getElementById('coinSearch');
const coinDropdown = document.getElementById('coinDropdown');
const purchaseDate = document.getElementById('purchaseDate');
const quantity = document.getElementById('quantity');
const avgCost = document.getElementById('avgCost');
const addCoin = document.getElementById('addCoin');
const loadFromCache = document.getElementById('loadFromCache');
const portfolioBody = document.getElementById('portfolioBody');
const apiCounter = document.getElementById('apiCounter');
const totalInvestment = document.getElementById('totalInvestment');
const currentValue = document.getElementById('currentValue');
const profitLoss = document.getElementById('profitLoss');
// Set default date to today
const today = new Date().toISOString().split('T')[0];
purchaseDate.value = today;
// Initialize from localStorage
loadPortfolioFromStorage();
updateSummary();
// Event listeners
coinSearch.addEventListener('input', handleCoinSearch);
coinSearch.addEventListener('focus', () => {
if (coinSearch.value.length > 0) {
coinDropdown.classList.remove('hidden');
}
});
coinSearch.addEventListener('blur', () => {
setTimeout(() => coinDropdown.classList.add('hidden'), 200);
});
addCoin.addEventListener('click', addCoinToPortfolio);
loadFromCache.addEventListener('click', loadPortfolioFromStorage);
// Functions
function handleCoinSearch() {
const searchTerm = coinSearch.value.toLowerCase();
if (searchTerm.length === 0) {
coinDropdown.classList.add('hidden');
return;
}
const filteredCoins = sampleCoins.filter(coin =>
coin.name.toLowerCase().includes(searchTerm) ||
coin.code.toLowerCase().includes(searchTerm)
);
renderCoinDropdown(filteredCoins);
}
function renderCoinDropdown(coins) {
coinDropdown.innerHTML = '';
if (coins.length === 0) {
coinDropdown.innerHTML = '<div class="px-4 py-2 text-sm text-gray-500">No coins found</div>';
coinDropdown.classList.remove('hidden');
return;
}
coins.forEach(coin => {
const coinItem = document.createElement('div');
coinItem.className = 'px-4 py-2 hover:bg-gray-100 cursor-pointer flex justify-between items-center';
coinItem.innerHTML = `
<span>${coin.name} (${coin.code})</span>
<button class="text-blue-500 hover:text-blue-700" data-code="${coin.code}" data-name="${coin.name}">
<i data-feather="plus" class="w-4 h-4"></i>
</button>
`;
coinItem.querySelector('button').addEventListener('click', (e) => {
e.stopPropagation();
coinSearch.value = `${coin.name} (${coin.code})`;
coinDropdown.classList.add('hidden');
});
coinDropdown.appendChild(coinItem);
});
coinDropdown.classList.remove('hidden');
feather.replace();
}
async function addCoinToPortfolio() {
const coinMatch = coinSearch.value.match(/\((.*?)\)/);
if (!coinMatch || !coinMatch[1]) {
alert('Please select a valid cryptocurrency from the dropdown');
return;
}
const coinCode = coinMatch[1];
const coinName = coinSearch.value.split('(')[0].trim();
const qty = parseFloat(quantity.value);
const date = purchaseDate.value;
const avgCostValue = avgCost.value ? parseFloat(avgCost.value) : null;
if (!qty || qty <= 0) {
alert('Please enter a valid quantity');
return;
}
// Get current price
try {
const priceData = await getCoinPrice(coinCode);
const price = priceData.rate;
// For historical date, get historical price
let historicalPrice = price;
let daysSince = 0;
if (date !== today) {
const historicalData = await getHistoricalPrice(coinCode, date);
historicalPrice = historicalData.rate;
const purchaseDateObj = new Date(date);
const todayObj = new Date();
daysSince = Math.floor((todayObj - purchaseDateObj) / (1000 * 60 * 60 * 24));
}
// Create portfolio item
const currentCost = qty * price;
const avgValue = avgCostValue ? qty * avgCostValue : currentCost;
const diffDollar = currentCost - avgValue;
const diffPercent = ((currentCost - avgValue) / avgValue) * 100;
const status = diffDollar >= 0 ? 'Profit' : 'Loss';
const portfolioItem = {
id: Date.now(),
coinCode,
coinName,
date,
price,
historicalPrice,
qty,
currentCost,
avgCost: avgCostValue,
avgValue,
diffDollar,
diffPercent,
status,
daysSince
};
addPortfolioRow(portfolioItem);
savePortfolioToStorage();
updateSummary();
// Reset form
coinSearch.value = '';
quantity.value = '';
avgCost.value = '';
purchaseDate.value = today;
} catch (error) {
console.error('Error adding coin:', error);
alert('Failed to fetch coin data. Please try again.');
}
}
function addPortfolioRow(item) {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
row.dataset.id = item.id;
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">${item.coinName}</div>
<div class="ml-2 text-sm text-gray-500">${item.coinCode}</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formatDate(item.date)}</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<span class="text-sm text-gray-900">$${item.price.toFixed(2)}</span>
<button class="ml-2 text-blue-500 hover:text-blue-700 refresh-price" data-coin="${item.coinCode}">
<i data-feather="refresh-cw" class="w-4 h-4"></i>
</button>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<input type="number" value="${item.qty}" step="0.00000001" class="qty-input w-24 px-2 py-1 border border-gray-300 rounded-md text-sm text-gray-900">
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">$${item.currentCost.toFixed(2)}</td>
<td class="px-6 py-4 whitespace-nowrap">
<input type="number" value="${item.avgCost || ''}" step="0.01" class="avg-cost-input w-24 px-2 py-1 border border-gray-300 rounded-md text-sm text-gray-900">
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">$${item.avgValue.toFixed(2)}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm ${item.diffDollar >= 0 ? 'text-green-600' : 'text-red-600'}">
$${item.diffDollar.toFixed(2)}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm ${item.diffPercent >= 0 ? 'text-green-600' : 'text-red-600'}">
${item.diffPercent.toFixed(2)}%
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-1 text-xs rounded-full ${item.status === 'Profit' ? 'profit' : 'loss'}">
${item.status}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${item.daysSince}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<button class="text-red-500 hover:text-red-700 delete-item">
<i data-feather="trash-2" class="w-4 h-4"></i>
</button>
</td>
`;
portfolioBody.appendChild(row);
feather.replace();
// Add event listeners for the new row
row.querySelector('.refresh-price').addEventListener('click', async (e) => {
const coinCode = e.target.closest('button').dataset.coin;
try {
const priceData = await getCoinPrice(coinCode);
const priceCell = e.target.closest('td');
priceCell.querySelector('span').textContent = `$${priceData.rate.toFixed(2)}`;
// Update the entire row data
updateRowData(row, priceData.rate);
savePortfolioToStorage();
updateSummary();
} catch (error) {
console.error('Error refreshing price:', error);
alert('Failed to refresh price. Please try again.');
}
});
row.querySelector('.qty-input').addEventListener('change', (e) => {
const newQty = parseFloat(e.target.value);
if (!isNaN(newQty) && newQty > 0) {
updateRowData(row, null, newQty);
savePortfolioToStorage();
updateSummary();
}
});
row.querySelector('.avg-cost-input').addEventListener('change', (e) => {
const newAvgCost = e.target.value ? parseFloat(e.target.value) : null;
updateRowData(row, null, null, newAvgCost);
savePortfolioToStorage();
updateSummary();
});
row.querySelector('.delete-item').addEventListener('click', () => {
row.remove();
savePortfolioToStorage();
updateSummary();
});
}
function updateRowData(row, newPrice, newQty, newAvgCost) {
const cells = row.querySelectorAll('td');
const currentPrice = newPrice !== null ? newPrice : parseFloat(cells[2].querySelector('span').textContent.replace('$', ''));
const qty = newQty !== null ? newQty : parseFloat(cells[3].querySelector('input').value);
const avgCost = newAvgCost !== null ? newAvgCost : (cells[5].querySelector('input').value ? parseFloat(cells[5].querySelector('input').value) : null);
const currentCost = qty * currentPrice;
const avgValue = avgCost ? qty * avgCost : currentCost;
const diffDollar = currentCost - avgValue;
const diffPercent = ((currentCost - avgValue) / avgValue) * 100;
const status = diffDollar >= 0 ? 'Profit' : 'Loss';
// Update cells
if (newPrice !== null) {
cells[2].querySelector('span').textContent = `$${currentPrice.toFixed(2)}`;
}
if (newQty !== null) {
cells[3].querySelector('input').value = qty;
}
if (newAvgCost !== null) {
cells[5].querySelector('input').value = avgCost !== null ? avgCost : '';
}
cells[4].textContent = `$${currentCost.toFixed(2)}`;
cells[6].textContent = `$${avgValue.toFixed(2)}`;
cells[7].textContent = `$${diffDollar.toFixed(2)}`;
cells[7].className = `px-6 py-4 whitespace-nowrap text-sm ${diffDollar >= 0 ? 'text-green-600' : 'text-red-600'}`;
cells[8].textContent = `${diffPercent.toFixed(2)}%`;
cells[8].className = `px-6 py-4 whitespace-nowrap text-sm ${diffPercent >= 0 ? 'text-green-600' : 'text-red-600'}`;
cells[9].innerHTML = `<span class="px-2 py-1 text-xs rounded-full ${status === 'Profit' ? 'profit' : 'loss'}">${status}</span>`;
}
async function getCoinPrice(coinCode) {
// Check if we've already called the API today
const currentDate = new Date().toDateString();
if (currentDate !== lastApiCallDate) {
apiCallCount = 0;
lastApiCallDate = currentDate;
}
// Check localStorage for cached price
const cacheKey = `price_${coinCode}_${new Date().toDateString()}`;
const cachedPrice = localStorage.getItem(cacheKey);
if (cachedPrice) {
return JSON.parse(cachedPrice);
}
// Call API
const response = await fetch(`${API_BASE_URL}/coins/single`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
body: JSON.stringify({
code: coinCode,
currency: 'USD',
meta: false
})
});
if (!response.ok) {
throw new Error('Failed to fetch coin price');
}
const data = await response.json();
// Update API call counter
apiCallCount++;
updateApiCounter();
// Cache the result
localStorage.setItem(cacheKey, JSON.stringify({
rate: data.rate,
timestamp: Date.now()
}));
return { rate: data.rate };
}
async function getHistoricalPrice(coinCode, date) {
// Check localStorage for cached historical price
const cacheKey = `historical_${coinCode}_${date}`;
const cachedPrice = localStorage.getItem(cacheKey);
if (cachedPrice) {
return JSON.parse(cachedPrice);
}
// Convert date to timestamp (end of day)
const dateObj = new Date(date);
dateObj.setHours(23, 59, 59, 999);
const timestamp = Math.floor(dateObj.getTime() / 1000);
// Call API
const response = await fetch(`${API_BASE_URL}/coins/single/history`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': API_KEY
},
body: JSON.stringify({
code: coinCode,
currency: 'USD',
start: timestamp,
end: timestamp,
meta: false
})
});
if (!response.ok) {
throw new Error('Failed to fetch historical price');
}
const data = await response.json();
// Update API call counter
apiCallCount++;
updateApiCounter();
// Cache the result
if (data.history && data.history.length > 0) {
localStorage.setItem(cacheKey, JSON.stringify({
rate: data.history[0].rate,
timestamp: Date.now()
}));
return { rate: data.history[0].rate };
} else {
throw new Error('No historical data available');
}
}
function updateApiCounter() {
apiCounter.textContent = `API calls today: ${apiCallCount}`;
}
function formatDate(dateString) {
const options = { year: 'numeric', month: 'short', day: 'numeric' };
return new Date(dateString).toLocaleDateString(undefined, options);
}
function savePortfolioToStorage() {
const portfolioItems = [];
document.querySelectorAll('#portfolioBody tr').forEach(row => {
const cells = row.querySelectorAll('td');
portfolioItems.push({
id: row.dataset.id,
coinCode: cells[0].querySelector('div:last-child').textContent.trim(),
coinName: cells[0].querySelector('div:first-child').textContent.trim(),
date: cells[1].textContent.trim(),
price: parseFloat(cells[2].querySelector('span').textContent.replace('$', '')),
qty: parseFloat(cells[3].querySelector('input').value),
avgCost: cells[5].querySelector('input').value ? parseFloat(cells[5].querySelector('input').value) : null,
daysSince: parseInt(cells[10].textContent.trim()) || 0
});
});
localStorage.setItem('cryptoPortfolio', JSON.stringify(portfolioItems));
}
async function loadPortfolioFromStorage() {
const savedPortfolio = localStorage.getItem('cryptoPortfolio');
if (!savedPortfolio) return;
portfolioBody.innerHTML = '';
const portfolioItems = JSON.parse(savedPortfolio);
for (const item of portfolioItems) {
try {
// Get current price
const priceData = await getCoinPrice(item.coinCode);
const price = priceData.rate;
// For historical date, get historical price
let historicalPrice = price;
let daysSince = item.daysSince;
if (item.date !== today) {
try {
const historicalData = await getHistoricalPrice(item.coinCode, item.date);
historicalPrice = historicalData.rate;
} catch (error) {
console.error('Error fetching historical price:', error);
// Use current price if historical fails
historicalPrice = price;
}
}
// Calculate values
const currentCost = item.qty * price;
const avgValue = item.avgCost ? item.qty * item.avgCost : currentCost;
const diffDollar = currentCost - avgValue;
const diffPercent = ((currentCost - avgValue) / avgValue) * 100;
const status = diffDollar >= 0 ? 'Profit' : 'Loss';
const portfolioItem = {
id: item.id,
coinCode: item.coinCode,
coinName: item.coinName,
date: item.date,
price,
historicalPrice,
qty: item.qty,
currentCost,
avgCost: item.avgCost,
avgValue,
diffDollar,
diffPercent,
status,
daysSince
};
addPortfolioRow(portfolioItem);
} catch (error) {
console.error('Error loading portfolio item:', error);
}
}
updateSummary();
}
function updateSummary() {
let totalInv = 0;
let totalCurrent = 0;
document.querySelectorAll('#portfolioBody tr').forEach(row => {
const cells = row.querySelectorAll('td');
const avgValue = parseFloat(cells[6].textContent.replace('$', ''));
const currentValue = parseFloat(cells[4].textContent.replace('$', ''));
totalInv += avgValue;
totalCurrent += currentValue;
});
const profitLossValue = totalCurrent - totalInv;
totalInvestment.textContent = `$${totalInv.toFixed(2)}`;
currentValue.textContent = `$${totalCurrent.toFixed(2)}`;
profitLoss.textContent = `$${profitLossValue.toFixed(2)}`;
// Style profit/loss
profitLoss.className = `text-2xl font-bold ${profitLossValue >= 0 ? 'text-green-600' : 'text-red-600'}`;
}
</script>
</body>
</html>