feat: Add search to brew logs
All checks were successful
Deploy Brew Application / deploy (push) Successful in 11s
All checks were successful
Deploy Brew Application / deploy (push) Successful in 11s
This commit is contained in:
74
src/App.jsx
74
src/App.jsx
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useContext, useRef } from "react";
|
||||
import { AuthContext } from "./AuthContext";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
// Import pages/views
|
||||
import Login from "./pages/Login";
|
||||
@@ -72,12 +73,14 @@ export default function CoffeeLogbook() {
|
||||
const [selectedBean, setSelectedBean] = useState(null);
|
||||
const [brewFilter, setBrewFilter] = useState("all");
|
||||
const [editingBean, setEditingBean] = useState(null);
|
||||
const [brewSearchQuery, setBrewSearchQuery] = useState("");
|
||||
|
||||
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [showSyncedStatus, setShowSyncedStatus] = useState(false);
|
||||
|
||||
useEffect(() => { loadData().then(setData); }, []);
|
||||
useEffect(() => { setBrewSearchQuery(""); }, [view]);
|
||||
|
||||
const dataRef = useRef(data);
|
||||
dataRef.current = data;
|
||||
@@ -178,7 +181,37 @@ export default function CoffeeLogbook() {
|
||||
const addBrew = (form) => { persist({ ...data, brewLogs: [...allLogs, { id: uid(), createdAt: Date.now(), ...form, updatedAt: new Date().toISOString(), isDeleted: false }] }); setModal(null); };
|
||||
|
||||
const getBeanName = (id) => allBeans.find(b => b.id === id)?.name || "Unknown Bean";
|
||||
const filteredLogs = (brewFilter === "all" ? brewLogs : brewLogs.filter(l => l.method === brewFilter)).sort((a, b) => b.createdAt - a.createdAt);
|
||||
const filteredLogs = (brewFilter === "all" ? brewLogs : brewLogs.filter(l => l.method === brewFilter))
|
||||
.filter(log => {
|
||||
if (!brewSearchQuery) return true;
|
||||
const dateObj = new Date(log.createdAt);
|
||||
const weekday = dateObj.toLocaleDateString("en-US", { weekday: "long" }).toLowerCase();
|
||||
const month = dateObj.toLocaleDateString("en-US", { month: "long" }).toLowerCase();
|
||||
const monthShort = dateObj.toLocaleDateString("en-US", { month: "short" }).toLowerCase();
|
||||
const dateNum = dateObj.getDate().toString();
|
||||
const year = dateObj.getFullYear().toString();
|
||||
|
||||
const searchableText = [
|
||||
getBeanName(log.beanId).toLowerCase(),
|
||||
METHOD_LABELS[log.method]?.toLowerCase() || "",
|
||||
log.recipeDetails?.toLowerCase() || "",
|
||||
log.tasteNotes?.toLowerCase() || "",
|
||||
log.notes?.toLowerCase() || "",
|
||||
log.grindSize?.toLowerCase() || log.grind?.toLowerCase() || "",
|
||||
log.waterTemp?.toLowerCase() || "",
|
||||
log.brewRatio?.toLowerCase() || log.ratio?.toLowerCase() || "",
|
||||
log.brewTime?.toLowerCase() || log.time?.toLowerCase() || "",
|
||||
weekday,
|
||||
month,
|
||||
monthShort,
|
||||
dateNum,
|
||||
year
|
||||
].join(" ");
|
||||
|
||||
const queryWords = brewSearchQuery.trim().toLowerCase().split(/\s+/);
|
||||
return queryWords.every(word => searchableText.includes(word));
|
||||
})
|
||||
.sort((a, b) => b.createdAt - a.createdAt);
|
||||
const methodCounts = { pourover: 0, espresso: 0, coldbrew: 0 };
|
||||
brewLogs.forEach(l => { if (methodCounts[l.method] !== undefined) methodCounts[l.method]++; });
|
||||
|
||||
@@ -292,6 +325,33 @@ export default function CoffeeLogbook() {
|
||||
{/* ── Brew Logs ── */}
|
||||
{view === "brews" && (
|
||||
<div className="animate-page-enter">
|
||||
{/* Search Bar */}
|
||||
<div className="mb-4 relative">
|
||||
<input
|
||||
type="text"
|
||||
value={brewSearchQuery}
|
||||
onChange={(e) => setBrewSearchQuery(e.target.value)}
|
||||
placeholder="Search brews (e.g. June, espresso, floral...)"
|
||||
className="w-full pl-10 pr-10 py-3.5 bg-white dark:bg-[#22120B] border border-[#E8DFD3] dark:border-[#3B2217] rounded-2xl text-xs placeholder-[#9C8B7A] dark:placeholder-[#C8B9A6] text-[#2C1810] dark:text-[#FAF6F1] shadow-[0_1px_3px_rgba(44,24,16,0.04)] dark:shadow-none focus:outline-none focus:border-[#8B6914] dark:focus:border-[#D4A325] transition-all"
|
||||
/>
|
||||
{/* Search Icon */}
|
||||
<div className="absolute left-3.5 top-1/2 -translate-y-1/2 text-[#9C8B7A] dark:text-[#C8B9A6] flex items-center pointer-events-none">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="opacity-70">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
</div>
|
||||
{/* Clear button */}
|
||||
{brewSearchQuery && (
|
||||
<button
|
||||
onClick={() => setBrewSearchQuery("")}
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 w-6 h-6 flex items-center justify-center rounded-full text-[#9C8B7A] dark:text-[#C8B9A6] hover:text-[#2C1810] dark:hover:text-[#FAF6F1] border-none bg-transparent cursor-pointer text-base font-bold transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1.5 mb-4 overflow-x-auto pb-1">
|
||||
<button className={filterPillCls(brewFilter === "all")} onClick={() => setBrewFilter("all")}>All</button>
|
||||
{METHODS.map(m => (
|
||||
@@ -302,9 +362,15 @@ export default function CoffeeLogbook() {
|
||||
</div>
|
||||
{filteredLogs.length === 0 ? (
|
||||
<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 className="flex justify-center mb-3 text-[#9C8B7A] dark:text-[#C8B9A6] opacity-60">
|
||||
{brewSearchQuery ? <Search size={36} strokeWidth={2} /> : <span className="text-4xl">📋</span>}
|
||||
</div>
|
||||
<h3 className="text-base mb-1.5 font-serif text-[#6B5744] dark:text-[#FAF6F1]">
|
||||
{brewSearchQuery ? "No search results" : `No logs ${brewFilter !== "all" ? `for ${METHOD_LABELS[brewFilter]}` : "yet"}`}
|
||||
</h3>
|
||||
<p className="text-[13px] leading-relaxed">
|
||||
{brewSearchQuery ? "Try refining your search terms or filters." : "Start brewing and log your recipes here."}
|
||||
</p>
|
||||
</div>
|
||||
) : filteredLogs.map(log => (
|
||||
<BrewCard key={log.id} log={log} beanName={getBeanName(log.beanId)} />
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setTheme(opt.value)}
|
||||
className={`py-2 px-3 rounded-lg text-xs font-semibold transition-all cursor-pointer ${
|
||||
className={`py-2 px-3 rounded-xl 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]"
|
||||
|
||||
Reference in New Issue
Block a user