Add offline queue with localStorage
This commit is contained in:
parent
297479fc1d
commit
34e8ffe5f2
1 changed files with 100 additions and 11 deletions
107
src/app/page.tsx
107
src/app/page.tsx
|
|
@ -2,6 +2,67 @@
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
const OFFLINE_QUEUE_KEY = "tia_offline_queue";
|
||||||
|
|
||||||
|
export interface OfflineEntry {
|
||||||
|
id: string;
|
||||||
|
type: "feed" | "diaper" | "sleep";
|
||||||
|
data: {
|
||||||
|
type: "feed" | "diaper" | "sleep";
|
||||||
|
childId: string;
|
||||||
|
subType: string;
|
||||||
|
amountMl?: number;
|
||||||
|
notes?: string;
|
||||||
|
startedAt?: string;
|
||||||
|
endedAt?: string;
|
||||||
|
};
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get queue from localStorage
|
||||||
|
export function getOfflineQueue(): OfflineEntry[] {
|
||||||
|
if (typeof window === "undefined") return [];
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(OFFLINE_QUEUE_KEY);
|
||||||
|
return data ? JSON.parse(data) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to queue
|
||||||
|
export function addToOfflineQueue(entry: Omit<OfflineEntry, "id" | "timestamp">) {
|
||||||
|
const queue = getOfflineQueue();
|
||||||
|
queue.push({
|
||||||
|
...entry,
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process queue when online
|
||||||
|
export async function processOfflineQueue() {
|
||||||
|
const queue = getOfflineQueue();
|
||||||
|
if (queue.length === 0) return;
|
||||||
|
|
||||||
|
const failed: OfflineEntry[] = [];
|
||||||
|
|
||||||
|
for (const entry of queue) {
|
||||||
|
try {
|
||||||
|
await fetch("/api/logs", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(entry.data),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
failed.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed));
|
||||||
|
}
|
||||||
|
|
||||||
interface LogModalProps {
|
interface LogModalProps {
|
||||||
type: "feed" | "diaper" | "sleep" | null;
|
type: "feed" | "diaper" | "sleep" | null;
|
||||||
childId: string;
|
childId: string;
|
||||||
|
|
@ -18,21 +79,31 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
const data = {
|
||||||
await fetch("/api/logs", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
type,
|
type,
|
||||||
childId,
|
childId,
|
||||||
subType,
|
subType,
|
||||||
amountMl: amountMl ? Number(amountMl) : undefined,
|
amountMl: amountMl ? Number(amountMl) : undefined,
|
||||||
notes: notes || undefined,
|
notes: notes || undefined,
|
||||||
}),
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try online first
|
||||||
|
const res = await fetch("/api/logs", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!res.ok && !navigator.onLine) {
|
||||||
|
// Offline - add to queue
|
||||||
|
addToOfflineQueue({ type: type as any, data });
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
// Network error - add to queue
|
||||||
|
addToOfflineQueue({ type: type as any, data });
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
@ -118,7 +189,19 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
|
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
|
||||||
const [childId, setChildId] = useState("5ad3b16a-1e0d-45ab-bc91-038397d75d0a");
|
const [childId] = useState("5ad3b16a-1e0d-45ab-bc91-038397d75d0a");
|
||||||
|
const [pendingCount, setPendingCount] = useState(0);
|
||||||
|
|
||||||
|
// Check queue on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const queue = getOfflineQueue();
|
||||||
|
setPendingCount(queue.length);
|
||||||
|
|
||||||
|
// Process queue when coming online
|
||||||
|
const handleOnline = () => processOfflineQueue();
|
||||||
|
window.addEventListener("online", handleOnline);
|
||||||
|
return () => window.removeEventListener("online", handleOnline);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50">
|
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50">
|
||||||
|
|
@ -128,6 +211,12 @@ export default function HomePage() {
|
||||||
<p className="text-gray-600 mt-2">Your baby tracking companion</p>
|
<p className="text-gray-600 mt-2">Your baby tracking companion</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{pendingCount > 0 && (
|
||||||
|
<div className="bg-amber-100 text-amber-800 px-4 py-2 rounded-xl text-center mb-4">
|
||||||
|
{pendingCount} pending log{pendingCount > 1 ? "s" : ""} (offline)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 max-w-md mx-auto">
|
<div className="grid grid-cols-2 gap-4 max-w-md mx-auto">
|
||||||
<button
|
<button
|
||||||
onClick={() => setModalType("feed")}
|
onClick={() => setModalType("feed")}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue