feat: dark modesupport
This commit is contained in:
87
src/App.jsx
87
src/App.jsx
@@ -54,6 +54,12 @@ async function saveData(data) {
|
||||
|
||||
const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||||
|
||||
const LoadingScreen = () => (
|
||||
<div className="w-full max-w-[480px] mx-auto min-h-screen border-x border-[#E8DFD3] dark:border-[#3B2217] bg-[#FAF6F1] dark:bg-[#150B07] max-sm:border-x-0 flex items-center justify-center transition-colors duration-200">
|
||||
<div className="text-center text-[#9C8B7A] dark:text-[#C8B9A6]"><div className="text-4xl">☕</div><div className="mt-2 text-sm">Loading…</div></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// ─── Main App ───
|
||||
export default function CoffeeLogbook() {
|
||||
const { token, user, loading, logout } = useContext(AuthContext);
|
||||
@@ -120,23 +126,18 @@ export default function CoffeeLogbook() {
|
||||
setData(newData); await saveData(newData); syncData(newData);
|
||||
}, [syncData]);
|
||||
|
||||
const LoadingScreen = () => (
|
||||
<div className="w-full max-w-[480px] mx-auto min-h-screen border-x border-[#E8DFD3] bg-[#FAF6F1] max-sm:border-x-0 flex items-center justify-center">
|
||||
<div className="text-center text-[#9C8B7A]"><div className="text-4xl">☕</div><div className="mt-2 text-sm">Loading…</div></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading) return <LoadingScreen />;
|
||||
|
||||
if (!token || !user) {
|
||||
return (
|
||||
<div className="w-full max-w-[480px] mx-auto min-h-screen border-x border-[#E8DFD3] bg-[#FAF6F1] max-sm:border-x-0">
|
||||
<div className="w-full max-w-[480px] mx-auto min-h-screen border-x border-[#E8DFD3] dark:border-[#3B2217] bg-[#FAF6F1] dark:bg-[#150B07] max-sm:border-x-0 transition-colors duration-200">
|
||||
{authView === "login" ? (
|
||||
<>
|
||||
<Login onLoginSuccess={() => {}} />
|
||||
<div className="text-center mt-5 px-5">
|
||||
<p className="text-[#9C8B7A] text-sm">Don't have an account?{' '}
|
||||
<button onClick={() => setAuthView("register")} className="border-none bg-transparent text-[#8B6914] cursor-pointer underline text-sm">Register</button>
|
||||
<p className="text-[#9C8B7A] dark:text-[#C8B9A6] text-sm">Don't have an account?{' '}
|
||||
<button onClick={() => setAuthView("register")} className="border-none bg-transparent text-[#8B6914] dark:text-[#D4A325] cursor-pointer underline text-sm">Register</button>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
@@ -144,8 +145,8 @@ export default function CoffeeLogbook() {
|
||||
<>
|
||||
<Register onRegisterSuccess={() => setAuthView("login")} />
|
||||
<div className="text-center mt-5 px-5">
|
||||
<p className="text-[#9C8B7A] text-sm">Already have an account?{' '}
|
||||
<button onClick={() => setAuthView("login")} className="border-none bg-transparent text-[#8B6914] cursor-pointer underline text-sm">Login</button>
|
||||
<p className="text-[#9C8B7A] dark:text-[#C8B9A6] text-sm">Already have an account?{' '}
|
||||
<button onClick={() => setAuthView("login")} className="border-none bg-transparent text-[#8B6914] dark:text-[#D4A325] cursor-pointer underline text-sm">Login</button>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
@@ -180,23 +181,23 @@ export default function CoffeeLogbook() {
|
||||
const methodCounts = { pourover: 0, espresso: 0, coldbrew: 0 };
|
||||
brewLogs.forEach(l => { if (methodCounts[l.method] !== undefined) methodCounts[l.method]++; });
|
||||
|
||||
const filterPillCls = (active) => `px-3.5 py-1.5 rounded-full border text-xs font-medium whitespace-nowrap cursor-pointer transition-all ${active ? "bg-[#2C1810] text-[#FAF6F1] border-[#2C1810]" : "bg-white border-[#E8DFD3] text-[#6B5744]"}`;
|
||||
const filterPillCls = (active) => `px-3.5 py-1.5 rounded-full border text-xs font-medium whitespace-nowrap cursor-pointer transition-all ${active ? "bg-[#2C1810] text-[#FAF6F1] border-[#2C1810] dark:bg-[#FAF6F1] dark:text-[#2C1810] dark:border-[#FAF6F1]" : "bg-white border-[#E8DFD3] text-[#6B5744] dark:bg-[#22120B] dark:border-[#3B2217] dark:text-[#C8B9A6]"}`;
|
||||
|
||||
// Page title per view
|
||||
const pageTitles = { dashboard: "Brew Journal", beans: "Bean Recipes", brews: "Brew Logs", profile: "Profile" };
|
||||
const pageSubtitles = { dashboard: "Coffee Logbook", beans: "Your Collection", brews: "All Sessions", profile: "Account" };
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[480px] mx-auto min-h-screen bg-[#FAF6F1] border-x border-[#E8DFD3] shadow-[0_0_30px_rgba(44,24,16,0.05)] max-sm:border-x-0 max-sm:shadow-none font-sans relative">
|
||||
<div className="w-full max-w-[480px] mx-auto min-h-screen bg-[#FAF6F1] dark:bg-[#150B07] border-x border-[#E8DFD3] dark:border-[#3B2217] shadow-[0_0_30px_rgba(44,24,16,0.05)] dark:shadow-none max-sm:border-x-0 max-sm:shadow-none font-sans relative text-[#2C1810] dark:text-[#FAF6F1] transition-colors duration-200">
|
||||
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-6 pb-4 flex items-center justify-between sticky top-0 bg-[#FAF6F1]/95 backdrop-blur-sm z-50 border-b border-[#E8DFD3]">
|
||||
<div className="px-5 pt-6 pb-4 flex items-center justify-between sticky top-0 bg-[#FAF6F1]/95 dark:bg-[#150B07]/95 backdrop-blur-sm z-50 border-b border-[#E8DFD3] dark:border-[#3B2217] transition-colors duration-200">
|
||||
<div>
|
||||
<div className="text-[10px] text-[#9C8B7A] font-semibold tracking-[1.5px] uppercase">{pageSubtitles[view] || "Coffee Logbook"}</div>
|
||||
<h1 className="font-serif text-[21px] font-semibold tracking-tight text-[#2C1810] mt-0.5">{pageTitles[view] || "Brew Journal"}</h1>
|
||||
<div className="text-[10px] text-[#9C8B7A] dark:text-[#C8B9A6] font-semibold tracking-[1.5px] uppercase">{pageSubtitles[view] || "Coffee Logbook"}</div>
|
||||
<h1 className="font-serif text-[21px] font-semibold tracking-tight text-[#2C1810] dark:text-[#FAF6F1] mt-0.5">{pageTitles[view] || "Brew Journal"}</h1>
|
||||
</div>
|
||||
{showSyncedStatus && (
|
||||
<div className={`flex items-center gap-1 text-[11px] font-medium px-2.5 py-1 rounded-full transition-all ${isOnline ? "text-[#4A7C59] bg-[rgba(74,124,89,0.1)]" : "text-[#B44040] bg-[rgba(180,64,64,0.1)]"}`}
|
||||
<div className={`flex items-center gap-1 text-[11px] font-medium px-2.5 py-1 rounded-full transition-all ${isOnline ? "text-[#4A7C59] bg-[rgba(74,124,89,0.1)] dark:text-[#6CB281] dark:bg-[rgba(108,178,129,0.15)]" : "text-[#B44040] bg-[rgba(180,64,64,0.1)] dark:text-[#E55B5B] dark:bg-[rgba(229,91,91,0.15)]"}`}
|
||||
title={isOnline ? "Synchronized" : "Offline"}>
|
||||
<span className={`text-[10px] ${syncing ? "animate-sync-pulse" : ""}`}>●</span>
|
||||
<span>{isOnline ? (syncing ? "Syncing..." : "Synced") : "Offline"}</span>
|
||||
@@ -213,27 +214,27 @@ export default function CoffeeLogbook() {
|
||||
<div className="animate-page-enter">
|
||||
<div className="flex gap-2 mb-5">
|
||||
{[{ num: beans.length, label: "Beans" }, { num: brewLogs.length, label: "Brews" }, { num: new Set(brewLogs.map(l => l.beanId)).size, label: "Tried" }].map(s => (
|
||||
<div key={s.label} className="flex-1 bg-white border border-[#E8DFD3] rounded-2xl p-3.5 text-center">
|
||||
<div className="font-serif text-2xl font-bold text-[#2C1810]">{s.num}</div>
|
||||
<div className="text-[10px] text-[#9C8B7A] uppercase tracking-widest mt-0.5">{s.label}</div>
|
||||
<div key={s.label} className="flex-1 bg-white dark:bg-[#22120B] border border-[#E8DFD3] dark:border-[#3B2217] rounded-2xl p-3.5 text-center transition-colors duration-200">
|
||||
<div className="font-serif text-2xl font-bold text-[#2C1810] dark:text-[#FAF6F1]">{s.num}</div>
|
||||
<div className="text-[10px] text-[#9C8B7A] dark:text-[#C8B9A6] uppercase tracking-widest mt-0.5">{s.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[13px] font-semibold text-[#6B5744] uppercase tracking-widest mb-3">By Method</div>
|
||||
<div className="text-[13px] font-semibold text-[#6B5744] dark:text-[#C8B9A6] uppercase tracking-widest mb-3">By Method</div>
|
||||
<div className="flex gap-2 mb-6">
|
||||
{METHODS.map(m => (
|
||||
<div key={m} className="flex-1 bg-white border border-[#E8DFD3] rounded-2xl p-3 text-center" style={{ borderTop: `3px solid ${METHOD_COLORS[m]}` }}>
|
||||
<div key={m} className="flex-1 bg-white dark:bg-[#22120B] border border-[#E8DFD3] dark:border-[#3B2217] rounded-2xl p-3 text-center transition-colors duration-200" style={{ borderTop: `3px solid ${METHOD_COLORS[m]}` }}>
|
||||
<div className="text-xl mb-1">{METHOD_ICONS[m]}</div>
|
||||
<div className="font-serif text-xl font-bold text-[#2C1810]">{methodCounts[m]}</div>
|
||||
<div className="text-[10px] text-[#9C8B7A] uppercase tracking-widest mt-0.5">{METHOD_LABELS[m]}</div>
|
||||
<div className="font-serif text-xl font-bold text-[#2C1810] dark:text-[#FAF6F1]">{methodCounts[m]}</div>
|
||||
<div className="text-[10px] text-[#9C8B7A] dark:text-[#C8B9A6] uppercase tracking-widest mt-0.5">{METHOD_LABELS[m]}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[13px] font-semibold text-[#6B5744] uppercase tracking-widest mb-3">Recent Brews</div>
|
||||
<div className="text-[13px] font-semibold text-[#6B5744] dark:text-[#C8B9A6] uppercase tracking-widest mb-3">Recent Brews</div>
|
||||
{brewLogs.length === 0 ? (
|
||||
<div className="text-center py-12 text-[#9C8B7A]">
|
||||
<div className="text-4xl mb-3 opacity-50">📝</div>
|
||||
<h3 className="text-base mb-1.5 font-serif text-[#6B5744]">No brews yet</h3>
|
||||
<div className="text-center py-12 text-[#9C8B7A] dark:text-[#C8B9A6]">
|
||||
<div className="text-4xl mb-3 opacity-50 dark:opacity-80">📝</div>
|
||||
<h3 className="text-base mb-1.5 font-serif text-[#6B5744] dark:text-[#FAF6F1]">No brews yet</h3>
|
||||
<p className="text-[13px] leading-relaxed">Tap the + button to log your first brew.</p>
|
||||
</div>
|
||||
) : brewLogs.sort((a, b) => b.createdAt - a.createdAt).slice(0, 5).map(log => (
|
||||
@@ -245,32 +246,36 @@ export default function CoffeeLogbook() {
|
||||
{/* ── Bean Library ── */}
|
||||
{view === "beans" && !selectedBean && (
|
||||
<div className="animate-page-enter">
|
||||
<div className="text-[13px] font-semibold text-[#6B5744] uppercase tracking-widest mb-3">Your Beans ({beans.length})</div>
|
||||
<div className="text-[13px] font-semibold text-[#6B5744] dark:text-[#C8B9A6] uppercase tracking-widest mb-3">Your Beans ({beans.length})</div>
|
||||
{beans.length === 0 ? (
|
||||
<div className="text-center py-12 text-[#9C8B7A]">
|
||||
<div className="text-4xl mb-3 opacity-50">🫘</div>
|
||||
<h3 className="text-base mb-1.5 font-serif text-[#6B5744]">Library is empty</h3>
|
||||
<div className="text-center py-12 text-[#9C8B7A] dark:text-[#C8B9A6]">
|
||||
<div className="text-4xl mb-3 opacity-50 dark:opacity-80">🫘</div>
|
||||
<h3 className="text-base mb-1.5 font-serif text-[#6B5744] dark:text-[#FAF6F1]">Library is empty</h3>
|
||||
<p className="text-[13px] leading-relaxed">Tap + to add your first coffee bean.</p>
|
||||
</div>
|
||||
) : beans.map(bean => {
|
||||
const count = brewLogs.filter(l => l.beanId === bean.id).length;
|
||||
const roastTagCls = bean.roastType?.toLowerCase().includes("dark") ? "bg-[#E0D0BD] text-[#4A3520]" : bean.roastType?.toLowerCase().includes("medium") ? "bg-[#F0E0C8] text-[#6B4E2A]" : "bg-[#FFF3D6] text-[#8B6914]";
|
||||
const roastTagCls = bean.roastType?.toLowerCase().includes("dark")
|
||||
? "bg-[#E0D0BD] text-[#4A3520] dark:bg-[#2F1E12] dark:text-[#BCA38A]"
|
||||
: bean.roastType?.toLowerCase().includes("medium")
|
||||
? "bg-[#F0E0C8] text-[#6B4E2A] dark:bg-[#402A14] dark:text-[#D9AC7B]"
|
||||
: "bg-[#FFF3D6] text-[#8B6914] dark:bg-[#4A3912] dark:text-[#E6B640]";
|
||||
return (
|
||||
<div key={bean.id}
|
||||
className="bg-white rounded-2xl shadow-[0_1px_3px_rgba(44,24,16,0.06),0_4px_12px_rgba(44,24,16,0.04)] p-[18px] mb-3 border border-[#E8DFD3] cursor-pointer transition-all hover:shadow-[0_4px_16px_rgba(44,24,16,0.08)] active:scale-[0.99]"
|
||||
className="bg-white dark:bg-[#22120B] rounded-2xl shadow-[0_1px_3px_rgba(44,24,16,0.06),0_4px_12px_rgba(44,24,16,0.04)] dark:shadow-[0_4px_12px_rgba(0,0,0,0.2)] p-[18px] mb-3 border border-[#E8DFD3] dark:border-[#3B2217] cursor-pointer transition-all hover:shadow-[0_4px_16px_rgba(44,24,16,0.08)] dark:hover:shadow-[0_4px_16px_rgba(0,0,0,0.35)] active:scale-[0.99]"
|
||||
onClick={() => setSelectedBean(bean)}>
|
||||
<div className="flex justify-between items-start gap-3 mb-2">
|
||||
{bean.image && <img src={bean.image} alt="" className="w-14 h-14 rounded-xl object-cover flex-shrink-0" />}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-serif text-[17px] font-semibold text-[#2C1810]">{bean.name}</div>
|
||||
{bean.roastery && <div className="text-[13px] text-[#6B5744] mt-0.5">{bean.roastery}</div>}
|
||||
<div className="font-serif text-[17px] font-semibold text-[#2C1810] dark:text-[#FAF6F1]">{bean.name}</div>
|
||||
{bean.roastery && <div className="text-[13px] text-[#6B5744] dark:text-[#C8B9A6] mt-0.5">{bean.roastery}</div>}
|
||||
</div>
|
||||
<span className="text-[11px] px-2.5 py-1 rounded-full bg-[#F3EDE4] text-[#6B5744] font-medium flex-shrink-0">{count} brew{count !== 1 ? "s" : ""}</span>
|
||||
<span className="text-[11px] px-2.5 py-1 rounded-full bg-[#F3EDE4] dark:bg-[#2C1810] text-[#6B5744] dark:text-[#C8B9A6] font-medium flex-shrink-0">{count} brew{count !== 1 ? "s" : ""}</span>
|
||||
</div>
|
||||
{bean.tastingNotes && <div className="text-[13px] text-[#6B5744] italic mt-1.5 leading-snug">👅 {bean.tastingNotes}</div>}
|
||||
{bean.tastingNotes && <div className="text-[13px] text-[#6B5744] dark:text-[#C8B9A6] italic mt-1.5 leading-snug">👅 {bean.tastingNotes}</div>}
|
||||
<div className="flex gap-2 mt-2.5 flex-wrap">
|
||||
{bean.roastType && <span className={`text-[11px] px-2.5 py-1 rounded-full font-medium ${roastTagCls}`}>{bean.roastType}</span>}
|
||||
{bean.roastDate && <span className="text-[11px] px-2.5 py-1 rounded-full bg-[#F3EDE4] text-[#6B5744] font-medium">Roasted {bean.roastDate}</span>}
|
||||
{bean.roastDate && <span className="text-[11px] px-2.5 py-1 rounded-full bg-[#F3EDE4] dark:bg-[#2C1810] text-[#6B5744] dark:text-[#C8B9A6] font-medium">Roasted {bean.roastDate}</span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -301,9 +306,9 @@ export default function CoffeeLogbook() {
|
||||
))}
|
||||
</div>
|
||||
{filteredLogs.length === 0 ? (
|
||||
<div className="text-center py-12 text-[#9C8B7A]">
|
||||
<div className="text-4xl mb-3 opacity-50">📋</div>
|
||||
<h3 className="text-base mb-1.5 font-serif text-[#6B5744]">No logs {brewFilter !== "all" ? `for ${METHOD_LABELS[brewFilter]}` : "yet"}</h3>
|
||||
<div className="text-center py-12 text-[#9C8B7A] dark:text-[#C8B9A6]">
|
||||
<div className="text-4xl mb-3 opacity-50 dark:opacity-80">📋</div>
|
||||
<h3 className="text-base mb-1.5 font-serif text-[#6B5744] dark:text-[#FAF6F1]">No logs {brewFilter !== "all" ? `for ${METHOD_LABELS[brewFilter]}` : "yet"}</h3>
|
||||
<p className="text-[13px] leading-relaxed">Start brewing and log your recipes here.</p>
|
||||
</div>
|
||||
) : filteredLogs.map(log => (
|
||||
|
||||
Reference in New Issue
Block a user