<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Glow - Receipt Tracker</title>
<!-- Tailwind CSS for modern, high-contrast styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Supabase JS Client -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<style>
body {
background-color: #f3f4f6;
color: #111827;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
</style>
</head>
<body class="min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-slate-900 text-white shadow-md p-4 sticky top-0 z-50">
<div class="max-w-md mx-auto flex justify-between items-center">
<h1 class="text-xl font-extrabold tracking-tight flex items-center gap-2">
✨ Glow <span class="text-amber-500 font-light text-xs">Receipts</span>
</h1>
<span id="connection-status" class="text-xs px-2 py-1 rounded bg-red-800 text-red-100 font-medium">Not Connected</span>
</div>
</header>
<!-- Main Container -->
<main class="flex-1 max-w-md w-full mx-auto p-4 pb-24 space-y-6">
<!-- Setup Configuration Card -->
<section id="config-card" class="bg-white p-4 rounded-xl shadow-sm border border-slate-200">
<h2 class="text-sm font-bold uppercase tracking-wider text-slate-500 mb-3">Supabase Connection Setup</h2>
<div class="space-y-3">
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1">Supabase Project URL</label>
<input type="text" id="supabase-url" placeholder="https://xyz.supabase.co" class="w-full p-3 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-900 focus:outline-none">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 mb-1">Supabase Anon / Public Key</label>
<input type="password" id="supabase-key" placeholder="eyJhbGci..." class="w-full p-3 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-900 focus:outline-none">
</div>
<button onclick="saveConfig()" class="w-full bg-slate-900 hover:bg-slate-800 text-white font-bold py-3 px-4 rounded-lg transition text-sm shadow">
Connect & Save
</button>
</div>
</section>
<!-- Action Card: Snap / Upload -->
<section class="bg-white p-5 rounded-xl shadow-sm border border-slate-200 text-center space-y-4">
<h2 class="text-lg font-bold text-slate-800">Add New Receipt</h2>
<!-- Hidden file input triggered by big button -->
<input type="file" id="receipt-file" accept="image/*" capture="environment" class="hidden" onchange="handleImageCapture(event)">
<button onclick="document.getElementById('receipt-file').click()" class="w-full bg-amber-500 hover:bg-amber-600 text-slate-950 font-black py-4 px-6 rounded-xl shadow-md transition text-lg flex items-center justify-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
SNAP RECEIPT
</button>
<p id="upload-status" class="text-xs text-slate-500 font-medium">Tap to open phone camera or select file.</p>
</section>
<!-- Recent Receipts Feed -->
<section class="space-y-3">
<div class="flex justify-between items-center">
<h2 class="text-sm font-bold uppercase tracking-wider text-slate-500">Recent Receipts</h2>
<button onclick="loadReceipts()" class="text-xs text-slate-600 font-semibold hover:underline">Refresh</button>
</div>
<div id="receipts-list" class="space-y-2">
<div class="p-4 bg-white rounded-xl border border-slate-200 text-sm text-slate-400 text-center">
No receipts logged yet. Connect Supabase above to start.
</div>
</div>
</section>
</main>
<!-- Footer / Export Bar -->
<footer class="bg-white border-t border-slate-200 p-4 fixed bottom-0 left-0 right-0 shadow-lg">
<div class="max-w-md mx-auto flex gap-3">
<button onclick="exportToCSV()" class="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-800 font-bold py-3 px-4 rounded-xl text-sm transition border border-slate-300">
Export CSV
</button>
</div>
</footer>
<!-- Application Script Logic -->
<script>
let supabaseClient = null;
window.addEventListener('DOMContentLoaded', () => {
const savedUrl = localStorage.getItem('sb_url');
const savedKey = localStorage.getItem('sb_key');
if (savedUrl && savedKey) {
document.getElementById('supabase-url').value = savedUrl;
document.getElementById('supabase-key').value = savedKey;
initSupabase(savedUrl, savedKey);
}
});
function saveConfig() {
const url = document.getElementById('supabase-url').value.trim();
const key = document.getElementById('supabase-key').value.trim();
if (!url || !key) {
alert('Please enter both Supabase URL and Key.');
return;
}
localStorage.setItem('sb_url', url);
localStorage.setItem('sb_key', key);
initSupabase(url, key);
}
function initSupabase(url, key) {
try {
supabaseClient = supabase.createClient(url, key);
document.getElementById('connection-status').textContent = 'Connected';
document.getElementById('connection-status').className = 'text-xs px-2 py-1 rounded bg-emerald-800 text-emerald-100 font-medium';
loadReceipts();
} catch (err) {
console.error(err);
alert('Failed to initialize Supabase connection.');
}
}
async function handleImageCapture(event) {
if (!supabaseClient) {
alert('Please connect your Supabase credentials first.');
return;
}
const file = event.target.files[0];
if (!file) return;
const statusEl = document.getElementById('upload-status');
statusEl.textContent = 'Uploading receipt image...';
try {
const fileName = `${Date.now()}_${file.name}`;
const { data: uploadData, error: uploadError } = await supabaseClient.storage
.from('receipt-images')
.upload(fileName, file);
if (uploadError) throw uploadError;
const { data: publicUrlData } = supabaseClient.storage
.from('receipt-images')
.getPublicUrl(fileName);
statusEl.textContent = 'Saving entry to database...';
const { error: dbError } = await supabaseClient
.from('receipts')
.insert([
{
vendor_name: 'Pending Review (Captured)',
total_amount: 0.00,
receipt_date: new Date().toISOString().split('T')[0],
image_url: publicUrlData.publicUrl
}
]);
if (dbError) throw dbError;
statusEl.textContent = 'Success! Receipt saved.';
setTimeout(() => { statusEl.textContent = 'Tap to open phone camera or select file.'; }, 3000);
loadReceipts();
} catch (error) {
console.error(error);
statusEl.textContent = 'Error uploading. Check console logs.';
}
}
async function loadReceipts() {
if (!supabaseClient) return;
const listEl = document.getElementById('receipts-list');
const { data, error } = await supabaseClient
.from('receipts')
.select('*')
.order('created_at', { ascending: false })
.limit(10);
if (error) {
console.error(error);
return;
}
if (!data || data.length === 0) {
listEl.innerHTML = `<div class="p-4 bg-white rounded-xl border border-slate-200 text-sm text-slate-400 text-center">No receipts found yet.</div>`;
return;
}
listEl.innerHTML = data.map(r => `
<div class="p-3 bg-white rounded-xl border border-slate-200 flex justify-between items-center shadow-xs">
<div>
<p class="font-bold text-slate-800 text-sm">${r.vendor_name || 'Unnamed Vendor'}</p>
<p class="text-xs text-slate-500">${r.receipt_date || 'No Date'} • <span class="text-amber-600 font-semibold">$${Number(r.total_amount || 0).toFixed(2)}</span></p>
</div>
${r.image_url ? `<a href="${r.image_url}" target="_blank" class="text-xs bg-slate-100 hover:bg-slate-200 text-slate-700 px-3 py-1.5 rounded-lg font-medium border border-slate-300">View</a>` : ''}
</div>
`).join('');
}
function exportToCSV() {
alert('CSV Export feature ready for wiring to accountant format!');
}
</script>
</body>
</html>