diff --git a/src/components/LoadingScreen.tsx b/src/components/LoadingScreen.tsx
index 2aeed99..66c5011 100644
--- a/src/components/LoadingScreen.tsx
+++ b/src/components/LoadingScreen.tsx
@@ -1,4 +1,4 @@
-import { AnimatePresence, motion } from "framer-motion";
+import { AnimatePresence, m } from "framer-motion";
import { useEffect, useState } from "react";
interface LoadingScreenProps {
@@ -25,7 +25,7 @@ export function LoadingScreen({ isVisible }: LoadingScreenProps) {
return (
{isVisible && (
-
{/* The actual progress bar */}
- (
-
{text}
-
+
))}
@@ -98,7 +98,7 @@ export function LoadingScreen({ isVisible }: LoadingScreenProps) {
Scales v1.0 {/* System Kernel */}
-
+
)}
);
diff --git a/src/components/datasets/DatasetCard.tsx b/src/components/datasets/DatasetCard.tsx
index 736456c..4befc5c 100644
--- a/src/components/datasets/DatasetCard.tsx
+++ b/src/components/datasets/DatasetCard.tsx
@@ -1,21 +1,12 @@
import { Link } from "@tanstack/react-router";
-import { AnimatePresence, motion } from "framer-motion";
+import { AnimatePresence, m } from "framer-motion";
import { MoreVertical, Pencil, Trash2 } from "lucide-react";
-import { useEffect, useMemo, useRef, useState } from "react";
-import {
- Area,
- AreaChart,
- Bar,
- BarChart,
- Line,
- LineChart,
- ResponsiveContainer,
- Tooltip,
- XAxis,
-} from "recharts";
+import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react";
import type { Dataset, Measurement } from "../../types/dataset";
import { formatDate } from "../../utils/format";
+const PreviewChart = lazy(() => import("./PreviewChart"));
+
interface DatasetCardProps {
dataset: Dataset;
onEdit?: (dataset: Dataset) => void;
@@ -27,88 +18,6 @@ interface PreviewData extends Measurement {
displayDate: string;
}
-const CustomTooltip = ({
- active,
- payload,
-}: {
- active?: boolean;
- payload?: { value: number; payload: PreviewData }[];
-}) => {
- if (active && payload && payload.length > 0) {
- const data = payload[0].payload;
- return (
-
-
- {data.displayDate}
-
-
- {payload[0].value}
-
-
- );
- }
- return null;
-};
-
-const PreviewChart = ({ dataset, data }: { dataset: Dataset; data: PreviewData[] }) => {
- const viewType = dataset.views[0] ?? "line";
- const commonAxis = ;
- const tooltip = (
- }
- cursor={{ stroke: "rgba(139, 92, 246, 0.2)", strokeWidth: 2 }}
- isAnimationActive={false}
- />
- );
-
- switch (viewType) {
- case "area":
- return (
-
-
-
-
-
-
-
- {commonAxis}
- {tooltip}
-
-
- );
- case "bar":
- return (
-
- {commonAxis}
- {tooltip}
-
-
- );
- default:
- return (
-
- {commonAxis}
- {tooltip}
-
-
- );
- }
-};
-
export function DatasetCard({ dataset, onEdit, onDelete }: DatasetCardProps) {
const [isClient, setIsClient] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -141,13 +50,14 @@ export function DatasetCard({ dataset, onEdit, onDelete }: DatasetCardProps) {
}, [dataset.measurements, isClient]);
return (
-
@@ -175,7 +85,7 @@ export function DatasetCard({ dataset, onEdit, onDelete }: DatasetCardProps) {
{isMenuOpen && (
-
Delete
-
+
)}
-
- {isClient ? : }
-
+ {isClient ? (
+
}
+ >
+
+
+ ) : (
+
+ )}
{dataset.description || "Refined tracking parameters."}
-
+
);
}
diff --git a/src/components/datasets/DatasetGraph.tsx b/src/components/datasets/DatasetGraph.tsx
index 977a4c8..6960695 100644
--- a/src/components/datasets/DatasetGraph.tsx
+++ b/src/components/datasets/DatasetGraph.tsx
@@ -1,25 +1,9 @@
-import { useEffect, useMemo, useState } from "react";
-import {
- Area,
- AreaChart,
- Bar,
- BarChart,
- CartesianGrid,
- Cell,
- Line,
- LineChart,
- Pie,
- PieChart,
- ResponsiveContainer,
- Scatter,
- ScatterChart,
- Tooltip,
- XAxis,
- YAxis,
-} from "recharts";
+import { lazy, Suspense, useEffect, useMemo, useState } from "react";
import type { Measurement, ViewType } from "../../types/dataset";
import { formatDate } from "../../utils/format";
+const DatasetGraphContent = lazy(() => import("./DatasetGraphContent"));
+
interface DatasetGraphProps {
data: Measurement[];
viewType: ViewType;
@@ -31,170 +15,6 @@ interface ChartData extends Measurement {
displayDate: string;
}
-const COLORS = ["#8b5cf6", "#a78bfa", "#c4b5fd", "#ddd6fe", "#ede9fe"];
-
-interface TooltipPayload {
- payload: ChartData;
- value: number | string;
-}
-
-const CustomTooltip = ({
- active,
- payload,
- unit,
-}: {
- active?: boolean;
- payload?: TooltipPayload[];
- unit: string;
-}) => {
- if (active && payload && payload.length > 0) {
- const firstPayload = payload[0];
- if (firstPayload?.payload) {
- const data = firstPayload.payload;
- return (
-
-
- {data.displayDate}
-
-
- {firstPayload.value}{" "}
- {unit}
-
-
- );
- }
- }
- return null;
-};
-
-// Reusable Chart Elements
-const CommonXAxis = ({ chartData }: { chartData: ChartData[] }) => (
- chartData[index]?.displayDate ?? ""}
- stroke="rgba(255,255,255,0.1)"
- tick={{
- fill: "rgba(255,255,255,0.4)",
- fontSize: 10,
- fontFamily: "JetBrains Mono",
- }}
- tickLine={false}
- axisLine={false}
- />
-);
-
-const CommonYAxis = () => (
-
-);
-
-const CommonGrid = () => (
-
-);
-
-const CommonTooltip = ({ unit }: { unit: string }) => (
- }
- cursor={{ stroke: "rgba(139, 92, 246, 0.2)", strokeWidth: 2 }}
- />
-);
-
-// Modular Chart Renderers
-const LineRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
-
-
-
-
-
-
-
-);
-
-const BarRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
-
-
-
-
-
-
-
-);
-
-const AreaRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-const PieRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
-
-
- {chartData.map((entry) => (
- |
- ))}
-
-
-
-);
-
-const ScatterRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
-
-
-
-
-
-
-
-);
-
export function DatasetGraph({ data, viewType, unit }: DatasetGraphProps) {
const [isClient, setIsClient] = useState(false);
useEffect(() => {
@@ -212,30 +32,13 @@ export function DatasetGraph({ data, viewType, unit }: DatasetGraphProps) {
}));
}, [data, isClient]);
- const renderContent = () => {
- if (!isClient) return null;
-
- switch (viewType) {
- case "line":
- return ;
- case "bar":
- return ;
- case "area":
- return ;
- case "pie":
- return ;
- case "scatter":
- return ;
- default:
- return null;
- }
- };
-
return (
-
- {renderContent()}
-
+ {isClient && (
+ }>
+
+
+ )}
);
}
diff --git a/src/components/datasets/DatasetGraphContent.tsx b/src/components/datasets/DatasetGraphContent.tsx
new file mode 100644
index 0000000..74b6419
--- /dev/null
+++ b/src/components/datasets/DatasetGraphContent.tsx
@@ -0,0 +1,219 @@
+import {
+ Area,
+ AreaChart,
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Cell,
+ Line,
+ LineChart,
+ Pie,
+ PieChart,
+ ResponsiveContainer,
+ Scatter,
+ ScatterChart,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+import type { Measurement, ViewType } from "../../types/dataset";
+
+interface ChartData extends Measurement {
+ tooltipId: string;
+ displayDate: string;
+}
+
+const COLORS = ["#8b5cf6", "#a78bfa", "#c4b5fd", "#ddd6fe", "#ede9fe"];
+
+interface TooltipPayload {
+ payload: ChartData;
+ value: number | string;
+}
+
+const CustomTooltip = ({
+ active,
+ payload,
+ unit,
+}: {
+ active?: boolean;
+ payload?: TooltipPayload[];
+ unit: string;
+}) => {
+ if (active && payload && payload.length > 0) {
+ const firstPayload = payload[0];
+ if (firstPayload?.payload) {
+ const data = firstPayload.payload;
+ return (
+
+
+ {data.displayDate}
+
+
+ {firstPayload.value}{" "}
+ {unit}
+
+
+ );
+ }
+ }
+ return null;
+};
+
+const CommonXAxis = ({ chartData }: { chartData: ChartData[] }) => (
+ chartData[index]?.displayDate ?? ""}
+ stroke="rgba(255,255,255,0.1)"
+ tick={{
+ fill: "rgba(255,255,255,0.4)",
+ fontSize: 10,
+ fontFamily: "JetBrains Mono",
+ }}
+ tickLine={false}
+ axisLine={false}
+ />
+);
+
+const CommonYAxis = () => (
+
+);
+
+const CommonGrid = () => (
+
+);
+
+const CommonTooltip = ({ unit }: { unit: string }) => (
+ }
+ cursor={{ stroke: "rgba(139, 92, 246, 0.2)", strokeWidth: 2 }}
+ />
+);
+
+const LineRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
+
+
+
+
+
+
+
+);
+
+const BarRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
+
+
+
+
+
+
+
+);
+
+const AreaRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+const PieRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
+
+
+ {chartData.map((entry) => (
+ |
+ ))}
+
+
+
+);
+
+const ScatterRenderer = ({ chartData, unit }: { chartData: ChartData[]; unit: string }) => (
+
+
+
+
+
+
+
+);
+
+export default function DatasetGraphContent({
+ chartData,
+ viewType,
+ unit,
+}: {
+ chartData: ChartData[];
+ viewType: ViewType;
+ unit: string;
+}) {
+ const renderContent = () => {
+ switch (viewType) {
+ case "line":
+ return ;
+ case "bar":
+ return ;
+ case "area":
+ return ;
+ case "pie":
+ return ;
+ case "scatter":
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+ {renderContent()}
+
+ );
+}
diff --git a/src/components/datasets/DatasetGrid.tsx b/src/components/datasets/DatasetGrid.tsx
index 6755ed6..867765e 100644
--- a/src/components/datasets/DatasetGrid.tsx
+++ b/src/components/datasets/DatasetGrid.tsx
@@ -1,4 +1,4 @@
-import { AnimatePresence, motion } from "framer-motion";
+import { AnimatePresence, m } from "framer-motion";
import { Info } from "lucide-react";
import type { Dataset } from "../../types/dataset";
import { DatasetCard } from "./DatasetCard";
@@ -31,7 +31,7 @@ export function DatasetGrid({ datasets, onEdit, onDelete }: DatasetGridProps) {
{datasets.map((dataset) => (
-
-
+
))}
) : (
-
The void remains silent.
-
+
)}
diff --git a/src/components/datasets/DatasetView.tsx b/src/components/datasets/DatasetView.tsx
index f576a38..cfd0229 100644
--- a/src/components/datasets/DatasetView.tsx
+++ b/src/components/datasets/DatasetView.tsx
@@ -1,5 +1,5 @@
import { Link } from "@tanstack/react-router";
-import { AnimatePresence, motion } from "framer-motion";
+import { AnimatePresence, m } from "framer-motion";
import { ChevronRight, Info, Pencil, Trash2 } from "lucide-react";
import { useDatasetStore } from "@/store";
import type { Dataset } from "../../types/dataset";
@@ -33,7 +33,7 @@ export function DatasetList({ datasets, onEdit, onDelete }: DatasetListProps) {
{datasets.map((dataset) => (
-
-
+
))}
) : (
-
The void remains silent.
-
+
)}
diff --git a/src/components/datasets/PreviewChart.tsx b/src/components/datasets/PreviewChart.tsx
new file mode 100644
index 0000000..29e0d66
--- /dev/null
+++ b/src/components/datasets/PreviewChart.tsx
@@ -0,0 +1,110 @@
+import {
+ Area,
+ AreaChart,
+ Bar,
+ BarChart,
+ Line,
+ LineChart,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+} from "recharts";
+import type { Dataset, Measurement } from "../../types/dataset";
+
+interface PreviewData extends Measurement {
+ tooltipId: string;
+ displayDate: string;
+}
+
+const CustomTooltip = ({
+ active,
+ payload,
+}: {
+ active?: boolean;
+ payload?: { value: number; payload: PreviewData }[];
+}) => {
+ if (active && payload && payload.length > 0) {
+ const data = payload[0].payload;
+ return (
+
+
+ {data.displayDate}
+
+
+ {payload[0].value}
+
+
+ );
+ }
+ return null;
+};
+
+export default function PreviewChart({ dataset, data }: { dataset: Dataset; data: PreviewData[] }) {
+ const viewType = dataset.views[0] ?? "line";
+ const commonAxis = ;
+ const tooltip = (
+ }
+ cursor={{ stroke: "rgba(139, 92, 246, 0.2)", strokeWidth: 2 }}
+ isAnimationActive={false}
+ />
+ );
+
+ return (
+
+ {(() => {
+ switch (viewType) {
+ case "area":
+ return (
+
+
+
+
+
+
+
+ {commonAxis}
+ {tooltip}
+
+
+ );
+ case "bar":
+ return (
+
+ {commonAxis}
+ {tooltip}
+
+
+ );
+ default:
+ return (
+
+ {commonAxis}
+ {tooltip}
+
+
+ );
+ }
+ })()}
+
+ );
+}
diff --git a/src/components/layout/AddDatasetFAB.tsx b/src/components/layout/AddDatasetFAB.tsx
index d02e238..50af9c4 100644
--- a/src/components/layout/AddDatasetFAB.tsx
+++ b/src/components/layout/AddDatasetFAB.tsx
@@ -1,4 +1,4 @@
-import { motion } from "framer-motion";
+import { m } from "framer-motion";
import { Plus } from "lucide-react";
import { useAppStore } from "../../store";
@@ -6,7 +6,7 @@ export function AddDatasetFAB() {
const setAddDatasetModalOpen = useAppStore((state) => state.setAddDatasetModalOpen);
return (
- setAddDatasetModalOpen(true)}
@@ -14,6 +14,6 @@ export function AddDatasetFAB() {
aria-label="Add new dataset"
>
-
+
);
}
diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx
index b30d460..d91fe4c 100644
--- a/src/components/ui/Modal.tsx
+++ b/src/components/ui/Modal.tsx
@@ -1,4 +1,4 @@
-import { AnimatePresence, motion } from "framer-motion";
+import { AnimatePresence, m } from "framer-motion";
import { X } from "lucide-react";
import { useEffect } from "react";
@@ -29,7 +29,7 @@ export function Modal({ isOpen, onClose, title, children, zIndex = 100 }: ModalP
{isOpen && (
-
-
{children}
-
+
)}
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 2b6e5bb..bf1f73a 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -1,6 +1,7 @@
import { TanStackDevtools } from "@tanstack/react-devtools";
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
+import { domAnimation, LazyMotion } from "framer-motion";
import { useEffect, useState } from "react";
import { LoadingScreen } from "../components/LoadingScreen";
import { Modals } from "../components/ui/Modals";
@@ -72,8 +73,10 @@ function RootComponent() {
<>
-
-
+
+
+
+
>
);
diff --git a/src/utils/subscriptions.ts b/src/utils/subscriptions.ts
index 97ff31a..caa9a9c 100644
--- a/src/utils/subscriptions.ts
+++ b/src/utils/subscriptions.ts
@@ -1,253 +1,11 @@
-import { db } from "../lib/dexieDb";
-import { pb } from "../lib/pocketbase";
-import { useDatasetStore } from "../store";
-import type { DatasetRecord, MeasurementRecord, UnitRecord } from "../types/dataset";
+import { subscribeDatasets } from "./subscriptions/datasets";
+import { subscribeMeasurements } from "./subscriptions/measurements";
+import { subscribePreferences } from "./subscriptions/preferences";
+import { subscribeUnits } from "./subscriptions/units";
export const setupSubscriptions = () => {
- // 1. DATASETS SUBSCRIPTION
- pb.collection("datasets").subscribe("*", async (e) => {
- const { action, record } = e;
- const updated = new Date(record.updated).getTime();
- const created = new Date(record.created).getTime();
-
- if (action === "create" || action === "update") {
- const datasetRecord: DatasetRecord = {
- id: record.id,
- title: record.title,
- description: record.description,
- unitId: record.unit_id,
- views: record.views,
- created,
- updated,
- };
-
- // Refresh Store FIRST
- useDatasetStore.setState((state) => {
- const existing = state.datasets.find((d) => d.id === record.id);
-
- if (existing) {
- // CHECK: Is this update actually new?
- const localUpdated = existing.updated || 0;
- if (updated <= localUpdated) {
- // Even if timestamps match, double check if data is actually different
- const isDifferent =
- existing.title !== datasetRecord.title ||
- existing.description !== datasetRecord.description ||
- existing.unit.id !== datasetRecord.unitId ||
- JSON.stringify(existing.views) !== JSON.stringify(datasetRecord.views);
-
- if (!isDifferent) return state; // SKIP
- }
-
- // Apply update
- return {
- datasets: state.datasets.map((d) =>
- d.id === record.id
- ? {
- ...d,
- title: datasetRecord.title,
- description: datasetRecord.description,
- views: datasetRecord.views,
- unit: state.units.find((u) => u.id === datasetRecord.unitId) || d.unit,
- created: datasetRecord.created,
- updated: datasetRecord.updated,
- }
- : d,
- ),
- };
- } else {
- // Add new
- return {
- datasets: [
- {
- ...datasetRecord,
- unit: state.units.find((u) => u.id === datasetRecord.unitId) || {
- id: datasetRecord.unitId,
- name: "Unknown",
- symbol: "",
- created: Date.now(),
- updated: Date.now(),
- },
- measurements: [],
- },
- ...state.datasets,
- ],
- };
- }
- });
-
- // Update Dexie NEXT
- await db.datasets.put(datasetRecord);
- } else if (action === "delete") {
- useDatasetStore.setState((state) => {
- if (!state.datasets.some((d) => d.id === record.id)) return state;
- return { datasets: state.datasets.filter((d) => d.id !== record.id) };
- });
- await Promise.all([
- db.datasets.delete(record.id),
- db.measurements.where("datasetId").equals(record.id).delete(),
- ]);
- }
- });
-
- // 2. MEASUREMENTS SUBSCRIPTION
- pb.collection("measurements").subscribe("*", async (e) => {
- const { action, record } = e;
- const updated = new Date(record.updated).getTime();
- const created = new Date(record.created).getTime();
-
- if (action === "create" || action === "update") {
- const measurementRecord: MeasurementRecord = {
- id: record.id,
- datasetId: record.dataset_id,
- timestamp: record.timestamp,
- value: record.value,
- created,
- updated,
- };
-
- useDatasetStore.setState((state) => {
- let changed = false;
- const newDatasets = state.datasets.map((dataset) => {
- if (dataset.id === measurementRecord.datasetId) {
- const existing = dataset.measurements.find((m) => m.id === record.id);
-
- if (existing) {
- const localUpdated = existing.updated || 0;
- if (updated <= localUpdated) {
- const isDifferent =
- existing.value !== measurementRecord.value ||
- existing.timestamp !== measurementRecord.timestamp;
- if (!isDifferent) return dataset;
- }
-
- changed = true;
- return {
- ...dataset,
- measurements: dataset.measurements.map((m) =>
- m.id === record.id ? { ...m, ...measurementRecord } : m,
- ),
- };
- } else {
- changed = true;
- const newMeasurements = [...dataset.measurements, measurementRecord].sort(
- (a, b) => a.timestamp - b.timestamp,
- );
- return { ...dataset, measurements: newMeasurements };
- }
- }
- return dataset;
- });
-
- return changed ? { datasets: newDatasets } : state;
- });
-
- await db.measurements.put(measurementRecord);
- } else if (action === "delete") {
- useDatasetStore.setState((state) => {
- let changed = false;
- const newDatasets = state.datasets.map((dataset) => {
- if (dataset.measurements.some((m) => m.id === record.id)) {
- changed = true;
- return {
- ...dataset,
- measurements: dataset.measurements.filter((m) => m.id !== record.id),
- };
- }
- return dataset;
- });
- return changed ? { datasets: newDatasets } : state;
- });
- await db.measurements.delete(record.id);
- }
- });
-
- // 3. UNITS SUBSCRIPTION
- pb.collection("units").subscribe("*", async (e) => {
- const { action, record } = e;
- const updated = new Date(record.updated).getTime();
- const created = new Date(record.created).getTime();
-
- const unitRecord: UnitRecord = {
- id: record.id,
- name: record.name,
- symbol: record.symbol,
- created,
- updated,
- };
-
- if (action === "create" || action === "update") {
- useDatasetStore.setState((state) => {
- const existing = state.units.find((u) => u.id === record.id);
- if (existing) {
- const localUpdated = existing.updated || 0;
- if (updated <= localUpdated) {
- if (existing.name === unitRecord.name && existing.symbol === unitRecord.symbol)
- return state;
- }
- }
-
- const newUnits = existing
- ? state.units.map((u) => (u.id === record.id ? unitRecord : u))
- : [...state.units, unitRecord];
-
- const newDatasets = state.datasets.map((d) =>
- d.unit.id === unitRecord.id ? { ...d, unit: unitRecord } : d,
- );
-
- return { units: newUnits, datasets: newDatasets };
- });
- await db.units.put(unitRecord);
- } else if (action === "delete") {
- useDatasetStore.setState((state) => {
- if (!state.units.some((u) => u.id === record.id)) return state;
- return { units: state.units.filter((u) => u.id !== record.id) };
- });
- await db.units.delete(record.id);
- }
- });
-
- // 4. PREFERENCES SUBSCRIPTION
- pb.collection("preferences").subscribe("*", async (e) => {
- const { action, record } = e;
- const updated = new Date(record.updated).getTime();
- const created = new Date(record.created).getTime();
-
- const preferenceRecord = {
- id: record.id,
- preference: record.preference,
- value: record.value,
- created,
- updated,
- };
-
- if (action === "create" || action === "update") {
- useDatasetStore.setState((state) => {
- const existing = state.preferences.find((p) => p.id === record.id);
- if (existing) {
- const localUpdated = existing.updated || 0;
- if (updated <= localUpdated) {
- if (
- existing.preference === preferenceRecord.preference &&
- JSON.stringify(existing.value) === JSON.stringify(preferenceRecord.value)
- )
- return state;
- }
- }
-
- const newPreferences = existing
- ? state.preferences.map((p) => (p.id === record.id ? preferenceRecord : p))
- : [...state.preferences, preferenceRecord];
-
- return { preferences: newPreferences };
- });
- await db.preferences.put(preferenceRecord);
- } else if (action === "delete") {
- useDatasetStore.setState((state) => {
- if (!state.preferences.some((p) => p.id === record.id)) return state;
- return { preferences: state.preferences.filter((p) => p.id !== record.id) };
- });
- await db.preferences.delete(record.id);
- }
- });
+ subscribeDatasets();
+ subscribeMeasurements();
+ subscribeUnits();
+ subscribePreferences();
};
diff --git a/src/utils/subscriptions/datasets.ts b/src/utils/subscriptions/datasets.ts
new file mode 100644
index 0000000..b2c8ff4
--- /dev/null
+++ b/src/utils/subscriptions/datasets.ts
@@ -0,0 +1,85 @@
+import { db } from "../../lib/dexieDb";
+import { pb } from "../../lib/pocketbase";
+import { useDatasetStore } from "../../store";
+import type { DatasetRecord } from "../../types/dataset";
+
+export const subscribeDatasets = () => {
+ return pb.collection("datasets").subscribe("*", async (e) => {
+ const { action, record } = e;
+ const updated = new Date(record.updated).getTime();
+ const created = new Date(record.created).getTime();
+
+ if (action === "create" || action === "update") {
+ const datasetRecord: DatasetRecord = {
+ id: record.id,
+ title: record.title,
+ description: record.description,
+ unitId: record.unit_id,
+ views: record.views,
+ created,
+ updated,
+ };
+
+ useDatasetStore.setState((state) => {
+ const existing = state.datasets.find((d) => d.id === record.id);
+
+ if (existing) {
+ const localUpdated = existing.updated || 0;
+ if (updated <= localUpdated) {
+ const isDifferent =
+ existing.title !== datasetRecord.title ||
+ existing.description !== datasetRecord.description ||
+ existing.unit.id !== datasetRecord.unitId ||
+ JSON.stringify(existing.views) !== JSON.stringify(datasetRecord.views);
+
+ if (!isDifferent) return state;
+ }
+
+ return {
+ datasets: state.datasets.map((d) =>
+ d.id === record.id
+ ? {
+ ...d,
+ title: datasetRecord.title,
+ description: datasetRecord.description,
+ views: datasetRecord.views,
+ unit: state.units.find((u) => u.id === datasetRecord.unitId) || d.unit,
+ created: datasetRecord.created,
+ updated: datasetRecord.updated,
+ }
+ : d,
+ ),
+ };
+ } else {
+ return {
+ datasets: [
+ {
+ ...datasetRecord,
+ unit: state.units.find((u) => u.id === datasetRecord.unitId) || {
+ id: datasetRecord.unitId,
+ name: "Unknown",
+ symbol: "",
+ created: Date.now(),
+ updated: Date.now(),
+ },
+ measurements: [],
+ },
+ ...state.datasets,
+ ],
+ };
+ }
+ });
+
+ await db.datasets.put(datasetRecord);
+ } else if (action === "delete") {
+ useDatasetStore.setState((state) => {
+ if (!state.datasets.some((d) => d.id === record.id)) return state;
+ return { datasets: state.datasets.filter((d) => d.id !== record.id) };
+ });
+ await Promise.all([
+ db.datasets.delete(record.id),
+ db.measurements.where("datasetId").equals(record.id).delete(),
+ ]);
+ }
+ });
+};
diff --git a/src/utils/subscriptions/measurements.ts b/src/utils/subscriptions/measurements.ts
new file mode 100644
index 0000000..5b3f087
--- /dev/null
+++ b/src/utils/subscriptions/measurements.ts
@@ -0,0 +1,77 @@
+import { db } from "../../lib/dexieDb";
+import { pb } from "../../lib/pocketbase";
+import { useDatasetStore } from "../../store";
+import type { MeasurementRecord } from "../../types/dataset";
+
+export const subscribeMeasurements = () => {
+ return pb.collection("measurements").subscribe("*", async (e) => {
+ const { action, record } = e;
+ const updated = new Date(record.updated).getTime();
+ const created = new Date(record.created).getTime();
+
+ if (action === "create" || action === "update") {
+ const measurementRecord: MeasurementRecord = {
+ id: record.id,
+ datasetId: record.dataset_id,
+ timestamp: record.timestamp,
+ value: record.value,
+ created,
+ updated,
+ };
+
+ useDatasetStore.setState((state) => {
+ let changed = false;
+ const newDatasets = state.datasets.map((dataset) => {
+ if (dataset.id === measurementRecord.datasetId) {
+ const existing = dataset.measurements.find((m) => m.id === record.id);
+
+ if (existing) {
+ const localUpdated = existing.updated || 0;
+ if (updated <= localUpdated) {
+ const isDifferent =
+ existing.value !== measurementRecord.value ||
+ existing.timestamp !== measurementRecord.timestamp;
+ if (!isDifferent) return dataset;
+ }
+
+ changed = true;
+ return {
+ ...dataset,
+ measurements: dataset.measurements.map((m) =>
+ m.id === record.id ? { ...m, ...measurementRecord } : m,
+ ),
+ };
+ } else {
+ changed = true;
+ const newMeasurements = [...dataset.measurements, measurementRecord].sort(
+ (a, b) => a.timestamp - b.timestamp,
+ );
+ return { ...dataset, measurements: newMeasurements };
+ }
+ }
+ return dataset;
+ });
+
+ return changed ? { datasets: newDatasets } : state;
+ });
+
+ await db.measurements.put(measurementRecord);
+ } else if (action === "delete") {
+ useDatasetStore.setState((state) => {
+ let changed = false;
+ const newDatasets = state.datasets.map((dataset) => {
+ if (dataset.measurements.some((m) => m.id === record.id)) {
+ changed = true;
+ return {
+ ...dataset,
+ measurements: dataset.measurements.filter((m) => m.id !== record.id),
+ };
+ }
+ return dataset;
+ });
+ return changed ? { datasets: newDatasets } : state;
+ });
+ await db.measurements.delete(record.id);
+ }
+ });
+};
diff --git a/src/utils/subscriptions/preferences.ts b/src/utils/subscriptions/preferences.ts
new file mode 100644
index 0000000..01117ef
--- /dev/null
+++ b/src/utils/subscriptions/preferences.ts
@@ -0,0 +1,48 @@
+import { db } from "../../lib/dexieDb";
+import { pb } from "../../lib/pocketbase";
+import { useDatasetStore } from "../../store";
+
+export const subscribePreferences = () => {
+ return pb.collection("preferences").subscribe("*", async (e) => {
+ const { action, record } = e;
+ const updated = new Date(record.updated).getTime();
+ const created = new Date(record.created).getTime();
+
+ const preferenceRecord = {
+ id: record.id,
+ preference: record.preference,
+ value: record.value,
+ created,
+ updated,
+ };
+
+ if (action === "create" || action === "update") {
+ useDatasetStore.setState((state) => {
+ const existing = state.preferences.find((p) => p.id === record.id);
+ if (existing) {
+ const localUpdated = existing.updated || 0;
+ if (updated <= localUpdated) {
+ if (
+ existing.preference === preferenceRecord.preference &&
+ JSON.stringify(existing.value) === JSON.stringify(preferenceRecord.value)
+ )
+ return state;
+ }
+ }
+
+ const newPreferences = existing
+ ? state.preferences.map((p) => (p.id === record.id ? preferenceRecord : p))
+ : [...state.preferences, preferenceRecord];
+
+ return { preferences: newPreferences };
+ });
+ await db.preferences.put(preferenceRecord);
+ } else if (action === "delete") {
+ useDatasetStore.setState((state) => {
+ if (!state.preferences.some((p) => p.id === record.id)) return state;
+ return { preferences: state.preferences.filter((p) => p.id !== record.id) };
+ });
+ await db.preferences.delete(record.id);
+ }
+ });
+};
diff --git a/src/utils/subscriptions/units.ts b/src/utils/subscriptions/units.ts
new file mode 100644
index 0000000..192f29d
--- /dev/null
+++ b/src/utils/subscriptions/units.ts
@@ -0,0 +1,50 @@
+import { db } from "../../lib/dexieDb";
+import { pb } from "../../lib/pocketbase";
+import { useDatasetStore } from "../../store";
+import type { UnitRecord } from "../../types/dataset";
+
+export const subscribeUnits = () => {
+ return pb.collection("units").subscribe("*", async (e) => {
+ const { action, record } = e;
+ const updated = new Date(record.updated).getTime();
+ const created = new Date(record.created).getTime();
+
+ const unitRecord: UnitRecord = {
+ id: record.id,
+ name: record.name,
+ symbol: record.symbol,
+ created,
+ updated,
+ };
+
+ if (action === "create" || action === "update") {
+ useDatasetStore.setState((state) => {
+ const existing = state.units.find((u) => u.id === record.id);
+ if (existing) {
+ const localUpdated = existing.updated || 0;
+ if (updated <= localUpdated) {
+ if (existing.name === unitRecord.name && existing.symbol === unitRecord.symbol)
+ return state;
+ }
+ }
+
+ const newUnits = existing
+ ? state.units.map((u) => (u.id === record.id ? unitRecord : u))
+ : [...state.units, unitRecord];
+
+ const newDatasets = state.datasets.map((d) =>
+ d.unit.id === unitRecord.id ? { ...d, unit: unitRecord } : d,
+ );
+
+ return { units: newUnits, datasets: newDatasets };
+ });
+ await db.units.put(unitRecord);
+ } else if (action === "delete") {
+ useDatasetStore.setState((state) => {
+ if (!state.units.some((u) => u.id === record.id)) return state;
+ return { units: state.units.filter((u) => u.id !== record.id) };
+ });
+ await db.units.delete(record.id);
+ }
+ });
+};
diff --git a/vite.config.ts b/vite.config.ts
index 6508580..e58c8cb 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -6,7 +6,7 @@ import viteReact from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
-const config = defineConfig({
+export default defineConfig({
plugins: [
devtools(),
tsconfigPaths({ projects: ["./tsconfig.json"] }),
@@ -23,5 +23,3 @@ const config = defineConfig({
}),
],
});
-
-export default config;