<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JobsDone App - Supabase Blogger Edition</title>
<!-- STYLES: Tailwind CSS & Google Fonts -->
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style type="text/tailwindcss">
@layer base {
html { font-family: 'Inter', sans-serif; }
}
@layer components {
.input-select {
@apply w-full h-12 px-4 bg-slate-50 border border-slate-200 rounded-xl text-slate-700 focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 outline-none transition-all appearance-none;
}
.btn-primary {
@apply w-full h-14 bg-gradient-to-br from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white text-lg font-bold uppercase tracking-widest rounded-2xl shadow-lg shadow-blue-500/20 active:scale-[0.98] transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2;
}
}
</style>
<!-- DEPENDENCIES -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@supabase/supabase-js@2"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-slate-50">
<!-- Application Root -->
<div id="jobsdone-blogger-root"></div>
<script type="text/babel">
/**
* JOBSDONE APP for Blogger - Supabase Edition
* Optimized for Blogger Gadget usage.
*/
const { useState, useEffect, useMemo } = React;
// --- CONFIGURATION ---
const SUPABASE_URL = 'https://ipjlqlsaxhvzvelcgqav.supabase.co';
const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlwamxxbHNheGh2enZlbGNncWF2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY2MDA4NTUsImV4cCI6MjA5MjE3Njg1NX0.ceuuTpDjAHS4vmwgczr6pV0Kf1DQo97p25nPfW-JLTQ';
const TABLE_NAME = 'web-tasks'; // Your main database table
const supabaseClient = supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
const MONTH_NAMES = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
function App() {
const [jobs, setJobs] = useState([]);
const [loading, setLoading] = useState(true);
const [connStatus, setConnStatus] = useState('checking');
const [newMonth, setNewMonth] = useState(MONTH_NAMES[new Date().getMonth()]);
const [newYear, setNewYear] = useState(new Date().getFullYear().toString());
const [newDesc, setNewDesc] = useState('');
const [isSaving, setIsSaving] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [sortOrder, setSortOrder] = useState('desc');
const fetchJobs = async () => {
setLoading(true);
try {
const { data, error } = await supabaseClient
.from(TABLE_NAME)
.select('*')
.order('created_at', { ascending: sortOrder === 'asc' });
if (error) throw error;
setJobs(data || []);
setConnStatus('ok');
} catch (err) {
console.error('Connection error:', err);
setConnStatus('failed');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchJobs();
// Initialize icons
setTimeout(() => lucide.createIcons(), 500);
}, [sortOrder]);
useEffect(() => {
lucide.createIcons();
}, [jobs, connStatus, loading]);
const handleAdd = async () => {
if (!newDesc.trim()) { alert("Please enter a description."); return; }
setIsSaving(true);
const headerStr = `${newMonth.toUpperCase()} ${newYear}`;
const dateStr = `${newMonth} ${newYear}`;
const jobData = {
header: headerStr,
detail: newDesc,
date: dateStr,
status: 'DONE',
id: Math.random().toString(36).substring(7) // Local ID for immediate state update if needed
};
try {
const { error } = await supabaseClient.from(TABLE_NAME).insert([jobData]);
if (error) throw error;
setNewDesc('');
fetchJobs();
} catch (err) {
alert("Sync failed. Check connection.");
} finally {
setIsSaving(false);
}
};
const handleDelete = async (id) => {
if (!confirm('Are you sure you want to delete this record?')) return;
try {
const { error } = await supabaseClient.from(TABLE_NAME).delete().eq('id', id);
if (error) throw error;
fetchJobs();
} catch (err) { console.error(err); }
};
const filteredJobs = useMemo(() => {
return jobs.filter(job =>
(job.detail || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
(job.header || "").toLowerCase().includes(searchQuery.toLowerCase()) ||
(job.date || "").toLowerCase().includes(searchQuery.toLowerCase())
);
}, [jobs, searchQuery]);
const todayStr = new Date().toLocaleDateString('en-US', {
month: 'long', day: 'numeric', year: 'numeric'
});
return (
<div className="min-h-screen bg-slate-50 text-slate-900 p-4 md:p-8 max-w-4xl mx-auto border-x border-slate-100">
<header className="mb-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div className="space-y-1">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-600 rounded-lg shadow-lg">
<i data-lucide="check-circle-2" className="w-6 h-6 text-white"></i>
</div>
<h1 className="text-3xl font-extrabold tracking-tight text-slate-900">JOBSDONE APP</h1>
</div>
<p className="text-sm font-medium text-slate-500 italic pl-11">Refined Activity Logging — {todayStr}</p>
</div>
<div className="flex items-center">
{connStatus === 'ok' ? (
<span className="flex items-center gap-2 px-3 py-1 bg-emerald-50 text-emerald-700 text-[10px] font-bold rounded-full border border-emerald-100 uppercase tracking-wider">
<div className="w-1.5 h-1.5 bg-emerald-500 rounded-full shadow-[0_0_8px_rgba(16,185,129,0.5)]"></div>
Supabase Connected
</span>
) : (
<span className="flex items-center gap-2 px-3 py-1 bg-rose-50 text-rose-700 text-[10px] font-bold rounded-full border border-rose-100 uppercase tracking-wider">
<div className="w-1.5 h-1.5 bg-rose-500 rounded-full"></div>
Connection Error
</span>
)}
</div>
</header>
<div className="flex justify-center mb-10 text-emerald-500 opacity-20">
<i data-lucide="history" className="w-10 h-10 animate-[spin_10s_linear_infinite]"></i>
</div>
<div className="space-y-10">
{/* INPUT SECTION */}
<section className="bg-white border border-slate-200 rounded-3xl shadow-sm p-6 md:p-8 space-y-6 relative overflow-hidden">
<div className="absolute top-0 left-0 w-2 h-full bg-blue-600"></div>
<div className="flex items-center gap-3 text-xl font-bold text-slate-800">
<i data-lucide="plus-circle" className="text-blue-600 w-6 h-6"></i>
<span>Log New Milestone</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-[0.15em] ml-1">Select Month</label>
<select value={newMonth} onChange={(e) => setNewMonth(e.target.value)} className="input-select">
{MONTH_NAMES.map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-[0.15em] ml-1">Select Year</label>
<select value={newYear} onChange={(e) => setNewYear(e.target.value)} className="input-select">
{Array.from({length: 10}, (_, i) => 2023 + i).map(y => <option key={y} value={y}>{y}</option>)}
</select>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-[0.15em] ml-1">Description</label>
<textarea
placeholder="What did you achieve?..."
value={newDesc}
onChange={(e) => setNewDesc(e.target.value)}
className="w-full min-h-[140px] p-5 text-lg border border-slate-200 rounded-2xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all bg-slate-50/50 outline-none resize-none"
></textarea>
</div>
<button onClick={handleAdd} disabled={isSaving || connStatus !== 'ok'} className="btn-primary">
<i data-lucide="plus-circle" className="w-5 h-5"></i>
<span>{isSaving ? "Syncing..." : "Commit to Database"}</span>
</button>
</section>
{/* TOOLS SECTION */}
<section className="bg-white border border-slate-200 rounded-2xl p-4 flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="relative w-full md:max-w-xs">
<i data-lucide="search" className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"></i>
<input
type="text"
placeholder="Search milestones..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full h-11 pl-11 pr-4 bg-slate-50 border border-slate-100 rounded-xl text-sm outline-none focus:ring-2 focus:ring-blue-500/10 transition-all"
/>
</div>
<button
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
className="flex items-center gap-2 px-4 h-11 bg-slate-50 text-slate-600 text-sm font-semibold rounded-xl border border-slate-100 hover:bg-slate-100 transition-colors"
>
<i data-lucide="arrow-up-down" className="w-4 h-4"></i>
<span>{sortOrder === 'desc' ? 'Newest First' : 'Oldest First'}</span>
</button>
</section>
{/* DATA TABLE */}
<section className="bg-white border border-slate-200 rounded-3xl shadow-sm overflow-hidden min-h-[300px]">
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead className="bg-slate-50/80 border-b border-slate-100 uppercase text-[10px] font-bold text-slate-400 tracking-widest">
<tr>
<th className="px-8 py-5">Period</th>
<th className="px-8 py-5 text-center">Milestone Details</th>
<th className="px-8 py-5 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50 text-sm">
{loading && filteredJobs.length === 0 ? (
<tr><td colSpan="3" className="py-24 text-center text-slate-300 italic">Pulling records from cloud...</td></tr>
) : filteredJobs.length === 0 ? (
<tr><td colSpan="3" className="py-24 text-center text-slate-300 italic">No milestones captured yet.</td></tr>
) : (
filteredJobs.map(job => (
<tr key={job.id} className="hover:bg-slate-50/50 transition-colors">
<td className="px-8 py-7 w-48">
<div className="font-bold text-slate-900 leading-tight">
{(job.header || job.date || "").split(' ')[0]}
</div>
<div className="text-[10px] font-bold text-blue-600 uppercase tracking-wider mt-1 opacity-70">
Year: {(job.header || job.date || "").split(' ')[1]}
</div>
</td>
<td className="px-8 py-7 text-slate-600 leading-relaxed italic border-x border-slate-100/30">
<div className="max-w-md mx-auto">{job.detail}</div>
</td>
<td className="px-8 py-7 text-right">
<button onClick={() => handleDelete(job.id)} className="p-3 text-slate-200 hover:text-rose-500 hover:bg-rose-50 rounded-2xl transition-all">
<i data-lucide="trash-2" className="w-5 h-5"></i>
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
<footer className="mt-20 py-10 border-t border-slate-100 text-center">
<p className="text-[10px] font-bold text-slate-300 uppercase tracking-[0.5em]">Household++ Productivity Core</p>
</footer>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('jobsdone-blogger-root'));
root.render(<App />);
</script>
</body>
</html>
Comments
Post a Comment