diff --git a/index.html b/index.html index 904fde8..56c24bb 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,18 @@ brew +
diff --git a/src/App.jsx b/src/App.jsx index dcefd79..997cb51 100644 --- a/src/App.jsx +++ b/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 = () => ( +
+
Loading…
+
+); + // ─── 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 = () => ( -
-
Loading…
-
- ); if (loading) return ; if (!token || !user) { return ( -
+
{authView === "login" ? ( <> {}} />
-

Don't have an account?{' '} - +

Don't have an account?{' '} +

@@ -144,8 +145,8 @@ export default function CoffeeLogbook() { <> setAuthView("login")} />
-

Already have an account?{' '} - +

Already have an account?{' '} +

@@ -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 ( -
+
{/* Header */} -
+
-
{pageSubtitles[view] || "Coffee Logbook"}
-

{pageTitles[view] || "Brew Journal"}

+
{pageSubtitles[view] || "Coffee Logbook"}
+

{pageTitles[view] || "Brew Journal"}

{showSyncedStatus && ( -
{isOnline ? (syncing ? "Syncing..." : "Synced") : "Offline"} @@ -213,27 +214,27 @@ export default function CoffeeLogbook() {
{[{ num: beans.length, label: "Beans" }, { num: brewLogs.length, label: "Brews" }, { num: new Set(brewLogs.map(l => l.beanId)).size, label: "Tried" }].map(s => ( -
-
{s.num}
-
{s.label}
+
+
{s.num}
+
{s.label}
))}
-
By Method
+
By Method
{METHODS.map(m => ( -
+
{METHOD_ICONS[m]}
-
{methodCounts[m]}
-
{METHOD_LABELS[m]}
+
{methodCounts[m]}
+
{METHOD_LABELS[m]}
))}
-
Recent Brews
+
Recent Brews
{brewLogs.length === 0 ? ( -
-
📝
-

No brews yet

+
+
📝
+

No brews yet

Tap the + button to log your first brew.

) : 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 && (
-
Your Beans ({beans.length})
+
Your Beans ({beans.length})
{beans.length === 0 ? ( -
-
🫘
-

Library is empty

+
+
🫘
+

Library is empty

Tap + to add your first coffee bean.

) : 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 (
setSelectedBean(bean)}>
{bean.image && }
-
{bean.name}
- {bean.roastery &&
{bean.roastery}
} +
{bean.name}
+ {bean.roastery &&
{bean.roastery}
}
- {count} brew{count !== 1 ? "s" : ""} + {count} brew{count !== 1 ? "s" : ""}
- {bean.tastingNotes &&
👅 {bean.tastingNotes}
} + {bean.tastingNotes &&
👅 {bean.tastingNotes}
}
{bean.roastType && {bean.roastType}} - {bean.roastDate && Roasted {bean.roastDate}} + {bean.roastDate && Roasted {bean.roastDate}}
); @@ -301,9 +306,9 @@ export default function CoffeeLogbook() { ))}
{filteredLogs.length === 0 ? ( -
-
📋
-

No logs {brewFilter !== "all" ? `for ${METHOD_LABELS[brewFilter]}` : "yet"}

+
+
📋
+

No logs {brewFilter !== "all" ? `for ${METHOD_LABELS[brewFilter]}` : "yet"}

Start brewing and log your recipes here.

) : filteredLogs.map(log => ( diff --git a/src/ThemeContext.jsx b/src/ThemeContext.jsx new file mode 100644 index 0000000..2556573 --- /dev/null +++ b/src/ThemeContext.jsx @@ -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 ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + return context; +} diff --git a/src/components/BeanDetail.jsx b/src/components/BeanDetail.jsx index 9a7134b..2d9f561 100644 --- a/src/components/BeanDetail.jsx +++ b/src/components/BeanDetail.jsx @@ -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 ( -
- +
+ {bean.image && {bean.name}} -

{bean.name}

- {bean.roastery &&
{bean.roastery}
} - {bean.tastingNotes &&
👅 {bean.tastingNotes}
} +

{bean.name}

+ {bean.roastery &&
{bean.roastery}
} + {bean.tastingNotes &&
👅 {bean.tastingNotes}
}
{bean.roastType && {bean.roastType}} - {bean.roastDate && Roasted {bean.roastDate}} - {beanLogs.length} brew{beanLogs.length !== 1 ? "s" : ""} + {bean.roastDate && Roasted {bean.roastDate}} + {beanLogs.length} brew{beanLogs.length !== 1 ? "s" : ""}
- - + +
-
Brew History
+
Brew History
{beanLogs.length === 0 ? ( -

No brews logged with this bean yet.

+

No brews logged with this bean yet.

) : beanLogs.map(log => )}
diff --git a/src/components/BeanForm.jsx b/src/components/BeanForm.jsx index efab8e8..68b3131 100644 --- a/src/components/BeanForm.jsx +++ b/src/components/BeanForm.jsx @@ -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()} >
e.stopPropagation()} > -
-
{initial ? "Edit Bean" : "Add Bean"}
+
+
{initial ? "Edit Bean" : "Add Bean"}
set("name", e.target.value)} />
@@ -58,20 +58,20 @@ export default function BeanForm({ onSave, onClose, initial }) {
-
+
{form.image ? ( <>Bean - - ) : (<>
📷
Tap to upload a photo
)} + ) : (<>
📷
Tap to upload a photo
)}
-
Max 2 MB · JPG, PNG, or WebP
+
Max 2 MB · JPG, PNG, or WebP