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)} + /> +
+ ) : + (