feat: dark modesupport

This commit is contained in:
2026-06-06 12:08:17 +05:30
parent cdb7ded29d
commit f99ba7e9a7
17 changed files with 291 additions and 149 deletions

View File

@@ -8,6 +8,18 @@
<meta name="theme-color" content="#2C1810" />
<link rel="apple-touch-icon" href="/icon-192.png" />
<title>brew</title>
<script>
(function() {
try {
const theme = localStorage.getItem('brew-theme') || 'system';
if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} catch (e) {}
})();
</script>
</head>
<body>
<div id="root"></div>

View File

@@ -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 => (

66
src/ThemeContext.jsx Normal file
View File

@@ -0,0 +1,66 @@
/* eslint-disable react-refresh/only-export-components */
import { createContext, useContext, useState, useEffect } from "react";
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setThemeState] = useState(() => {
try {
return localStorage.getItem("brew-theme") || "system";
} catch {
return "system";
}
});
const [resolvedTheme, setResolvedTheme] = useState("light");
const setTheme = (newTheme) => {
try {
localStorage.setItem("brew-theme", newTheme);
} catch (e) {
console.error("Failed to set theme in localStorage", e);
}
setThemeState(newTheme);
};
useEffect(() => {
const root = window.document.documentElement;
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const applyTheme = () => {
const isDark = theme === "dark" || (theme === "system" && mediaQuery.matches);
if (isDark) {
root.classList.add("dark");
setResolvedTheme("dark");
} else {
root.classList.remove("dark");
setResolvedTheme("light");
}
};
applyTheme();
const handleChange = () => {
if (theme === "system") {
applyTheme();
}
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme, isDark: resolvedTheme === "dark" }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}

View File

@@ -1,31 +1,32 @@
import React from "react";
import BrewCard from "./BrewCard";
export default function BeanDetail({ bean, logs, onBack, onEdit, onDelete }) {
const beanLogs = logs.filter(l => l.beanId === bean.id).sort((a, b) => b.createdAt - a.createdAt);
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>
<button className="flex items-center gap-1.5 border-none bg-transparent text-[13px] text-[#6B5744] cursor-pointer p-0 mb-4" onClick={onBack}> Back</button>
<div className="transition-colors duration-200">
<button className="flex items-center gap-1.5 border-none bg-transparent text-[13px] text-[#6B5744] dark:text-[#C8B9A6] cursor-pointer p-0 mb-4" onClick={onBack}> Back</button>
{bean.image && <img src={bean.image} alt={bean.name} className="w-full h-44 object-cover rounded-2xl mb-4" />}
<h2 className="font-serif text-[22px] font-semibold text-[#2C1810] mb-1">{bean.name}</h2>
{bean.roastery && <div className="text-[#6B5744] text-sm mb-2">{bean.roastery}</div>}
{bean.tastingNotes && <div className="text-[13px] text-[#6B5744] italic mt-1.5 leading-snug mb-2.5">👅 {bean.tastingNotes}</div>}
<h2 className="font-serif text-[22px] font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-1">{bean.name}</h2>
{bean.roastery && <div className="text-[#6B5744] dark:text-[#C8B9A6] text-sm mb-2">{bean.roastery}</div>}
{bean.tastingNotes && <div className="text-[13px] text-[#6B5744] dark:text-[#C8B9A6] italic mt-1.5 leading-snug mb-2.5">👅 {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>}
<span className="text-[11px] px-2.5 py-1 rounded-full bg-[#F3EDE4] text-[#6B5744] font-medium">{beanLogs.length} brew{beanLogs.length !== 1 ? "s" : ""}</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>}
<span className="text-[11px] px-2.5 py-1 rounded-full bg-[#F3EDE4] dark:bg-[#2C1810] text-[#6B5744] dark:text-[#C8B9A6] font-medium">{beanLogs.length} brew{beanLogs.length !== 1 ? "s" : ""}</span>
</div>
<div className="flex gap-2 mt-4">
<button className="flex-1 py-2.5 border border-[#E8DFD3] rounded-xl text-sm font-semibold text-[#6B5744] bg-transparent cursor-pointer hover:bg-[#F3EDE4] transition-colors" onClick={onEdit}>Edit</button>
<button className="flex-1 py-2.5 border border-[#B44040] rounded-xl text-xs font-semibold text-[#B44040] bg-transparent cursor-pointer" onClick={onDelete}>Delete</button>
<button className="flex-1 py-2.5 border border-[#E8DFD3] dark:border-[#3B2217] rounded-xl text-sm font-semibold text-[#6B5744] dark:text-[#C8B9A6] bg-transparent cursor-pointer hover:bg-[#F3EDE4] dark:hover:bg-[#2C1810] transition-colors duration-200" onClick={onEdit}>Edit</button>
<button className="flex-1 py-2.5 border border-[#B44040] dark:border-[#E55B5B] rounded-xl text-xs font-semibold text-[#B44040] dark:text-[#E55B5B] bg-transparent cursor-pointer hover:bg-[rgba(180,64,64,0.05)] dark:hover:bg-[rgba(229,91,91,0.05)] transition-all duration-200" onClick={onDelete}>Delete</button>
</div>
<div className="mt-5">
<div className="text-[13px] font-semibold text-[#6B5744] uppercase tracking-widest mb-3">Brew History</div>
<div className="text-[13px] font-semibold text-[#6B5744] dark:text-[#C8B9A6] uppercase tracking-widest mb-3">Brew History</div>
{beanLogs.length === 0 ? (
<div className="text-center py-6 text-[#9C8B7A]"><p className="text-[13px]">No brews logged with this bean yet.</p></div>
<div className="text-center py-6 text-[#9C8B7A] dark:text-[#C8B9A6]"><p className="text-[13px]">No brews logged with this bean yet.</p></div>
) : beanLogs.map(log => <BrewCard key={log.id} log={log} beanName={bean.name} />)}
</div>
</div>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { ROAST_TYPES, inputCls, labelCls } from "../constants";
export default function BeanForm({ onSave, onClose, initial }) {
@@ -38,11 +38,11 @@ export default function BeanForm({ onSave, onClose, initial }) {
onClick={() => handleClose()}
>
<div
className={`bg-[#FAF6F1] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8 ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8 ${active ? "translate-y-0" : "translate-y-full"}`}
onClick={e => e.stopPropagation()}
>
<div className="w-9 h-1 bg-[#E8DFD3] rounded mx-auto my-3" />
<div className="font-serif text-xl font-semibold text-[#2C1810] mb-5">{initial ? "Edit Bean" : "Add Bean"}</div>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto my-3" />
<div className="font-serif text-xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-5">{initial ? "Edit Bean" : "Add Bean"}</div>
<div className="mb-4"><label className={labelCls}>Bean Name *</label>
<input className={inputCls} placeholder="e.g. Ethiopia Yirgacheffe" value={form.name} onChange={e => set("name", e.target.value)} /></div>
<div className="mb-4"><label className={labelCls}>Roastery</label>
@@ -58,20 +58,20 @@ export default function BeanForm({ onSave, onClose, initial }) {
</div>
<div className="mb-4">
<label className={labelCls}>Bean Photo</label>
<div className={`relative border-2 rounded-lg text-center cursor-pointer transition-all overflow-hidden ${form.image ? "border-[#E8DFD3] p-0" : "border-dashed border-[#E8DFD3] p-5 hover:border-[#8B6914]"}`}>
<div className={`relative border-2 rounded-lg text-center cursor-pointer transition-all overflow-hidden ${form.image ? "border-[#E8DFD3] dark:border-[#3B2217] p-0" : "border-dashed border-[#E8DFD3] dark:border-[#3B2217] p-5 hover:border-[#8B6914] dark:hover:border-[#D4A325]"}`}>
{form.image ? (
<><img src={form.image} alt="Bean" className="w-full h-40 object-cover rounded-lg block" />
<button className="absolute top-2 right-2 w-7 h-7 rounded-full bg-[rgba(44,24,16,0.7)] text-white border-none text-sm cursor-pointer flex items-center justify-center"
<button className="absolute top-2 right-2 w-7 h-7 rounded-full bg-[rgba(44,24,16,0.7)] dark:bg-[rgba(250,246,241,0.8)] text-white dark:text-[#2C1810] border-none text-sm cursor-pointer flex items-center justify-center font-bold"
onClick={(e) => { e.stopPropagation(); set("image", ""); }} type="button">×</button></>
) : (<><div className="text-3xl mb-1.5 opacity-40">📷</div><div className="text-xs text-[#9C8B7A]">Tap to upload a photo</div></>)}
) : (<><div className="text-3xl mb-1.5 opacity-40 dark:opacity-75">📷</div><div className="text-xs text-[#9C8B7A] dark:text-[#C8B9A6]">Tap to upload a photo</div></>)}
<input type="file" accept="image/*" onChange={handleImage} className="absolute inset-0 opacity-0 cursor-pointer" />
</div>
<div className="text-[11px] text-[#9C8B7A] mt-1">Max 2 MB · JPG, PNG, or WebP</div>
<div className="text-[11px] text-[#9C8B7A] dark:text-[#C8B9A6] mt-1">Max 2 MB · JPG, PNG, or WebP</div>
</div>
<div className="mb-4"><label className={labelCls}>Tasting Notes</label>
<textarea className={`${inputCls} resize-y min-h-[80px]`} placeholder="e.g. Citrus, dark chocolate, floral…" value={form.tastingNotes || ""} onChange={e => set("tastingNotes", e.target.value)} />
<div className="text-[11px] text-[#9C8B7A] mt-1">Flavour profile from the bag or your own impressions</div></div>
<button className="w-full py-3.5 border-none rounded-xl bg-[#2C1810] text-[#FAF6F1] text-sm font-semibold cursor-pointer mt-2"
<div className="text-[11px] text-[#9C8B7A] dark:text-[#C8B9A6] mt-1">Flavour profile from the bag or your own impressions</div></div>
<button className="w-full py-3.5 border-none rounded-xl bg-[#2C1810] dark:bg-[#FAF6F1] text-[#FAF6F1] dark:text-[#2C1810] text-sm font-semibold cursor-pointer mt-2"
style={{ opacity: canSave ? 1 : 0.4, cursor: canSave ? "pointer" : "not-allowed" }} disabled={!canSave}
onClick={() => { if (canSave) handleClose(() => onSave(form)); }}>
{initial ? "Save Changes" : "Add to Library"}

View File

@@ -1,4 +1,3 @@
import React from "react";
import { Home, Coffee, Plus, Bookmark, User } from "lucide-react";
export default function BottomNav({ view, setView, setSelectedBean, onCreatePress }) {

View File

@@ -1,4 +1,3 @@
import React from "react";
import { METHOD_COLORS, METHOD_ICONS, METHOD_LABELS } from "../constants";
export default function BrewCard({ log, beanName }) {
@@ -10,11 +9,11 @@ export default function BrewCard({ log, beanName }) {
return (
<div className="relative mb-3">
<div className="brew-method-bar" style={{ background: color }} />
<div className="bg-white rounded-xl shadow-[0_1px_3px_rgba(44,24,16,0.06),0_4px_12px_rgba(44,24,16,0.04)] p-[18px] pl-6 border border-[#E8DFD3]">
<div className="bg-white dark:bg-[#22120B] rounded-xl 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] pl-6 border border-[#E8DFD3] dark:border-[#3B2217] transition-colors duration-200">
<div className="flex justify-between items-start">
<div>
<div className="text-[11px] text-[#9C8B7A] tracking-wide">{new Date(log.createdAt).toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })}</div>
{beanName && <div className="text-sm font-semibold text-[#2C1810] mt-0.5">{beanName}</div>}
<div className="text-[11px] text-[#9C8B7A] dark:text-[#C8B9A6] tracking-wide">{new Date(log.createdAt).toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" })}</div>
{beanName && <div className="text-sm font-semibold text-[#2C1810] dark:text-[#FAF6F1] mt-0.5">{beanName}</div>}
</div>
<div className="text-[11px] font-semibold uppercase tracking-widest" style={{ color }}>{METHOD_ICONS[log.method]} {METHOD_LABELS[log.method]}</div>
</div>
@@ -22,14 +21,14 @@ export default function BrewCard({ log, beanName }) {
<div className="flex flex-wrap gap-3 mt-2.5">
{allFields.map(([k, v]) => (
<div key={k} className="text-xs">
<div className="text-[#9C8B7A] text-[10px] uppercase tracking-wide">{fieldLabels[k] || k}</div>
<div className="font-semibold mt-0.5">{v}</div>
<div className="text-[#9C8B7A] dark:text-[#C8B9A6] text-[10px] uppercase tracking-wide">{fieldLabels[k] || k}</div>
<div className="font-semibold mt-0.5 text-[#2C1810] dark:text-[#FAF6F1]">{v}</div>
</div>
))}
</div>
)}
{log.recipeDetails && <div className="text-[13px] text-[#6B5744] mt-2.5 italic leading-relaxed">📝 {log.recipeDetails}</div>}
{log.tasteNotes && <div className="text-[13px] text-[#6B5744] mt-1.5 italic leading-relaxed">👅 {log.tasteNotes}</div>}
{log.recipeDetails && <div className="text-[13px] text-[#6B5744] dark:text-[#C8B9A6] mt-2.5 italic leading-relaxed">📝 {log.recipeDetails}</div>}
{log.tasteNotes && <div className="text-[13px] text-[#6B5744] dark:text-[#C8B9A6] mt-1.5 italic leading-relaxed">👅 {log.tasteNotes}</div>}
</div>
</div>
);

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { METHODS, METHOD_LABELS, METHOD_ICONS, METHOD_COLORS, inputCls, labelCls } from "../constants";
export default function BrewForm({ beans, onSave, onClose }) {
@@ -30,17 +30,17 @@ export default function BrewForm({ beans, onSave, onClose }) {
onClick={() => handleClose()}
>
<div
className={`bg-[#FAF6F1] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8 ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8 ${active ? "translate-y-0" : "translate-y-full"}`}
onClick={e => e.stopPropagation()}
>
<div className="w-9 h-1 bg-[#E8DFD3] rounded mx-auto my-3" />
<div className="font-serif text-xl font-semibold text-[#2C1810] mb-5">Log a Brew</div>
<div className="text-center py-12 text-[#9C8B7A]">
<div className="text-4xl mb-3 opacity-50">🫘</div>
<h3 className="text-base mb-1.5 text-[#6B5744]">No beans yet</h3>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto my-3" />
<div className="font-serif text-xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-5">Log a Brew</div>
<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 text-[#6B5744] dark:text-[#FAF6F1]">No beans yet</h3>
<p className="text-[13px] leading-relaxed">Add a bean to your library first.</p>
</div>
<button className="w-full py-3.5 border border-[#E8DFD3] rounded-xl text-sm font-semibold text-[#6B5744] bg-transparent cursor-pointer" onClick={() => handleClose()}>Close</button>
<button className="w-full py-3.5 border border-[#E8DFD3] dark:border-[#3B2217] rounded-xl text-sm font-semibold text-[#6B5744] dark:text-[#C8B9A6] bg-transparent cursor-pointer hover:bg-[#F3EDE4] dark:hover:bg-[#2C1810]" onClick={() => handleClose()}>Close</button>
</div>
</div>
);
@@ -75,15 +75,15 @@ export default function BrewForm({ beans, onSave, onClose }) {
onClick={() => handleClose()}
>
<div
className={`bg-[#FAF6F1] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8 ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[24px] w-full max-w-[480px] max-h-[88vh] overflow-y-auto transition-transform duration-200 ease-in-out px-5 pb-8 ${active ? "translate-y-0" : "translate-y-full"}`}
onClick={e => e.stopPropagation()}
>
<div className="w-9 h-1 bg-[#E8DFD3] rounded mx-auto my-3" />
<div className="font-serif text-xl font-semibold text-[#2C1810] mb-5">Log a Brew</div>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto my-3" />
<div className="font-serif text-xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-5">Log a Brew</div>
<div className="flex gap-1.5 mb-5">
{METHODS.map(m => (
<button key={m}
className={`flex-1 py-3 px-2 border-2 bg-white rounded-xl cursor-pointer text-center text-xs font-semibold transition-all ${method === m ? "border-current" : "border-[#E8DFD3] text-[#9C8B7A]"}`}
className={`flex-1 py-3 px-2 border-2 bg-white dark:bg-[#22120B] rounded-xl cursor-pointer text-center text-xs font-semibold transition-all ${method === m ? "border-current" : "border-[#E8DFD3] dark:border-[#3B2217] text-[#9C8B7A] dark:text-[#C8B9A6]"}`}
style={{ color: method === m ? METHOD_COLORS[m] : undefined }}
onClick={() => setMethod(m)}>
<div className="text-xl mb-1">{METHOD_ICONS[m]}</div>{METHOD_LABELS[m]}
@@ -92,7 +92,7 @@ export default function BrewForm({ beans, onSave, onClose }) {
</div>
<div className="mb-4"><label className={labelCls}>Bean *</label>
<select className={inputCls} value={form.beanId} onChange={e => set("beanId", e.target.value)}>
{beans.map(b => <option key={b.id} value={b.id}>{b.name}{b.roastery ? `${b.roastery}` : ""}</option>)}
{beans.map(b => <option key={b.id} value={b.id} className="dark:bg-[#150B07]">{b.name}{b.roastery ? `${b.roastery}` : ""}</option>)}
</select></div>
<div className="flex flex-wrap gap-2.5 mb-4">
{fields[method].map(f => (
@@ -100,7 +100,7 @@ export default function BrewForm({ beans, onSave, onClose }) {
<label className={labelCls}>{f.label}</label>
<input className={inputCls} type={f.type || "text"} placeholder={f.placeholder}
value={form[f.key] || ""} onChange={e => set(f.key, e.target.value)} />
{f.hint && <div className="text-[11px] text-[#9C8B7A] mt-1">{f.hint}</div>}
{f.hint && <div className="text-[11px] text-[#9C8B7A] dark:text-[#C8B9A6] mt-1">{f.hint}</div>}
</div>
))}
</div>
@@ -111,7 +111,7 @@ export default function BrewForm({ beans, onSave, onClose }) {
<div className="mb-4"><label className={labelCls}>Taste Notes</label>
<input className={inputCls} placeholder="e.g. citrus, chocolate, floral"
value={form.tasteNotes || ""} onChange={e => set("tasteNotes", e.target.value)} /></div>
<button className="w-full py-3.5 border-none rounded-xl bg-[#2C1810] text-[#FAF6F1] text-sm font-semibold cursor-pointer hover:opacity-90 mt-2"
<button className="w-full py-3.5 border-none rounded-xl bg-[#2C1810] dark:bg-[#FAF6F1] text-[#FAF6F1] dark:text-[#2C1810] text-sm font-semibold cursor-pointer hover:opacity-90 mt-2"
onClick={() => handleClose(() => onSave({ ...form, method }))}>Save Brew Log</button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
export default function CreateModal({ onClose, onAddBean, onAddBrew }) {
const [active, setActive] = useState(false);
@@ -25,28 +25,28 @@ export default function CreateModal({ onClose, onAddBean, onAddBrew }) {
onClick={() => handleClose()}
>
<div
className={`bg-[#FAF6F1] rounded-t-[28px] w-full max-w-[480px] px-5 pb-10 transition-transform duration-200 ease-in-out ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[28px] w-full max-w-[480px] px-5 pb-10 transition-transform duration-200 ease-in-out ${active ? "translate-y-0" : "translate-y-full"}`}
onClick={e => e.stopPropagation()}
>
<div className="w-9 h-1 bg-[#E8DFD3] rounded mx-auto mt-3 mb-6" />
<div className="font-serif text-xl font-semibold text-[#2C1810] mb-6">What would you like to add?</div>
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto mt-3 mb-6" />
<div className="font-serif text-xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-6">What would you like to add?</div>
<div className="flex flex-col gap-3">
<button
className="flex items-center gap-4 px-5 py-4 bg-white border border-[#E8DFD3] rounded-2xl text-left cursor-pointer hover:shadow-[0_4px_16px_rgba(44,24,16,0.08)] transition-all active:scale-[0.98]"
className="flex items-center gap-4 px-5 py-4 bg-white dark:bg-[#22120B] border border-[#E8DFD3] dark:border-[#3B2217] rounded-2xl text-left cursor-pointer hover:shadow-[0_4px_16px_rgba(44,24,16,0.08)] dark:hover:shadow-[0_4px_16px_rgba(0,0,0,0.3)] transition-all active:scale-[0.98]"
onClick={() => handleClose(onAddBrew)}>
<div className="w-12 h-12 rounded-xl bg-[#FAF6F1] flex items-center justify-center text-2xl flex-shrink-0"></div>
<div className="w-12 h-12 rounded-xl bg-[#FAF6F1] dark:bg-[#2C1810] flex items-center justify-center text-2xl flex-shrink-0"></div>
<div>
<div className="font-semibold text-[#2C1810] text-sm">Log a Brew</div>
<div className="text-xs text-[#9C8B7A] mt-0.5">Record a brewing session with recipe details</div>
<div className="font-semibold text-[#2C1810] dark:text-[#FAF6F1] text-sm">Log a Brew</div>
<div className="text-xs text-[#9C8B7A] dark:text-[#C8B9A6] mt-0.5">Record a brewing session with recipe details</div>
</div>
</button>
<button
className="flex items-center gap-4 px-5 py-4 bg-white border border-[#E8DFD3] rounded-2xl text-left cursor-pointer hover:shadow-[0_4px_16px_rgba(44,24,16,0.08)] transition-all active:scale-[0.98]"
className="flex items-center gap-4 px-5 py-4 bg-white dark:bg-[#22120B] border border-[#E8DFD3] dark:border-[#3B2217] rounded-2xl text-left cursor-pointer hover:shadow-[0_4px_16px_rgba(44,24,16,0.08)] dark:hover:shadow-[0_4px_16px_rgba(0,0,0,0.3)] transition-all active:scale-[0.98]"
onClick={() => handleClose(onAddBean)}>
<div className="w-12 h-12 rounded-xl bg-[#FAF6F1] flex items-center justify-center text-2xl flex-shrink-0">🫘</div>
<div className="w-12 h-12 rounded-xl bg-[#FAF6F1] dark:bg-[#2C1810] flex items-center justify-center text-2xl flex-shrink-0">🫘</div>
<div>
<div className="font-semibold text-[#2C1810] text-sm">Add Bean</div>
<div className="text-xs text-[#9C8B7A] mt-0.5">Add a new coffee bean to your library</div>
<div className="font-semibold text-[#2C1810] dark:text-[#FAF6F1] text-sm">Add Bean</div>
<div className="text-xs text-[#9C8B7A] dark:text-[#C8B9A6] mt-0.5">Add a new coffee bean to your library</div>
</div>
</button>
</div>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
export default function IosPromptModal() {
const [show, setShow] = useState(false);
@@ -39,16 +39,16 @@ export default function IosPromptModal() {
onClick={handleClose}
>
<div
className={`bg-[#FAF6F1] rounded-t-[28px] w-full max-w-[480px] px-6 pb-8 pt-4 relative shadow-[0_-8px_30px_rgba(44,24,16,0.15)] transition-transform duration-200 ease-in-out ${active ? "translate-y-0" : "translate-y-full"}`}
className={`bg-[#FAF6F1] dark:bg-[#150B07] rounded-t-[28px] w-full max-w-[480px] px-6 pb-8 pt-4 relative shadow-[0_-8px_30px_rgba(44,24,16,0.15)] dark:shadow-none transition-transform duration-200 ease-in-out ${active ? "translate-y-0" : "translate-y-full"}`}
onClick={e => e.stopPropagation()}
>
{/* Handle bar */}
<div className="w-9 h-1 bg-[#E8DFD3] rounded mx-auto mb-6" />
<div className="w-9 h-1 bg-[#E8DFD3] dark:bg-[#3B2217] rounded mx-auto mb-6" />
{/* Close Button */}
<button
onClick={handleClose}
className="absolute right-5 top-5 w-8 h-8 flex items-center justify-center rounded-full bg-white border border-[#E8DFD3] text-[#9C8B7A] hover:text-[#2C1810] transition-colors cursor-pointer text-lg font-semibold"
className="absolute right-5 top-5 w-8 h-8 flex items-center justify-center rounded-full bg-white dark:bg-[#22120B] border border-[#E8DFD3] dark:border-[#3B2217] text-[#9C8B7A] dark:text-[#C8B9A6] hover:text-[#2C1810] dark:hover:text-[#FAF6F1] transition-colors cursor-pointer text-lg font-semibold"
aria-label="Close"
>
&times;
@@ -57,28 +57,28 @@ export default function IosPromptModal() {
{/* Content */}
<div className="flex flex-col items-center text-center">
{/* Brew Logo */}
<div className="w-16 h-16 bg-white border border-[#E8DFD3] rounded-2xl flex items-center justify-center p-3 shadow-sm mb-3">
<div className="w-16 h-16 bg-white dark:bg-[#22120B] border border-[#E8DFD3] dark:border-[#3B2217] rounded-2xl flex items-center justify-center p-3 shadow-sm mb-3">
<img src="/favicon.svg" alt="Brew Logo" className="w-full h-full object-contain" />
</div>
{/* Brew Text */}
<div className="font-serif text-2xl font-bold text-[#2C1810] mb-3">Brew</div>
<div className="font-serif text-2xl font-bold text-[#2C1810] dark:text-[#FAF6F1] mb-3">Brew</div>
{/* Description */}
<p className="text-sm text-[#6B5744] leading-relaxed mb-6 px-4">
<p className="text-sm text-[#6B5744] dark:text-[#C8B9A6] leading-relaxed mb-6 px-4">
Open this page on safari and then install this app to your homescreen for a better experience.
</p>
{/* Instruction */}
<div className="w-full bg-white border border-[#E8DFD3] rounded-2xl p-4 flex items-center justify-center gap-3">
<span className="text-sm text-[#2C1810] font-medium flex items-center justify-center flex-wrap">
<div className="w-full bg-white dark:bg-[#22120B] border border-[#E8DFD3] dark:border-[#3B2217] rounded-2xl p-4 flex items-center justify-center gap-3">
<span className="text-sm text-[#2C1810] dark:text-[#FAF6F1] font-medium flex items-center justify-center flex-wrap">
Tap
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#8B6914" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="inline-block mx-1.5 align-middle">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#D4A325" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="inline-block mx-1.5 align-middle">
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
<polyline points="16 6 12 2 8 6" />
<line x1="12" y1="2" x2="12" y2="15" />
</svg>
then <span className="font-bold text-[#8B6914] ml-1">"Add to Home Screen"</span>
then <span className="font-bold text-[#8B6914] dark:text-[#D4A325] ml-1">"Add to Home Screen"</span>
</span>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { RefreshCw } from "lucide-react";
export default function UpdatePrompt() {

View File

@@ -4,5 +4,5 @@ export const METHOD_LABELS = { pourover: "Pour Over", espresso: "Espresso", cold
export const METHOD_ICONS = { pourover: "☕", espresso: "⚡", coldbrew: "❄️" };
export const METHOD_COLORS = { pourover: "#8B6914", espresso: "#5C3317", coldbrew: "#2F4F6F" };
export const inputCls = "w-full px-3.5 py-3 border border-[#E8DFD3] rounded-lg bg-white text-sm text-[#2C1810] transition-colors outline-none focus:border-[#8B6914]";
export const labelCls = "block text-[10px] font-semibold uppercase tracking-wider text-[#6B5744] mb-1.5";
export const inputCls = "w-full px-3.5 py-3 border border-[#E8DFD3] dark:border-[#3B2217] rounded-lg bg-white dark:bg-[#150B07] text-sm text-[#2C1810] dark:text-[#FAF6F1] transition-colors outline-none focus:border-[#8B6914] dark:focus:border-[#D4A325]";
export const labelCls = "block text-[10px] font-semibold uppercase tracking-wider text-[#6B5744] dark:text-[#C8B9A6] mb-1.5";

View File

@@ -1,5 +1,7 @@
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-brew-bg: #FAF6F1;
--color-brew-bg2: #F3EDE4;
@@ -40,11 +42,21 @@
background-color: #F3EDE4;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background-color 0.2s ease, color 0.2s ease;
}
html.dark {
background-color: #0E0704;
}
body {
margin: 0;
background-color: #F3EDE4;
transition: background-color 0.2s ease, color 0.2s ease;
}
html.dark body {
background-color: #0E0704;
}
#root {

View File

@@ -3,11 +3,14 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { AuthProvider } from './AuthContext.jsx'
import { ThemeProvider } from './ThemeContext.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<AuthProvider>
<App />
<ThemeProvider>
<App />
</ThemeProvider>
</AuthProvider>
</StrictMode>,
)

View File

@@ -22,15 +22,15 @@ export default function Login({ onLoginSuccess }) {
};
return (
<div className="px-5 pt-12 pb-8 max-w-[480px] mx-auto">
<div className="px-5 pt-12 pb-8 max-w-[480px] mx-auto transition-colors duration-200">
<div className="mb-8 text-center">
<div className="text-5xl mb-3"></div>
<h1 className="font-serif text-3xl font-semibold text-[#2C1810] mb-1">Brew Journal</h1>
<p className="text-sm text-[#9C8B7A]">Sign in to your coffee logbook</p>
<h1 className="font-serif text-3xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-1">Brew Journal</h1>
<p className="text-sm text-[#9C8B7A] dark:text-[#C8B9A6]">Sign in to your coffee logbook</p>
</div>
{error && (
<div className="bg-[rgba(180,64,64,0.08)] border border-[rgba(180,64,64,0.2)] text-[#B44040] text-sm px-4 py-3 rounded-lg mb-4">
<div className="bg-[rgba(180,64,64,0.08)] dark:bg-[rgba(229,91,91,0.08)] border border-[rgba(180,64,64,0.2)] dark:border-[rgba(229,91,91,0.2)] text-[#B44040] dark:text-[#E55B5B] text-sm px-4 py-3 rounded-lg mb-4">
{error}
</div>
)}
@@ -61,7 +61,7 @@ export default function Login({ onLoginSuccess }) {
<button
type="submit"
disabled={loading}
className="w-full py-3.5 border-none rounded-lg bg-[#2C1810] text-[#FAF6F1] text-sm font-semibold tracking-wide cursor-pointer transition-opacity mt-1"
className="w-full py-3.5 border-none rounded-lg bg-[#2C1810] dark:bg-[#FAF6F1] text-[#FAF6F1] dark:text-[#2C1810] text-sm font-semibold tracking-wide cursor-pointer transition-colors mt-1"
style={{ opacity: loading ? 0.6 : 1, cursor: loading ? 'not-allowed' : 'pointer' }}
>
{loading ? 'Signing in…' : 'Sign In'}

View File

@@ -1,19 +1,29 @@
import React, { useContext } from "react";
/* global __APP_VERSION__ */
import { useContext } from "react";
import { AuthContext } from "../AuthContext";
import { useTheme } from "../ThemeContext";
export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus }) {
const { logout } = useContext(AuthContext);
const { theme, setTheme } = useTheme();
const themeOptions = [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
{ value: "system", label: "System" }
];
return (
<div>
<div className="transition-colors duration-200">
{/* Avatar & Name */}
<div className="flex flex-col items-center pt-6 pb-8">
<div className="w-20 h-20 rounded-full bg-[#2C1810] flex items-center justify-center text-3xl text-[#FAF6F1] font-serif font-bold mb-3">
<div className="w-20 h-20 rounded-full bg-[#2C1810] dark:bg-[#D4A325] flex items-center justify-center text-3xl text-[#FAF6F1] dark:text-[#2C1810] font-serif font-bold mb-3 shadow-sm">
{user?.username?.[0]?.toUpperCase() || "☕"}
</div>
<div className="font-serif text-xl font-semibold text-[#2C1810]">{user?.username}</div>
<div className="text-sm text-[#9C8B7A] mt-0.5">{user?.email}</div>
<div className="font-serif text-xl font-semibold text-[#2C1810] dark:text-[#FAF6F1]">{user?.username}</div>
<div className="text-sm text-[#9C8B7A] dark:text-[#C8B9A6] mt-0.5">{user?.email}</div>
{showSyncedStatus && (
<div className={`flex items-center gap-1.5 text-[11px] font-medium px-3 py-1.5 rounded-full mt-3 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.5 text-[11px] font-medium px-3 py-1.5 rounded-full mt-3 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)]"}`}>
<span className={`text-[10px] ${syncing ? "animate-sync-pulse" : ""}`}></span>
<span>{isOnline ? (syncing ? "Syncing…" : "All data synced") : "Offline — saved locally"}</span>
</div>
@@ -22,41 +32,76 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
{/* Account section */}
<div className="mb-6">
<div className="text-[11px] font-semibold uppercase tracking-widest text-[#9C8B7A] mb-2 px-1">Account</div>
<div className="bg-white rounded-2xl border border-[#E8DFD3] overflow-hidden">
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-[#F3EDE4]">
<div className="text-[11px] font-semibold uppercase tracking-widest text-[#9C8B7A] dark:text-[#C8B9A6] mb-2 px-1">Account</div>
<div className="bg-white dark:bg-[#22120B] rounded-2xl border border-[#E8DFD3] dark:border-[#3B2217] overflow-hidden shadow-sm transition-colors duration-200">
<div className="flex items-center gap-3 px-4 py-3.5 border-b border-[#F3EDE4] dark:border-[#3B2217]">
<span className="text-lg">👤</span>
<div>
<div className="text-xs text-[#9C8B7A]">Username</div>
<div className="text-sm font-medium text-[#2C1810]">{user?.username}</div>
<div className="text-xs text-[#9C8B7A] dark:text-[#C8B9A6]">Username</div>
<div className="text-sm font-medium text-[#2C1810] dark:text-[#FAF6F1]">{user?.username}</div>
</div>
</div>
<div className="flex items-center gap-3 px-4 py-3.5">
<span className="text-lg"></span>
<div>
<div className="text-xs text-[#9C8B7A]">Email</div>
<div className="text-sm font-medium text-[#2C1810]">{user?.email}</div>
<div className="text-xs text-[#9C8B7A] dark:text-[#C8B9A6]">Email</div>
<div className="text-sm font-medium text-[#2C1810] dark:text-[#FAF6F1]">{user?.email}</div>
</div>
</div>
</div>
</div>
{/* Appearance Settings section */}
<div className="mb-6">
<div className="text-[11px] font-semibold uppercase tracking-widest text-[#9C8B7A] dark:text-[#C8B9A6] mb-2 px-1">Appearance</div>
<div className="bg-white dark:bg-[#22120B] rounded-2xl border border-[#E8DFD3] dark:border-[#3B2217] p-4 flex flex-col gap-3 shadow-sm transition-colors duration-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-lg">🌓</span>
<div>
<div className="text-sm font-medium text-[#2C1810] dark:text-[#FAF6F1]">Theme</div>
<div className="text-xs text-[#9C8B7A] dark:text-[#C8B9A6]">Customize your viewing experience</div>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-1 bg-[#F3EDE4] dark:bg-[#2C1810] p-1 rounded-xl transition-colors duration-200">
{themeOptions.map((opt) => {
const active = theme === opt.value;
return (
<button
key={opt.value}
onClick={() => setTheme(opt.value)}
className={`py-2 px-3 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
active
? "bg-[#2C1810] text-[#FAF6F1] dark:bg-[#FAF6F1] dark:text-[#2C1810] shadow-sm"
: "text-[#6B5744] hover:text-[#2C1810] dark:text-[#C8B9A6] dark:hover:text-[#FAF6F1]"
}`}
>
{opt.label}
</button>
);
})}
</div>
</div>
</div>
{/* App info */}
<div className="mb-6">
<div className="text-[11px] font-semibold uppercase tracking-widest text-[#9C8B7A] mb-2 px-1">App</div>
<div className="bg-white rounded-2xl border border-[#E8DFD3] overflow-hidden">
<div className="flex items-center justify-between px-4 py-3.5 border-b border-[#F3EDE4]">
<div className="text-[11px] font-semibold uppercase tracking-widest text-[#9C8B7A] dark:text-[#C8B9A6] mb-2 px-1">App</div>
<div className="bg-white dark:bg-[#22120B] rounded-2xl border border-[#E8DFD3] dark:border-[#3B2217] overflow-hidden shadow-sm transition-colors duration-200">
<div className="flex items-center justify-between px-4 py-3.5 border-b border-[#F3EDE4] dark:border-[#3B2217]">
<div className="flex items-center gap-3">
<span className="text-lg"></span>
<div className="text-sm font-medium text-[#2C1810]">Brew Journal</div>
<div className="text-sm font-medium text-[#2C1810] dark:text-[#FAF6F1]">Brew Journal</div>
</div>
<div className="text-xs text-[#9C8B7A] font-mono">{__APP_VERSION__}</div>
<div className="text-xs text-[#9C8B7A] dark:text-[#C8B9A6] font-mono">{__APP_VERSION__}</div>
</div>
<div className="flex items-center gap-3 px-4 py-3.5">
<span className="text-lg">🗄</span>
<div>
<div className="text-xs text-[#9C8B7A]">Storage</div>
<div className="text-sm font-medium text-[#2C1810]">Local + PostgreSQL</div>
<div className="text-xs text-[#9C8B7A] dark:text-[#C8B9A6]">Storage</div>
<div className="text-sm font-medium text-[#2C1810] dark:text-[#FAF6F1]">Local + PostgreSQL</div>
</div>
</div>
</div>
@@ -65,7 +110,7 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
{/* Sign out */}
<button
onClick={() => logout()}
className="w-full py-3.5 border border-[#B44040] rounded-2xl text-sm font-semibold text-[#B44040] bg-transparent cursor-pointer hover:bg-[rgba(180,64,64,0.05)] transition-colors">
className="w-full py-3.5 border border-[#B44040] dark:border-[#E55B5B] rounded-2xl text-sm font-semibold text-[#B44040] dark:text-[#E55B5B] bg-transparent cursor-pointer hover:bg-[rgba(180,64,64,0.05)] dark:hover:bg-[rgba(229,91,91,0.05)] transition-all">
Sign Out
</button>
</div>

View File

@@ -36,20 +36,20 @@ export default function Register({ onRegisterSuccess }) {
};
return (
<div className="px-5 pt-12 pb-8 max-w-[480px] mx-auto">
<div className="px-5 pt-12 pb-8 max-w-[480px] mx-auto transition-colors duration-200">
<div className="mb-8 text-center">
<div className="text-5xl mb-3">🫘</div>
<h1 className="font-serif text-3xl font-semibold text-[#2C1810] mb-1">Create Account</h1>
<p className="text-sm text-[#9C8B7A]">Start tracking your coffee journey</p>
<h1 className="font-serif text-3xl font-semibold text-[#2C1810] dark:text-[#FAF6F1] mb-1">Create Account</h1>
<p className="text-sm text-[#9C8B7A] dark:text-[#C8B9A6]">Start tracking your coffee journey</p>
</div>
{error && (
<div className="bg-[rgba(180,64,64,0.08)] border border-[rgba(180,64,64,0.2)] text-[#B44040] text-sm px-4 py-3 rounded-lg mb-4">
<div className="bg-[rgba(180,64,64,0.08)] dark:bg-[rgba(229,91,91,0.08)] border border-[rgba(180,64,64,0.2)] dark:border-[rgba(229,91,91,0.2)] text-[#B44040] dark:text-[#E55B5B] text-sm px-4 py-3 rounded-lg mb-4">
{error}
</div>
)}
{success && (
<div className="bg-[rgba(74,124,89,0.08)] border border-[rgba(74,124,89,0.2)] text-[#4A7C59] text-sm px-4 py-3 rounded-lg mb-4">
<div className="bg-[rgba(74,124,89,0.08)] dark:bg-[rgba(108,178,129,0.08)] border border-[rgba(74,124,89,0.2)] dark:border-[rgba(108,178,129,0.2)] text-[#4A7C59] dark:text-[#6CB281] text-sm px-4 py-3 rounded-lg mb-4">
{success}
</div>
)}
@@ -102,7 +102,7 @@ export default function Register({ onRegisterSuccess }) {
<button
type="submit"
disabled={loading}
className="w-full py-3.5 border-none rounded-lg bg-[#2C1810] text-[#FAF6F1] text-sm font-semibold tracking-wide mt-1 cursor-pointer transition-opacity"
className="w-full py-3.5 border-none rounded-lg bg-[#2C1810] dark:bg-[#FAF6F1] text-[#FAF6F1] dark:text-[#2C1810] text-sm font-semibold tracking-wide mt-1 cursor-pointer transition-colors"
style={{ opacity: loading ? 0.6 : 1, cursor: loading ? 'not-allowed' : 'pointer' }}
>
{loading ? 'Creating account…' : 'Create Account'}