Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf96fb9763 | |||
| f2af73ac69 | |||
| 1195159c3d | |||
| 4e234d075f | |||
| a0b1efd242 | |||
| e808aa8a37 | |||
| 4a9f6b6266 | |||
| 592ccf0a92 | |||
| f8a1f2cbdd | |||
| 1064c724f7 | |||
| 9d98966a8e |
35
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Deploy Brew Application
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: production
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build Frontend
|
||||
env:
|
||||
CI: "true"
|
||||
run: |
|
||||
npm install
|
||||
rm -rf dist/
|
||||
npm run build
|
||||
|
||||
- name: Deploy Frontend Files
|
||||
run: |
|
||||
rm -rf /var/www/brew/dist/
|
||||
cp -r dist/ /var/www/brew/
|
||||
|
||||
- name: Restart Backend with PM2
|
||||
run: |
|
||||
cd server
|
||||
npm install
|
||||
pm2 restart brew-backend || pm2 start index.js --name "brew-backend"
|
||||
58
index.html
@@ -1,28 +1,36 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<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>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/brew_favicons/brew_64.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#2C1810" />
|
||||
<link rel="apple-touch-icon" href="/icon-192.png" />
|
||||
<title>Brew Journal</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) { }
|
||||
})();
|
||||
|
||||
// Prevent pinch-to-zoom gesture on iOS devices
|
||||
document.addEventListener('gesturestart', function (e) {
|
||||
e.preventDefault();
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
BIN
public/brew_favicons/brew_1024.png
Normal file
|
After Width: | Height: | Size: 796 KiB |
BIN
public/brew_favicons/brew_64.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
BIN
public/icon-1024.png
Normal file
|
After Width: | Height: | Size: 731 KiB |
|
Before Width: | Height: | Size: 545 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 545 KiB After Width: | Height: | Size: 164 KiB |
@@ -15,6 +15,7 @@ import BrewCard from "./components/BrewCard";
|
||||
import BottomNav from "./components/BottomNav";
|
||||
import IosPromptModal from "./components/IosPromptModal";
|
||||
import UpdatePrompt from "./components/UpdatePrompt";
|
||||
import SyncIndicator from "./components/SyncIndicator";
|
||||
|
||||
|
||||
// Import constants
|
||||
@@ -196,13 +197,7 @@ export default function CoffeeLogbook() {
|
||||
<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)] 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>
|
||||
</div>
|
||||
)}
|
||||
<SyncIndicator isOnline={isOnline} syncing={syncing} showSyncedStatus={showSyncedStatus} />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -57,8 +57,8 @@ export default function IosPromptModal() {
|
||||
{/* Content */}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{/* Brew Logo */}
|
||||
<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 className="w-16 h-16 bg-white dark:bg-[#22120B] border border-[#E8DFD3] dark:border-[#3B2217] rounded-[22%] flex items-center justify-center overflow-hidden shadow-sm mb-3">
|
||||
<img src="/icon-192.png" alt="Brew Logo" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
|
||||
{/* Brew Text */}
|
||||
|
||||
69
src/components/SyncIndicator.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* SyncIndicator — a minimal coffee-cup icon that communicates sync results.
|
||||
*
|
||||
* Success: outlined cup → steam rises → checkmark appears → fades out (~2.4s)
|
||||
* Offline: outlined cup + subtle "!" warning, stays visible.
|
||||
*/
|
||||
export default function SyncIndicator({ isOnline, syncing, showSyncedStatus }) {
|
||||
const showSuccess = isOnline && showSyncedStatus && !syncing;
|
||||
const showOffline = !isOnline;
|
||||
|
||||
if (!showSuccess && !showOffline) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={showSuccess ? "sync-cup-success" : undefined}
|
||||
title={showOffline ? "Offline — data saved locally" : "All data synced"}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="28"
|
||||
height="28"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{/* ── Cup body ── */}
|
||||
<g className="text-[#9C8B7A] dark:text-[#C8B9A6]">
|
||||
<path
|
||||
d="M5 9h10v6.5a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3V9z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<path
|
||||
d="M15 11h1a2 2 0 0 1 0 4h-1"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* ── Steam lines (success only) ── */}
|
||||
{showSuccess && (
|
||||
<g className="text-[#9C8B7A] dark:text-[#C8B9A6]" stroke="currentColor" strokeWidth="1.2">
|
||||
<path className="sync-steam sync-steam-1" d="M8 7 Q8.7 5.5 8 4.5 Q7.5 3.8 8.2 3" />
|
||||
<path className="sync-steam sync-steam-2" d="M10 6.5 Q10.7 5 10 4 Q9.5 3.3 10.2 2.5" />
|
||||
<path className="sync-steam sync-steam-3" d="M12 7 Q12.7 5.5 12 4.5 Q11.5 3.8 12.2 3" />
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* ── Checkmark (success only) ── */}
|
||||
{showSuccess && (
|
||||
<polyline
|
||||
points="7.5,14 9.5,16 13,12"
|
||||
className="sync-check text-[#4A7C59] dark:text-[#6CB281]"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* ── Warning indicator (offline only) ── */}
|
||||
{showOffline && (
|
||||
<g className="text-[#B44040] dark:text-[#E55B5B]" style={{ opacity: 0.85 }}>
|
||||
<line x1="10" y1="12" x2="10" y2="15.5" stroke="currentColor" strokeWidth="1.8" />
|
||||
<circle cx="10" cy="17.2" r="0.8" fill="currentColor" stroke="none" />
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -37,9 +37,9 @@ export default function UpdatePrompt() {
|
||||
<div
|
||||
className={`fixed inset-x-0 bottom-28 z-[150] flex justify-center px-4 transition-all duration-300 ease-out ${active ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"}`}
|
||||
>
|
||||
<div className="bg-[#2C1810] text-[#FAF6F1] rounded-2xl shadow-[0_8px_32px_rgba(44,24,16,0.35)] flex items-center gap-3 px-4 py-3.5 max-w-[420px] w-full">
|
||||
<div className="bg-[#2C1810] text-[#FAF6F1] rounded-full shadow-[0_8px_32px_rgba(44,24,16,0.35)] flex items-center gap-3 pl-3 pr-4 py-2.5 max-w-[420px] w-full border border-white/5">
|
||||
{/* Icon */}
|
||||
<div className="w-9 h-9 rounded-xl bg-white/10 flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-9 h-9 rounded-full bg-white/10 flex items-center justify-center flex-shrink-0">
|
||||
<RefreshCw size={16} strokeWidth={2.5} className="text-white" />
|
||||
</div>
|
||||
|
||||
@@ -53,13 +53,13 @@ export default function UpdatePrompt() {
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="text-xs text-white/50 hover:text-white/80 transition-colors cursor-pointer px-1 py-1"
|
||||
className="text-xs text-white/50 hover:text-white/80 transition-colors cursor-pointer px-2 py-2"
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
className="text-xs font-semibold bg-white text-[#2C1810] px-3 py-1.5 rounded-lg hover:bg-white/90 transition-colors cursor-pointer"
|
||||
className="text-xs font-semibold bg-white text-[#2C1810] px-4 py-2 rounded-full hover:bg-white/90 transition-colors cursor-pointer"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
touch-action: pan-x pan-y;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
@@ -53,6 +55,8 @@
|
||||
margin: 0;
|
||||
background-color: #F3EDE4;
|
||||
transition: background-color 0.2s ease, color 0.2s ease;
|
||||
touch-action: pan-x pan-y;
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
html.dark body {
|
||||
@@ -138,4 +142,40 @@
|
||||
width: 4px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
/* ── Sync cup indicator ── */
|
||||
.sync-cup-success {
|
||||
animation: sync-cup-fade 2.4s ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes sync-cup-fade {
|
||||
0% { opacity: 0; transform: scale(0.88); }
|
||||
8% { opacity: 1; transform: scale(1); }
|
||||
68% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
|
||||
.sync-steam {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sync-steam-1 { animation: sync-steam-rise 1.4s ease-out 0.1s forwards; }
|
||||
.sync-steam-2 { animation: sync-steam-rise 1.4s ease-out 0.3s forwards; }
|
||||
.sync-steam-3 { animation: sync-steam-rise 1.4s ease-out 0.5s forwards; }
|
||||
|
||||
@keyframes sync-steam-rise {
|
||||
0% { opacity: 0; transform: translateY(0); }
|
||||
25% { opacity: 0.5; }
|
||||
100% { opacity: 0; transform: translateY(-4px); }
|
||||
}
|
||||
|
||||
.sync-check {
|
||||
stroke-dasharray: 14;
|
||||
stroke-dashoffset: 14;
|
||||
animation: sync-check-draw 0.45s ease-out 0.65s forwards;
|
||||
}
|
||||
|
||||
@keyframes sync-check-draw {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function Login({ onLoginSuccess }) {
|
||||
return (
|
||||
<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>
|
||||
<img src="/icon-192.png" alt="Brew Journal" className="w-16 h-16 mx-auto mb-3 rounded-[22%]" />
|
||||
<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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useContext } from "react";
|
||||
import { AuthContext } from "../AuthContext";
|
||||
import { useTheme } from "../ThemeContext";
|
||||
import { User, Mail, Contrast, Coffee, Database, MessageSquare } from "lucide-react";
|
||||
|
||||
export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus }) {
|
||||
const { logout } = useContext(AuthContext);
|
||||
@@ -18,16 +19,14 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
|
||||
{/* Avatar & Name */}
|
||||
<div className="flex flex-col items-center pt-6 pb-8">
|
||||
<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() || "☕"}
|
||||
{user?.username?.[0]?.toUpperCase() || <User size={32} strokeWidth={2} />}
|
||||
</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)] 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>
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Account section */}
|
||||
@@ -35,14 +34,14 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
|
||||
<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>
|
||||
<User size={18} strokeWidth={2} className="text-[#9C8B7A] dark:text-[#C8B9A6] flex-shrink-0" />
|
||||
<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>
|
||||
<Mail size={18} strokeWidth={2} className="text-[#9C8B7A] dark:text-[#C8B9A6] flex-shrink-0" />
|
||||
<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>
|
||||
@@ -51,13 +50,22 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign out */}
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => logout()}
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
<Contrast size={18} strokeWidth={2} className="text-[#9C8B7A] dark:text-[#C8B9A6] flex-shrink-0" />
|
||||
<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>
|
||||
@@ -92,13 +100,13 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
|
||||
<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>
|
||||
<Coffee size={18} strokeWidth={2} className="text-[#9C8B7A] dark:text-[#C8B9A6] flex-shrink-0" />
|
||||
<div className="text-sm font-medium text-[#2C1810] dark:text-[#FAF6F1]">Brew Journal</div>
|
||||
</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>
|
||||
<Database size={18} strokeWidth={2} className="text-[#9C8B7A] dark:text-[#C8B9A6] flex-shrink-0" />
|
||||
<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>
|
||||
@@ -107,12 +115,27 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign out */}
|
||||
<button
|
||||
onClick={() => logout()}
|
||||
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>
|
||||
{/* Support section */}
|
||||
<div className="mb-6">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-widest text-[#9C8B7A] dark:text-[#C8B9A6] mb-2 px-1">Support</div>
|
||||
<div className="bg-white dark:bg-[#22120B] rounded-2xl border border-[#E8DFD3] dark:border-[#3B2217] overflow-hidden shadow-sm transition-colors duration-200">
|
||||
<a
|
||||
href="https://git.adityagupta.dev/sortedcord/brew/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between px-4 py-3.5 hover:bg-[#F3EDE4]/50 dark:hover:bg-[#2C1810]/50 transition-colors cursor-pointer no-underline group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<MessageSquare size={18} strokeWidth={2} className="text-[#9C8B7A] dark:text-[#C8B9A6] flex-shrink-0" />
|
||||
<div>
|
||||
<div className="text-xs text-[#9C8B7A] dark:text-[#C8B9A6]">Feedback</div>
|
||||
<div className="text-sm font-medium text-[#2C1810] dark:text-[#FAF6F1]">Report an issue or request a feature</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[#9C8B7A] dark:text-[#C8B9A6] text-sm group-hover:translate-x-0.5 transition-transform duration-200">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function Register({ onRegisterSuccess }) {
|
||||
return (
|
||||
<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>
|
||||
<img src="/icon-192.png" alt="Brew Journal" className="w-16 h-16 mx-auto mb-3 rounded-[22%]" />
|
||||
<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>
|
||||
|
||||
@@ -6,12 +6,28 @@ import { execSync } from 'child_process'
|
||||
// Derive version from git tags; fall back to commit hash
|
||||
function getGitVersion() {
|
||||
try {
|
||||
// If there's a tag pointing at HEAD, use it exactly (e.g. "v1.2.0")
|
||||
// Otherwise use "v0.0.0-<hash>[-dirty]"
|
||||
const raw = execSync('git describe --tags --always --dirty', { stdio: ['pipe', 'pipe', 'ignore'] })
|
||||
// Attempt to fetch tags in case of a shallow clone (e.g. CI/CD)
|
||||
try {
|
||||
execSync('git fetch --tags', { stdio: 'ignore' });
|
||||
} catch (e) {
|
||||
// Ignore if fetch fails
|
||||
}
|
||||
|
||||
// Only include --dirty locally (development), omit in production to prevent false "dirty" flags
|
||||
const isProd = process.env.NODE_ENV === 'production' || process.env.CI;
|
||||
const describeCmd = isProd ? 'git describe --tags --always' : 'git describe --tags --always --dirty';
|
||||
const raw = execSync(describeCmd, { stdio: ['pipe', 'pipe', 'ignore'] })
|
||||
.toString()
|
||||
.trim();
|
||||
// If it looks like a pure tag (no dashes after the tag part) return as-is
|
||||
|
||||
// If it's just a commit hash (no tags found)
|
||||
if (/^[0-9a-f]{7,}$/.test(raw)) {
|
||||
return `v0.0.0-${raw}`;
|
||||
}
|
||||
|
||||
// Otherwise, return raw (e.g., v0.1.1-5-g4a9f6b6 or v1.2.0)
|
||||
|
||||
// Exact tag (e.g., v1.2.0)
|
||||
return raw;
|
||||
} catch {
|
||||
return 'dev';
|
||||
|
||||