<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JOBSDONE APP v2 - Supabase Edition</title>
<!-- External Dependencies (Blogger Compatible) -->
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;600;800&family=Space+Grotesk:wght@400;600;700&display=swap" rel="stylesheet">
<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>
<style type="text/tailwindcss">
@layer base {
html {
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
background: #F8FAFC;
}
h1, h2, h3 { font-family: 'Outfit', sans-serif; }
.font-mono { font-family: 'JetBrains Mono', monospace; }
}
@layer components {
.btn-primary {
@apply w-full h-14 bg-indigo-600 hover:bg-indigo-700 text-white font-bold rounded-2xl shadow-lg shadow-indigo-100 transition-all active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2;
}
.input-style {
@apply w-full px-4 py-3 bg-white border border-slate-200 rounded-xl focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 outline-none transition-all placeholder:text-slate-400;
}
.card-gradient {
@apply bg-white border border-slate-100 shadow-sm hover:shadow-md transition-shadow duration-300;
}
.badge {
@apply inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[10px] font-bold uppercase tracking-wider;
}
}
/* Custom scroller for Blogger integration */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #E2E8F0; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #CBD5E1; }
</style>
</head>
<body class="selection:bg-indigo-100 selection:text-indigo-900">
<div id="root"></div>
<script type="text/babel">
/**
* JOBSDONE APP v2 (Supabase Edition)
* Optimized for Blogger / Blogspot HTML Gadgets
*/
const { useState, useEffect, useMemo, useCallback } = React;
// --- Configuration (Injected directly for Blogger compatibility) ---
const SUPABASE_URL = 'https://ipjlqlsaxhvzvelcgqav.supabase.co';
const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlwamxxbHNheGh2enZlbGNncWF2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY2MDA4NTUsImV4cCI6MjA5MjE3Njg1NX0.ceuuTpDjAHS4vmwgczr6pV0Kf1DQo97p25nPfW-JLTQ';
const TABLE_NAME = 'tasks';
const supabaseClient = supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
const CATEGORIES = [
"TO DO LIST (FILING)",
"TO DO LIST (PERTUKANGAN)",
"TO DO LIST (more priority)",
"TO DO LIST (less priority)",
"TO be BOUGHT",
"TO DO didepan LAPTOP"
];
const STATUSES = ["ongoing", "finished"];
// Helper for Lucide icons in UMD environment
const Icon = ({ name, className }) => {
useEffect(() => {
lucide.createIcons();
}, [name]);
return <i data-lucide={name} className={className}></i>;
};
// --- UI Components ---
const Modal = ({ isOpen, onClose, title, children }) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm animate-in fade-in duration-300" onClick={onClose}></div>
<div className="relative w-full max-w-lg overflow-hidden bg-white rounded-3xl shadow-2xl animate-in zoom-in-95 fade-in duration-300">
<div className="flex items-center justify-between px-6 py-5 border-b border-slate-50">
<h3 className="text-xl font-extrabold text-slate-800">{title}</h3>
<button onClick={onClose} className="p-2 hover:bg-slate-50 rounded-full transition-colors text-slate-400 hover:text-slate-600">
<Icon name="x" className="w-5 h-5" />
</button>
</div>
<div className="p-6 md:p-8">
{children}
</div>
</div>
</div>
);
};
function App() {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [sortOrder, setSortOrder] = useState('desc');
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState(null);
const [formData, setFormData] = useState({
category: CATEGORIES[0],
overall_status: STATUSES[0],
task_title: ''
});
const [editingTask, setEditingTask] = useState(null);
const [viewingTask, setViewingTask] = useState(null);
const fetchTasks = async () => {
setLoading(true);
try {
const { data, error } = await supabaseClient
.from(TABLE_NAME)
.select('*');
if (error) throw error;
setTasks(data || []);
setError(null);
} catch (err) {
setError('Database connection error. Please verify configuration.');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchTasks();
}, []);
const handleSave = async (e) => {
if (e) e.preventDefault();
const currentData = editingTask || formData;
if (!currentData.task_title.trim()) return alert("Please enter a task title.");
setIsSaving(true);
const payload = {
category: currentData.category,
overall_status: currentData.overall_status,
task_title: currentData.task_title
};
try {
let res;
if (editingTask) {
res = await supabaseClient
.from(TABLE_NAME)
.update(payload)
.eq('_key', editingTask._key);
} else {
res = await supabaseClient
.from(TABLE_NAME)
.insert([{
...payload,
_key: Date.now() + "_" + Math.floor(Math.random() * 1000)
}]);
}
if (res.error) throw res.error;
setFormData({ ...formData, task_title: '' });
setEditingTask(null);
fetchTasks();
} catch (err) {
alert("Sync Error: " + err.message);
} finally {
setIsSaving(false);
}
};
const handleDelete = async (key) => {
if (!confirm('Permanently delete this task?')) return;
try {
const { error } = await supabaseClient
.from(TABLE_NAME)
.delete()
.eq('_key', key);
if (error) throw error;
fetchTasks();
} catch (err) { alert(err.message); }
};
const filteredTasks = useMemo(() => {
let result = tasks.filter(t => {
const cat = (t.category || '').toLowerCase();
const status = (t.overall_status || '').toLowerCase();
const title = (t.task_title || '').toLowerCase();
const s = search.toLowerCase();
return cat.includes(s) || status.includes(s) || title.includes(s);
});
result.sort((a, b) => {
const valA = a.created_at || a._key;
const valB = b.created_at || b._key;
return sortOrder === 'desc' ? (valB > valA ? 1 : -1) : (valA > valB ? 1 : -1);
});
return result;
}, [tasks, search, sortOrder]);
return (
<div className="min-h-screen py-8 px-4 md:py-16">
<div className="max-w-4xl mx-auto">
{/* Header Section */}
<header className="mb-12 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 mb-6 bg-indigo-600 text-white rounded-3xl shadow-xl shadow-indigo-200 rotate-3 transform hover:rotate-0 transition-transform">
<Icon name="check-check" className="w-8 h-8" />
</div>
<h1 className="text-5xl font-black tracking-tight text-slate-900 mb-2">
JOBSDONE <span className="text-indigo-600 font-extrabold italic opacity-90">APP</span>
</h1>
<div className="flex items-center justify-center gap-4 text-slate-400 font-bold text-[10px] uppercase tracking-[0.3em]">
<span className="w-8 h-[2px] bg-indigo-100"></span>
Household Productivity Systems
<span className="w-8 h-[2px] bg-indigo-100"></span>
</div>
</header>
{/* Connection Error */}
{error && (
<div className="mb-8 p-4 bg-rose-50 border border-rose-100 rounded-2xl text-rose-600 flex items-center gap-4 animate-bounce">
<Icon name="alert-circle" className="w-6 h-6 shrink-0" />
<span className="text-sm font-bold tracking-tight">{error}</span>
</div>
)}
{/* Dashboard Logic */}
<div className="grid grid-cols-1 gap-8">
{/* Entry Form */}
<section className="bg-white p-8 rounded-[2.5rem] shadow-sm border border-slate-100 card-gradient">
<form onSubmit={handleSave} className="space-y-8">
<div className="flex items-center gap-2 text-xs font-black text-slate-400 uppercase tracking-widest">
<Icon name="plus-circle" className="w-4 h-4 text-indigo-400" />
Task Deployment
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-[11px] font-black text-slate-500 uppercase tracking-wider ml-1">Select Category</label>
<select
className="input-style font-semibold text-slate-700"
value={formData.category}
onChange={e => setFormData({...formData, category: e.target.value})}
>
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div className="space-y-2">
<label className="text-[11px] font-black text-slate-500 uppercase tracking-wider ml-1">Current Status</label>
<select
className="input-style font-semibold text-slate-700"
value={formData.overall_status}
onChange={e => setFormData({...formData, overall_status: e.target.value})}
>
{STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
</div>
<div className="space-y-2">
<label className="text-[11px] font-black text-slate-500 uppercase tracking-wider ml-1">Task Specification</label>
<textarea
placeholder="Specify the requirements for this mission..."
className="input-style min-h-[140px] resize-none font-medium leading-relaxed italic"
value={formData.task_title}
onChange={e => setFormData({...formData, task_title: e.target.value})}
/>
</div>
<button
type="submit"
disabled={isSaving || !!error}
className="btn-primary group"
>
{isSaving ? (
<Icon name="refresh-cw" className="w-5 h-5 animate-spin" />
) : (
<>
<Icon name="zap" className="w-5 h-5 group-hover:scale-125 transition-transform" />
<span>DEPLOY TASK</span>
</>
)}
</button>
</form>
</section>
{/* Viewport Controls */}
<section className="flex flex-col md:flex-row gap-4 items-center justify-between">
<div className="relative w-full md:max-w-xs group">
<Icon name="search" className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-slate-300 group-focus-within:text-indigo-400 transition-colors" />
<input
type="text"
placeholder="Scan intelligence..."
className="w-full pl-12 pr-4 py-4 bg-white border border-slate-200 rounded-3xl shadow-sm focus:ring-4 focus:ring-indigo-500/10 outline-none font-medium placeholder:italic transition-all"
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<div className="flex items-center gap-4 bg-white p-2 border border-slate-100 rounded-3xl shadow-sm shrink-0">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest pl-4">Priority Sequence:</span>
<select
className="px-6 py-2 bg-slate-50 border-none rounded-2xl outline-none text-xs font-bold text-slate-600 cursor-pointer hover:bg-slate-100 transition-colors"
value={sortOrder}
onChange={e => setSortOrder(e.target.value)}
>
<option value="desc">NEXUS (Newest)</option>
<option value="asc">LEGACY (Oldest)</option>
</select>
</div>
</section>
{/* Main Register */}
<section className="bg-white rounded-[2.5rem] shadow-sm border border-slate-100 overflow-hidden card-gradient">
<div className="overflow-x-auto scroller">
<table className="w-full text-left">
<thead>
<tr className="bg-slate-50/80 border-b border-slate-50 uppercase tracking-[0.2em] text-[9px] font-black text-slate-400">
<th className="px-8 py-5">Metadata</th>
<th className="px-8 py-5">Objective Parameters</th>
<th className="px-8 py-5 text-right">Operations</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
{loading ? (
<tr>
<td colSpan="3" className="px-8 py-32 text-center">
<div className="flex flex-col items-center gap-6">
<div className="w-12 h-12 border-4 border-indigo-600 border-t-transparent rounded-full animate-spin"></div>
<div className="font-black text-indigo-400 text-xs uppercase tracking-[0.5em] animate-pulse">Establishing Secure Uplink...</div>
</div>
</td>
</tr>
) : filteredTasks.length === 0 ? (
<tr>
<td colSpan="3" className="px-8 py-32 text-center text-slate-300 font-bold italic tracking-tight">
No intelligence found matching search parameters.
</td>
</tr>
) : (
filteredTasks.map(task => (
<tr key={task._key} className="group hover:bg-indigo-50/30 transition-all">
<td className="px-8 py-8 w-64 align-top">
<div className="flex items-center gap-2 mb-3">
<Icon name="tag" className="w-3 h-3 text-indigo-400" />
<span className="text-[10px] font-black text-slate-500 uppercase tracking-tighter truncate max-w-[150px]">
{task.category}
</span>
</div>
<div className={`badge ${
task.overall_status === 'finished'
? 'bg-emerald-50 text-emerald-600'
: 'bg-amber-50 text-amber-600'
}`}>
<Icon name="activity" className="w-3 h-3" />
{task.overall_status}
</div>
</td>
<td className="px-8 py-8 align-top">
<div className="text-slate-700 font-bold text-sm leading-relaxed italic line-clamp-3 hover:line-clamp-none transition-all cursor-default">
{task.task_title}
</div>
</td>
<td className="px-8 py-8 text-right whitespace-nowrap align-top">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setViewingTask(task)}
className="p-3 rounded-2xl text-slate-400 hover:text-indigo-600 hover:bg-indigo-100/50 transition-all hover:rotate-12"
title="Analyze"
>
<Icon name="eye" className="w-5 h-5" />
</button>
<button
onClick={() => setEditingTask(task)}
className="p-3 rounded-2xl text-slate-400 hover:text-indigo-600 hover:bg-indigo-100/50 transition-all hover:-rotate-12"
title="Modify"
>
<Icon name="edit-3" className="w-5 h-5" />
</button>
<button
onClick={() => handleDelete(task._key)}
className="p-3 rounded-2xl text-slate-400 hover:text-rose-600 hover:bg-rose-100/50 transition-all hover:scale-125"
title="Terminate"
>
<Icon name="trash-2" className="w-5 h-5" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</section>
</div>
{/* System Footer */}
<footer className="mt-20 py-10 border-t border-slate-100 text-center">
<div className="flex justify-center gap-2 mb-6">
<span className="w-2 h-2 rounded-full bg-indigo-600 animate-ping"></span>
<span className="w-2 h-2 rounded-full bg-indigo-400 animate-pulse"></span>
<span className="w-2 h-2 rounded-full bg-indigo-200"></span>
</div>
<div className="text-[10px] font-black text-slate-300 uppercase tracking-[0.6em] mb-2">
Enterprise Productivity Infrastructure
</div>
<div className="text-[8px] font-bold text-slate-200 uppercase tracking-[0.2em]">
JOBSDONE CORE v2.4.0 (SUPABASE-JS-RUNTIME)
</div>
</footer>
</div>
{/* Modal Overlays */}
<Modal
isOpen={!!viewingTask}
onClose={() => setViewingTask(null)}
title="Objective Analysis"
>
{viewingTask && (
<div className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="p-5 bg-slate-50 rounded-[1.5rem]">
<div className="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-2">Sector</div>
<div className="text-xs font-black text-slate-700 uppercase">{viewingTask.category}</div>
</div>
<div className={`p-5 rounded-[1.5rem] border ${
viewingTask.overall_status === 'finished' ? 'bg-emerald-50 border-emerald-100 text-emerald-600' : 'bg-amber-50 border-amber-100 text-amber-600'
}`}>
<div className="text-[9px] font-black opacity-60 uppercase tracking-widest mb-2">Status</div>
<div className="text-xs font-black uppercase tracking-widest">{viewingTask.overall_status}</div>
</div>
</div>
<div className="p-8 bg-indigo-50/50 rounded-[2rem] border border-indigo-100">
<div className="text-[9px] font-black text-indigo-400 uppercase tracking-widest mb-4">Objective Details</div>
<div className="text-slate-800 font-bold italic leading-loose text-lg text-center">
"{viewingTask.task_title}"
</div>
</div>
<button
onClick={() => setViewingTask(null)}
className="w-full h-14 bg-slate-900 text-white font-black rounded-2xl active:scale-95 transition-transform uppercase tracking-widest text-xs"
>
Dismiss Analysis
</button>
</div>
)}
</Modal>
<Modal
isOpen={!!editingTask}
onClose={() => setEditingTask(null)}
title="Modify Specification"
>
{editingTask && (
<form onSubmit={handleSave} className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-wider ml-1">Category</label>
<select
className="input-style font-bold text-slate-700"
value={editingTask.category}
onChange={e => setEditingTask({...editingTask, category: e.target.value})}
>
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-wider ml-1">Status</label>
<select
className="input-style font-bold text-slate-700"
value={editingTask.overall_status}
onChange={e => setEditingTask({...editingTask, overall_status: e.target.value})}
>
{STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-slate-400 uppercase tracking-wider ml-1">Task specification</label>
<textarea
className="input-style min-h-[160px] resize-none font-bold italic"
value={editingTask.task_title}
onChange={e => setEditingTask({...editingTask, task_title: e.target.value})}
/>
</div>
<div className="flex gap-4 pt-4">
<button
type="button"
onClick={() => setEditingTask(null)}
className="flex-1 h-14 bg-slate-50 text-slate-400 font-black rounded-2xl hover:bg-slate-100 transition-colors uppercase text-xs"
>
Abort
</button>
<button
type="submit"
disabled={isSaving}
className="flex-1 h-14 bg-indigo-600 text-white font-black rounded-2xl shadow-xl shadow-indigo-100 hover:bg-indigo-700 active:scale-95 transition-all text-xs"
>
{isSaving ? "SYNCHRONIZING..." : "COMMIT CHANGES"}
</button>
</div>
</form>
)}
</Modal>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>
Comments
Post a Comment