diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..2874e71
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+**/.git
+**/.gitignore
+**/.vs
+**/.vscode
+**/bin
+**/obj
+**/.idea
+**/node_modules
+**/*.user
+**/*.md
+**/Dockerfile*
+**/.dockerignore
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..9422962
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,25 @@
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+WORKDIR /src
+
+# Copy csproj and restore dependencies
+COPY ["SessionZero.sln", "./"]
+COPY ["SessionZero/SessionZero.csproj", "SessionZero/"]
+RUN dotnet restore
+
+# Copy the rest of the files and build
+COPY . .
+RUN dotnet build "SessionZero/SessionZero.csproj" -c Release -o /app/build
+
+# Publish the application
+RUN dotnet publish "SessionZero/SessionZero.csproj" -c Release -o /app/publish
+
+# Use the official ASP.NET Core runtime image
+FROM nginx:alpine AS final
+WORKDIR /usr/share/nginx/html
+COPY --from=build /app/publish/wwwroot .
+COPY nginx.conf /etc/nginx/nginx.conf
+
+EXPOSE 80
+EXPOSE 443
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/README.docker.md b/README.docker.md
new file mode 100644
index 0000000..78266c3
--- /dev/null
+++ b/README.docker.md
@@ -0,0 +1,68 @@
+# SessionZero Docker Deployment Guide
+
+This guide explains how to build and deploy the SessionZero Blazor WebAssembly PWA using Docker.
+
+## Prerequisites
+
+- Docker installed on your machine
+- Docker Compose installed on your machine (optional, but recommended)
+
+## Building and Running with Docker Compose (Recommended)
+
+1. Navigate to the project root directory (where the `docker-compose.yml` file is located)
+2. Run the following command to build and start the container:
+
+ ```bash
+ docker-compose up -d --build
+ ```
+
+3. Access the application at http://localhost:8080
+
+## Building and Running with Docker CLI
+
+1. Navigate to the project root directory (where the `Dockerfile` is located)
+2. Build the Docker image:
+
+ ```bash
+ docker build -t sessionzero .
+ ```
+
+3. Run the container:
+
+ ```bash
+ docker run -d -p 8080:80 --name sessionzero-app sessionzero
+ ```
+
+4. Access the application at http://localhost:8080
+
+## Stopping the Container
+
+### With Docker Compose
+
+```bash
+docker-compose down
+```
+
+### With Docker CLI
+
+```bash
+docker stop sessionzero-app
+docker rm sessionzero-app
+```
+
+## Troubleshooting
+
+- If you encounter any issues with the container not starting, check the logs:
+
+ ```bash
+ docker logs sessionzero-app
+ ```
+
+- Make sure ports 8080 is not being used by another application on your host machine.
+
+- If the application doesn't work as expected, verify that all files were copied correctly by inspecting the container:
+
+ ```bash
+ docker exec -it sessionzero-app sh
+ ls -la /usr/share/nginx/html
+ ```
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..cf21678
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,15 @@
+version: '3.8'
+
+services:
+ sessionzero:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ - "8080:80" # Map container port 80 to host port 8080
+ restart: unless-stopped
+ volumes:
+ - nginx-cache:/var/cache/nginx
+
+volumes:
+ nginx-cache:
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..4eaf78f
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,40 @@
+worker_processes 1;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ sendfile on;
+ keepalive_timeout 65;
+
+ server {
+ listen 80;
+ server_name localhost;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html =404;
+ }
+
+ # Enable compression
+ gzip on;
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+
+ # Configure caching for static assets
+ location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
+ expires 1y;
+ add_header Cache-Control "public, max-age=31536000";
+ }
+
+ # Special handling for service worker
+ location = /service-worker.js {
+ expires -1;
+ add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
+ }
+ }
+}
diff --git a/old-react-datasets.txt b/old-react-datasets.txt
new file mode 100644
index 0000000..e4c001f
--- /dev/null
+++ b/old-react-datasets.txt
@@ -0,0 +1,320 @@
+import React, { useState, useEffect, useRef } from 'react';
+import './DatasetManager.css';
+import { Dataset, LibraryEntry, DataRecord, DataValue } from '../../types';
+import { HugeiconsIcon } from "@hugeicons/react";
+import { AddCircleIcon, Delete02Icon, FolderAddIcon, Edit02Icon } from "@hugeicons/core-free-icons";
+
+// Reusable Modal Component
+const Modal: React.FC<{
+ isOpen: boolean;
+ onClose: () => void;
+ title: string;
+ children: React.ReactNode;
+ actions: { label: string; onClick: () => void; className?: string }[];
+ size?: 'default' | 'large';
+}> = ({ isOpen, onClose, title, children, actions, size = 'default' }) => {
+ if (!isOpen) return null;
+
+ const modalContentClass = `modal-content ${size === 'large' ? 'entry-editor-content' : ''}`;
+
+ return (
+
+
e.stopPropagation()}>
+
{title}
+
{children}
+
+ {actions.map((action, index) => (
+
+ ))}
+
+
+
+ );
+};
+
+// Data Record Editor Component
+const DataRecordEditor = ({ data, onChange }: { data: DataRecord, onChange: (newData: DataRecord) => void }) => {
+ const [addingInfo, setAddingInfo] = useState<{type: 'field' | 'group'} | null>(null);
+ const [newKey, setNewKey] = useState('');
+ const newKeyInputRef = useRef(null);
+
+ useEffect(() => {
+ if (addingInfo) {
+ newKeyInputRef.current?.focus();
+ }
+ }, [addingInfo]);
+
+ const handleFieldChange = (key: string, value: DataValue) => {
+ onChange({ ...data, [key]: value });
+ };
+
+ const handleRemoveField = (key: string) => {
+ const { [key]: _, ...rest } = data;
+ onChange(rest);
+ };
+
+ const handleInitiateAdd = (type: 'field' | 'group') => {
+ setAddingInfo({ type });
+ };
+
+ const handleConfirmAdd = () => {
+ const key = newKey.trim();
+ if (key && !data.hasOwnProperty(key)) {
+ onChange({ ...data, [key]: addingInfo?.type === 'group' ? {} : '' });
+ setNewKey('');
+ setAddingInfo(null);
+ } else if (key) {
+ alert(`The key "${key}" already exists.`);
+ } else {
+ setAddingInfo(null);
+ }
+ };
+
+ return (
+
+ {Object.keys(data).length === 0 &&
This entry is empty. Add a field or group to begin.
}
+ {Object.entries(data).map(([key, value]) => (
+
+
+
+ {typeof value === 'object' && value !== null ?
+ (
+
+ handleFieldChange(key, newValue)}
+ />
+
+ ) :
+ (
+
+
+ ))}
+ {addingInfo ? (
+
+ setNewKey(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && handleConfirmAdd()}
+ placeholder={`New ${addingInfo.type} name...`}
+ />
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+ );
+};
+
+// Main Dataset Manager Component
+function DatasetManager() {
+ const [datasets, setDatasets] = useState(() => {
+ try {
+ const stored = localStorage.getItem('libraryDatasets');
+ return stored ? JSON.parse(stored) : [];
+ } catch (error) {
+ console.error("Failed to parse datasets from localStorage", error);
+ return [];
+ }
+ });
+
+ const [selectedDatasetId, setSelectedDatasetId] = useState(null);
+ const [editingEntry, setEditingEntry] = useState(null);
+ const [showCreateDatasetModal, setShowCreateDatasetModal] = useState(false);
+ const [newDatasetInfo, setNewDatasetInfo] = useState({ name: '', type: 'generic' });
+ const [confirmDeleteInfo, setConfirmDeleteInfo] = useState<{type: 'dataset'|'entry', id: number, name: string} | null>(null);
+
+ // Derive selectedDataset from state - this is our single source of truth
+ const selectedDataset = datasets.find(d => d.id === selectedDatasetId) || null;
+
+ // Effect to save to localStorage whenever datasets change
+ useEffect(() => {
+ try {
+ localStorage.setItem('libraryDatasets', JSON.stringify(datasets));
+ } catch (error) {
+ console.error("Failed to save datasets to localStorage", error);
+ }
+ }, [datasets]);
+
+ const handleSaveNewDataset = () => {
+ if (!newDatasetInfo.name.trim()) {
+ alert("Dataset name is required.");
+ return;
+ }
+ const newDataset: Dataset = { id: Date.now(), name: newDatasetInfo.name, type: newDatasetInfo.type || 'generic', entries: [] };
+ setDatasets([...datasets, newDataset]);
+ setShowCreateDatasetModal(false);
+ setNewDatasetInfo({ name: '', type: 'generic' });
+ };
+
+ const handleExecuteDelete = () => {
+ if (!confirmDeleteInfo) return;
+ const { type, id } = confirmDeleteInfo;
+
+ if (type === 'dataset') {
+ setDatasets(datasets.filter(d => d.id !== id));
+ if (selectedDatasetId === id) {
+ setSelectedDatasetId(null);
+ setEditingEntry(null);
+ }
+ } else if (type === 'entry' && selectedDatasetId) {
+ const updatedDatasets = datasets.map(d => {
+ if (d.id === selectedDatasetId) {
+ return { ...d, entries: d.entries.filter(e => e.id !== id) };
+ }
+ return d;
+ });
+ setDatasets(updatedDatasets);
+ }
+ setConfirmDeleteInfo(null);
+ };
+
+ const handleAddOrUpdateEntry = (entry: LibraryEntry) => {
+ if (!selectedDatasetId || !entry.name.trim()) return;
+
+ const updatedDatasets = datasets.map(d => {
+ if (d.id === selectedDatasetId) {
+ const isNew = entry.id === 0;
+ const updatedEntries = isNew
+ ? [...d.entries, { ...entry, id: Date.now() }]
+ : d.entries.map(e => e.id === entry.id ? entry : e);
+ return { ...d, entries: updatedEntries };
+ }
+ return d;
+ });
+ setDatasets(updatedDatasets);
+ setEditingEntry(null);
+ };
+
+ return (
+
+ {/* Entry Editor Modal */}
+ {editingEntry && (
+
setEditingEntry(null)}
+ title={editingEntry.id === 0 ? 'Create New Entry' : 'Edit Entry'}
+ size="large"
+ actions={[
+ { label: 'Cancel', onClick: () => setEditingEntry(null), className: 'load-btn' },
+ { label: 'Save Entry', onClick: () => handleAddOrUpdateEntry(editingEntry), className: 'save-btn' }
+ ]}
+ >
+
+
+ setEditingEntry({ ...editingEntry, name: e.target.value })}
+ />
+
+
+
+ setEditingEntry({ ...editingEntry, data: newData })}
+ />
+
+
+ )}
+
+ {/* Create Dataset Modal */}
+
setShowCreateDatasetModal(false)}
+ title="Create New Dataset"
+ actions={[
+ { label: 'Cancel', onClick: () => setShowCreateDatasetModal(false), className: 'load-btn' },
+ { label: 'Create', onClick: handleSaveNewDataset, className: 'save-btn' }
+ ]}
+ >
+
+
+ setNewDatasetInfo({...newDatasetInfo, name: e.target.value})}/>
+
+
+
+ setNewDatasetInfo({...newDatasetInfo, type: e.target.value})}/>
+
+
+
+ {/* Confirm Deletion Modal */}
+
setConfirmDeleteInfo(null)}
+ title="Confirm Deletion"
+ actions={[
+ { label: 'Cancel', onClick: () => setConfirmDeleteInfo(null), className: 'load-btn' },
+ { label: 'Delete', onClick: handleExecuteDelete, className: 'delete-btn' }
+ ]}
+ >
+ Are you sure you want to delete the {confirmDeleteInfo?.type} "{confirmDeleteInfo?.name}"? This action cannot be undone.
+
+
+
+
+
+
Datasets
+
+
+
+ {datasets.map(d => (
+ - setSelectedDatasetId(d.id)}>
+
+ {d.name}
+ {d.type}
+
+
+
+ ))}
+
+
+
+
+ {selectedDataset ? (
+ <>
+
+
{selectedDataset.name}
+
+
+
+ >
+ ) :
Select a dataset to view its entries.
}
+
+
+ );
+}
+
+export default DatasetManager;
\ No newline at end of file