-
🫘
-
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() {
))}
-
📋
-
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.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 ? (
<>

-
>
- ) : (<>
📷
Tap to upload a photo
>)}
+ ) : (<>
📷
Tap to upload a photo
>)}
-
Max 2 MB · JPG, PNG, or WebP
+
Max 2 MB · JPG, PNG, or WebP
-
+
diff --git a/src/components/CreateModal.jsx b/src/components/CreateModal.jsx
index b5fb71c..7915dc0 100644
--- a/src/components/CreateModal.jsx
+++ b/src/components/CreateModal.jsx
@@ -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()}
>
e.stopPropagation()}
>
-
-
What would you like to add?
+
+
What would you like to add?
handleClose(onAddBrew)}>
- ☕
+ ☕
-
Log a Brew
-
Record a brewing session with recipe details
+
Log a Brew
+
Record a brewing session with recipe details
handleClose(onAddBean)}>
- 🫘
+ 🫘
-
Add Bean
-
Add a new coffee bean to your library
+
Add Bean
+
Add a new coffee bean to your library
diff --git a/src/components/IosPromptModal.jsx b/src/components/IosPromptModal.jsx
index 48c5336..2e1bcad 100644
--- a/src/components/IosPromptModal.jsx
+++ b/src/components/IosPromptModal.jsx
@@ -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}
>
e.stopPropagation()}
>
{/* Handle bar */}
-
+
{/* Close Button */}
×
@@ -57,28 +57,28 @@ export default function IosPromptModal() {
{/* Content */}
{/* Brew Logo */}
-
+
{/* Brew Text */}
-
Brew
+
Brew
{/* Description */}
-
+
Open this page on safari and then install this app to your homescreen for a better experience.
{/* Instruction */}
-
-
+
+
Tap
-
diff --git a/src/components/UpdatePrompt.jsx b/src/components/UpdatePrompt.jsx
index d28f33c..b23992d 100644
--- a/src/components/UpdatePrompt.jsx
+++ b/src/components/UpdatePrompt.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from "react";
+import { useState, useEffect } from "react";
import { RefreshCw } from "lucide-react";
export default function UpdatePrompt() {
diff --git a/src/constants.js b/src/constants.js
index 2af906c..2853788 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -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";
diff --git a/src/index.css b/src/index.css
index b628ac6..f92fed4 100644
--- a/src/index.css
+++ b/src/index.css
@@ -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 {
diff --git a/src/main.jsx b/src/main.jsx
index 60c3750..78b572e 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -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(
-
+
+
+
,
)
diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx
index fed1e2c..57927b9 100644
--- a/src/pages/Login.jsx
+++ b/src/pages/Login.jsx
@@ -22,15 +22,15 @@ export default function Login({ onLoginSuccess }) {
};
return (
-
+
☕
-
Brew Journal
-
Sign in to your coffee logbook
+
Brew Journal
+
Sign in to your coffee logbook
{error && (
-
+
{error}
)}
@@ -61,7 +61,7 @@ export default function Login({ onLoginSuccess }) {
{loading ? 'Signing in…' : 'Sign In'}
diff --git a/src/pages/ProfilePage.jsx b/src/pages/ProfilePage.jsx
index b10f983..1def462 100644
--- a/src/pages/ProfilePage.jsx
+++ b/src/pages/ProfilePage.jsx
@@ -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 (
-
+
{/* Avatar & Name */}
-
+
{user?.username?.[0]?.toUpperCase() || "☕"}
-
{user?.username}
-
{user?.email}
+
{user?.username}
+
{user?.email}
{showSyncedStatus && (
-
+
●
{isOnline ? (syncing ? "Syncing…" : "All data synced") : "Offline — saved locally"}
@@ -22,41 +32,76 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
{/* Account section */}
-
Account
-
-
+
Account
+
+
👤
-
Username
-
{user?.username}
+
Username
+
{user?.username}
✉️
-
Email
-
{user?.email}
+
Email
+
{user?.email}
+ {/* Appearance Settings section */}
+
+
Appearance
+
+
+
+
🌓
+
+
Theme
+
Customize your viewing experience
+
+
+
+
+
+ {themeOptions.map((opt) => {
+ const active = theme === opt.value;
+ return (
+ 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}
+
+ );
+ })}
+
+
+
+
{/* App info */}
-
App
-
-
+
App
+
+
☕
-
Brew Journal
+
Brew Journal
-
{__APP_VERSION__}
+
{__APP_VERSION__}
🗄️
-
Storage
-
Local + PostgreSQL
+
Storage
+
Local + PostgreSQL
@@ -65,7 +110,7 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
{/* Sign out */}
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
diff --git a/src/pages/Register.jsx b/src/pages/Register.jsx
index 5ffda2a..ba11293 100644
--- a/src/pages/Register.jsx
+++ b/src/pages/Register.jsx
@@ -36,20 +36,20 @@ export default function Register({ onRegisterSuccess }) {
};
return (
-
+
🫘
-
Create Account
-
Start tracking your coffee journey
+
Create Account
+
Start tracking your coffee journey
{error && (
-
+
{error}
)}
{success && (
-
+
{success}
)}
@@ -102,7 +102,7 @@ export default function Register({ onRegisterSuccess }) {
{loading ? 'Creating account…' : 'Create Account'}