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;