feat: Improved sync editor
All checks were successful
Deploy Brew Application / deploy (push) Successful in 10s
All checks were successful
Deploy Brew Application / deploy (push) Successful in 10s
This commit is contained in:
@@ -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 */}
|
||||
|
||||
69
src/components/SyncIndicator.jsx
Normal file
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>
|
||||
);
|
||||
}
|
||||
@@ -138,4 +138,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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,10 @@ export default function ProfilePage({ user, isOnline, syncing, showSyncedStatus
|
||||
</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 */}
|
||||
|
||||
@@ -25,12 +25,7 @@ function getGitVersion() {
|
||||
return `v0.0.0-${raw}`;
|
||||
}
|
||||
|
||||
// If it's a tag + commits + hash (e.g., v0.1.1-5-g4a9f6b6)
|
||||
// Format it cleanly to v0.1.1-4a9f6b6
|
||||
const match = raw.match(/^(.*)-\d+-g([0-9a-f]+)$/);
|
||||
if (match) {
|
||||
return `${match[1]}-${match[2]}`;
|
||||
}
|
||||
// Otherwise, return raw (e.g., v0.1.1-5-g4a9f6b6 or v1.2.0)
|
||||
|
||||
// Exact tag (e.g., v1.2.0)
|
||||
return raw;
|
||||
|
||||
Reference in New Issue
Block a user