test docker stuff
This commit is contained in:
parent
347aadbfc9
commit
52e1d6ff6b
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@ -0,0 +1,12 @@
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/bin
|
||||
**/obj
|
||||
**/.idea
|
||||
**/node_modules
|
||||
**/*.user
|
||||
**/*.md
|
||||
**/Dockerfile*
|
||||
**/.dockerignore
|
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -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;"]
|
68
README.docker.md
Normal file
68
README.docker.md
Normal file
@ -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
|
||||
```
|
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
@ -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:
|
40
nginx.conf
Normal file
40
nginx.conf
Normal file
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
320
old-react-datasets.txt
Normal file
320
old-react-datasets.txt
Normal file
@ -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 (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className={modalContentClass} onClick={e => e.stopPropagation()}>
|
||||
<h3>{title}</h3>
|
||||
<div className="modal-body">{children}</div>
|
||||
<div className="modal-actions">
|
||||
{actions.map((action, index) => (
|
||||
<button key={index} onClick={action.onClick} className={`builder-btn ${action.className || ''}`}>
|
||||
{action.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 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<HTMLInputElement>(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 (
|
||||
<div className="data-record-editor">
|
||||
{Object.keys(data).length === 0 && <p className="placeholder-text">This entry is empty. Add a field or group to begin.</p>}
|
||||
{Object.entries(data).map(([key, value]) => (
|
||||
<div key={key} className="data-field-row">
|
||||
<label>{key}</label>
|
||||
<div className="data-field-input-group">
|
||||
{typeof value === 'object' && value !== null ?
|
||||
(
|
||||
<div className="data-group-container">
|
||||
<DataRecordEditor
|
||||
data={value as DataRecord}
|
||||
onChange={(newValue) => handleFieldChange(key, newValue)}
|
||||
/>
|
||||
</div>
|
||||
) :
|
||||
( <textarea value={value as string || ''} onChange={e => handleFieldChange(key, e.target.value)} rows={3} /> )
|
||||
}
|
||||
</div>
|
||||
<button onClick={() => handleRemoveField(key)} className="builder-btn remove-btn">
|
||||
<HugeiconsIcon icon={Delete02Icon} size={16}/>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{addingInfo ? (
|
||||
<div className="add-field-form">
|
||||
<input
|
||||
ref={newKeyInputRef}
|
||||
type="text"
|
||||
value={newKey}
|
||||
onChange={e => setNewKey(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleConfirmAdd()}
|
||||
placeholder={`New ${addingInfo.type} name...`}
|
||||
/>
|
||||
<button className="builder-btn save-btn" onClick={handleConfirmAdd}>Save</button>
|
||||
<button className="builder-btn load-btn" onClick={() => setAddingInfo(null)}>Cancel</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="data-editor-actions">
|
||||
<button className="builder-btn add-btn" onClick={() => handleInitiateAdd('field')}><HugeiconsIcon icon={AddCircleIcon} size={14}/> Add Field</button>
|
||||
<button className="builder-btn add-btn" onClick={() => handleInitiateAdd('group')}><HugeiconsIcon icon={FolderAddIcon} size={14}/> Add Group</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main Dataset Manager Component
|
||||
function DatasetManager() {
|
||||
const [datasets, setDatasets] = useState<Dataset[]>(() => {
|
||||
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<number | null>(null);
|
||||
const [editingEntry, setEditingEntry] = useState<LibraryEntry | null>(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 (
|
||||
<div className="dataset-manager-container">
|
||||
{/* Entry Editor Modal */}
|
||||
{editingEntry && (
|
||||
<Modal
|
||||
isOpen={!!editingEntry}
|
||||
onClose={() => 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' }
|
||||
]}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>Entry Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g., 'Longsword' or 'Fireball'"
|
||||
value={editingEntry.name}
|
||||
onChange={(e) => setEditingEntry({ ...editingEntry, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Data</label>
|
||||
<DataRecordEditor
|
||||
data={editingEntry.data}
|
||||
onChange={(newData) => setEditingEntry({ ...editingEntry, data: newData })}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Create Dataset Modal */}
|
||||
<Modal
|
||||
isOpen={showCreateDatasetModal}
|
||||
onClose={() => setShowCreateDatasetModal(false)}
|
||||
title="Create New Dataset"
|
||||
actions={[
|
||||
{ label: 'Cancel', onClick: () => setShowCreateDatasetModal(false), className: 'load-btn' },
|
||||
{ label: 'Create', onClick: handleSaveNewDataset, className: 'save-btn' }
|
||||
]}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label htmlFor="datasetName">Dataset Name</label>
|
||||
<input id="datasetName" type="text" placeholder='e.g., "Core Rulebook Items"' value={newDatasetInfo.name} onChange={e => setNewDatasetInfo({...newDatasetInfo, name: e.target.value})}/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="datasetType">Dataset Type</label>
|
||||
<input id="datasetType" type="text" placeholder='e.g., "spells" or "equipment"' value={newDatasetInfo.type} onChange={e => setNewDatasetInfo({...newDatasetInfo, type: e.target.value})}/>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Confirm Deletion Modal */}
|
||||
<Modal
|
||||
isOpen={!!confirmDeleteInfo}
|
||||
onClose={() => setConfirmDeleteInfo(null)}
|
||||
title="Confirm Deletion"
|
||||
actions={[
|
||||
{ label: 'Cancel', onClick: () => setConfirmDeleteInfo(null), className: 'load-btn' },
|
||||
{ label: 'Delete', onClick: handleExecuteDelete, className: 'delete-btn' }
|
||||
]}
|
||||
>
|
||||
<p>Are you sure you want to delete the {confirmDeleteInfo?.type} "<strong>{confirmDeleteInfo?.name}</strong>"? This action cannot be undone.</p>
|
||||
</Modal>
|
||||
|
||||
|
||||
<div className="datasets-list-panel">
|
||||
<div className="panel-header">
|
||||
<h3>Datasets</h3>
|
||||
</div>
|
||||
<button className="builder-btn new-dataset-btn" onClick={() => { setNewDatasetInfo({name: '', type: 'generic'}); setShowCreateDatasetModal(true); }}>
|
||||
<HugeiconsIcon icon={FolderAddIcon} size={16}/> New Dataset
|
||||
</button>
|
||||
<ul className="list-group">
|
||||
{datasets.map(d => (
|
||||
<li key={d.id}
|
||||
className={`list-group-item ${selectedDatasetId === d.id ? 'active' : ''}`}
|
||||
onClick={() => setSelectedDatasetId(d.id)}>
|
||||
<div className="dataset-info">
|
||||
<span className="dataset-name">{d.name}</span>
|
||||
<span className="dataset-type">{d.type}</span>
|
||||
</div>
|
||||
<button className="builder-btn delete-btn" onClick={(e) => { e.stopPropagation(); setConfirmDeleteInfo({type: 'dataset', id: d.id, name: d.name})}}>
|
||||
<HugeiconsIcon icon={Delete02Icon} size={16}/>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="entries-list-panel">
|
||||
{selectedDataset ? (
|
||||
<>
|
||||
<div className="panel-header">
|
||||
<h3>{selectedDataset.name}</h3>
|
||||
<button className="builder-btn save-btn" onClick={() => setEditingEntry({id: 0, name: '', data: { 'Description': '' }})}>
|
||||
<HugeiconsIcon icon={AddCircleIcon} size={16}/> New Entry
|
||||
</button>
|
||||
</div>
|
||||
<ul className="list-group">
|
||||
{selectedDataset.entries.map(entry => (
|
||||
<li key={entry.id} className="list-group-item">
|
||||
<span className="dataset-info">{entry.name}</span>
|
||||
<div className="entry-actions">
|
||||
<button className="builder-btn load-btn" onClick={() => setEditingEntry(entry)}><HugeiconsIcon icon={Edit02Icon} size={16}/></button>
|
||||
<button className="builder-btn delete-btn" onClick={() => setConfirmDeleteInfo({type: 'entry', id: entry.id, name: entry.name})}><HugeiconsIcon icon={Delete02Icon} size={16}/></button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : <div className="placeholder-text">Select a dataset to view its entries.</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DatasetManager;
|
Loading…
Reference in New Issue
Block a user