diff --git a/rafts/.env.example b/rafts/.env.example new file mode 100644 index 0000000..dbb34d3 --- /dev/null +++ b/rafts/.env.example @@ -0,0 +1,142 @@ +# ============================================================================= +# RAFTS Environment Configuration +# ============================================================================= +# Copy this file to .env and configure for your environment +# Usage: cp .env.example .env +# +# Variables marked [SECRET] should be kept secure and not committed to git +# Variables marked [REQUIRED] must be set for the application to work +# Variables marked [BUILD-TIME] require image rebuild when changed +# ============================================================================= + +# ============================================================================= +# DEPLOYMENT CONFIGURATION +# ============================================================================= + +# Domain where RAFTS will be accessible [REQUIRED] +# Examples: rafts.example.com, localhost +RAFTS_DOMAIN=rafts.localhost + +# Base path for subpath deployment [BUILD-TIME] +# Leave empty for root deployment (rafts.example.com) +# Set to /rafts for subpath deployment (example.com/rafts) +# IMPORTANT: Changing this requires rebuilding the Docker image! +RAFTS_BASE_PATH= + +# ============================================================================= +# TRAEFIK CONFIGURATION (for docker-compose.traefik.yml) +# ============================================================================= + +# Traefik network name (must exist) +# TRAEFIK_NETWORK=traefik_proxy + +# Traefik entrypoint (websecure for HTTPS, web for HTTP) +# TRAEFIK_ENTRYPOINT=websecure + +# Traefik certificate resolver name +# TRAEFIK_CERTRESOLVER=letsencrypt + +# ============================================================================= +# NEXTAUTH CONFIGURATION +# ============================================================================= + +# [REQUIRED] Public URL where the application is accessible +# For production: https://rafts.example.com +# For local dev: http://localhost:3000 +NEXTAUTH_URL=http://localhost:3000 + +# [SECRET] [REQUIRED] Random secret for session encryption +# Generate with: openssl rand -base64 32 +NEXTAUTH_SECRET=CHANGE_ME_generate_with_openssl_rand_base64_32 + +# Enable debug logging (set to 'false' in production) +NEXTAUTH_DEBUG=false + +# ============================================================================= +# APPLICATION SETTINGS +# ============================================================================= + +# Base path prefix (empty for root, or '/subpath' for subdomain deployment) +NEXT_PUBLIC_BASE_PATH= + +# Public API URL (usually same as NEXTAUTH_URL + /api) +NEXT_PUBLIC_API_URL=http://localhost:3000/api + +# Enable review workflow feature +UI_REVIEW_ENABLED=true + +# ============================================================================= +# CADC ACCESS CONTROL (AC) SERVICE +# ============================================================================= +# These URLs point to the CADC authentication and authorization services +# Default values work with CANFAR production environment + +# Login endpoint for CADC authentication +NEXT_CANFAR_AC_LOGIN_URL=https://ws-cadc.canfar.net/ac/login + +# User search endpoint +NEXT_CANFAR_AC_SEARCH_URL=https://ws-cadc.canfar.net/ac/search + +# Who am I endpoint (user identity) +NEXT_CANFAR_AC_WHOAMI_URL=https://ws-cadc.canfar.net/ac/whoami + +# Groups endpoint +NEXT_CANFAR_AC_GROUPS_URL=https://ws-cadc.canfar.net/ac/groups + +# [REQUIRED] Group name for RAFT reviewers (must exist in AC service) +NEXT_CANFAR_RAFT_GROUP_NAME=RAFTS-reviewers + +# ============================================================================= +# DOI SERVICE CONFIGURATION +# ============================================================================= + +# [REQUIRED] DOI service base URL +# Production: https://ws-cadc.canfar.net/doi/instances +# QA: https://rafts-api-qa.testapp.ca/rafts/instances +# Local dev with Docker DOI: http://host.docker.internal:8080/rafts/instances +NEXT_DOI_BASE_URL=https://ws-cadc.canfar.net/doi/instances + +# ============================================================================= +# STORAGE CONFIGURATION (CANFAR Vault/VOSpace) +# ============================================================================= + +# Base URL for file storage operations +NEXT_CANFAR_STORAGE_BASE_URL=https://ws-cadc.canfar.net/vault/files + +# Vault endpoint for file operations +NEXT_VAULT_BASE_ENDPOINT=https://ws-cadc.canfar.net/vault/files + +# Storage path prefix for RAFT data +# Production: AstroDataCitationDOI/CISTI.CANFAR +# Test: rafts-test +NEXT_CITE_URL=AstroDataCitationDOI/CISTI.CANFAR + +# ============================================================================= +# SSO COOKIE CONFIGURATION +# ============================================================================= + +# Cookie key for CADC SSO token +NEXT_COOKIE_SSO_KEY=CADC_SSO + +# Cookie domains for CANFAR and CADC +NEXT_CANFAR_COOKIE_DOMAIN=canfar.net +NEXT_CANFAR_COOKIE_URL=https://www.canfar.net/access/sso?cookieValue= +NEXT_CADC_COOKIE_DOMAIN=cadc-ccda.hia-iha.nrc-cnrc.gc.ca +NEXT_CADC_COOKIE_URL=https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/access/sso?cookieValue= + +# ============================================================================= +# VALIDATOR SERVICE (Internal - usually auto-configured) +# ============================================================================= +# These are typically overridden by docker-compose for internal networking +# Only set manually if running validator externally + +# NEXT_PUBLIC_VALIDATOR_URL_XML=http://localhost:8000/validate-xml +# NEXT_PUBLIC_VALIDATOR_URL_PSV=http://localhost:8000/validate-psv +# NEXT_PUBLIC_VALIDATOR_URL_MPC=http://localhost:8000/validate-mpc + +# ============================================================================= +# SSL/TLS CONFIGURATION (for production with HTTPS) +# ============================================================================= + +# Disable TLS verification (ONLY for local development with self-signed certs) +# NODE_TLS_REJECT_UNAUTHORIZED=0 diff --git a/rafts/.gitignore b/rafts/.gitignore new file mode 100644 index 0000000..03fa183 --- /dev/null +++ b/rafts/.gitignore @@ -0,0 +1,173 @@ +# ============================================================================= +# RAFTS Repository - Root .gitignore +# ============================================================================= +# This file covers root-level ignores. Subprojects have their own .gitignore +# files for project-specific patterns. +# ============================================================================= + +# ============================================================================= +# ENVIRONMENT & SECRETS +# ============================================================================= +# Environment files contain secrets - NEVER commit these +.env +.env.local +.env.development +.env.development.local +.env.test +.env.test.local +.env.production +.env.production.local +*.env.backup + +# Keep the example template +!.env.example + +# SSL certificates and keys +*.pem +*.key +*.crt +*.p12 +*.jks + +# ============================================================================= +# OPERATING SYSTEM +# ============================================================================= +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* +.Spotlight-V100 +.Trashes + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# Linux +*~ +.directory + +# ============================================================================= +# IDE & EDITORS +# ============================================================================= +# JetBrains (IntelliJ, WebStorm, PyCharm) +.idea/ +*.iml +*.ipr +*.iws +out/ + +# Visual Studio Code +.vscode/ +*.code-workspace + +# Vim/Neovim +*.swp +*.swo +*.swn +*~ +Session.vim +.netrwhist + +# Emacs +*~ +\#*\# +.#* +auto-save-list + +# Sublime Text +*.sublime-project +*.sublime-workspace + +# ============================================================================= +# AI ASSISTANT CONFIGS & PROMPTING FILES +# ============================================================================= +# Claude Code project-specific settings +.claude/ +CLAUDE.md + +# GitHub Copilot +.copilot/ + +# Cursor +.cursor/ + +# Codex and other AI assistants +.codex/ +.aider* +.continue/ + +# ============================================================================= +# DEVELOPMENT DOCUMENTATION (Internal/Planning) +# ============================================================================= +# Development notes, planning docs, guides +doc_n_dev/ +**/doc_n_dev/ + +# ============================================================================= +# DOCKER +# ============================================================================= +# Docker Compose override files (local customizations) +docker-compose.override.yml +docker-compose.local.yml + +# Docker data volumes (if mounted locally) +docker-data/ +volumes/ + +# ============================================================================= +# LOGS & TEMPORARY FILES +# ============================================================================= +*.log +logs/ +tmp/ +temp/ +.tmp/ +.temp/ +.cache/ + +# ============================================================================= +# BUILD ARTIFACTS (Root Level) +# ============================================================================= +dist/ +build/ +out/ + +# ============================================================================= +# JAVA/GRADLE (For parent monorepo compatibility) +# ============================================================================= +# Gradle +.gradle/ +build/ +!gradle/wrapper/gradle-wrapper.jar + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties + +# ============================================================================= +# MISCELLANEOUS +# ============================================================================= +# Backup files +*.bak +*.backup +*.orig + +# Archives (shouldn't be in repo) +*.zip +*.tar.gz +*.tgz +*.rar +*.7z + +# Local notes (personal) +TODO.local.md +NOTES.local.md +scratch.* diff --git a/rafts/README.md b/rafts/README.md index 809d68d..31865dd 100644 --- a/rafts/README.md +++ b/rafts/README.md @@ -1 +1,285 @@ -# RAFTS (Research Announcements for the Solar System) UI +# RAFTS - Research Announcement for Transient Sources + +A web application for managing astronomical transient observation submissions (RAFTs) with ADES file validation. + +## Quick Start + +### Prerequisites + +- Docker and Docker Compose +- Git + +### 1. Clone and Configure + +```bash +# Clone the repository +git clone rafts +cd rafts + +# Copy environment template +cp .env.example .env + +# Edit configuration (at minimum, set NEXTAUTH_SECRET) +# Generate a secret: openssl rand -base64 32 +nano .env +``` + +### 2. Deploy + +```bash +# Development (with hot reload) +./deploy.sh dev + +# Production +./deploy.sh prod + +# Check status +./deploy.sh health +``` + +### 3. Access + +| Environment | Frontend URL | Validator API | +|-------------|--------------|---------------| +| Development | http://localhost:3000 | http://localhost:8000 | +| Production | http://localhost (via nginx) | Internal only | + +--- + +## Project Structure + +``` +rafts/ +├── frontend/ # Next.js application +│ ├── Dockerfile # Production build +│ ├── Dockerfile.dev # Development with hot reload +│ ├── src/ # Application source code +│ └── ... +├── api/ +│ └── validator/ # ADES validation API (FastAPI) +│ ├── Dockerfile # Production build +│ ├── app/ # FastAPI application +│ └── ades/ # ADES validation library +├── nginx/ # Nginx configuration (production) +├── docker-compose.yml # Unified deployment +├── deploy.sh # Deployment script +├── .env.example # Environment template +└── README.md # This file +``` + +--- + +## Deployment Commands + +```bash +# Start development environment +./deploy.sh dev + +# Start production environment +./deploy.sh prod + +# View logs +./deploy.sh logs # All services +./deploy.sh logs rafts-frontend # Frontend only + +# Check health +./deploy.sh health + +# Stop all services +./deploy.sh stop + +# Clean up (remove containers and images) +./deploy.sh clean +``` + +--- + +## Configuration + +### Required Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `NEXTAUTH_URL` | Public URL of the application | `https://rafts.example.com` | +| `NEXTAUTH_SECRET` | Session encryption secret | `openssl rand -base64 32` | +| `NEXT_DOI_BASE_URL` | DOI service endpoint | `https://ws-cadc.canfar.net/doi/instances` | +| `NEXT_CANFAR_RAFT_GROUP_NAME` | Reviewer group name | `RAFTS-reviewers` | + +### Optional Configuration + +See `.env.example` for complete list of configuration options including: +- CADC Access Control settings +- Storage configuration +- SSO cookie settings +- SSL/TLS options + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Client │ +└────────────────────────────┬────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Nginx (Production) │ +│ Port 80/443 │ +└────────────────────────────┬────────────────────────────────┘ + │ + ┌──────────────┴──────────────┐ + │ │ + ▼ ▼ +┌─────────────────────────┐ ┌─────────────────────────┐ +│ rafts-frontend │ │ ades-validator-api │ +│ (Next.js) │──▶│ (FastAPI) │ +│ Port 8080 │ │ Port 8000 │ +└─────────────────────────┘ └─────────────────────────┘ + │ + ▼ +┌─────────────────────────┐ +│ External Services │ +│ - CADC AC (Auth) │ +│ - DOI Service │ +│ - VOSpace Storage │ +└─────────────────────────┘ +``` + +--- + +## Development + +### Running Locally (without Docker) + +```bash +# Frontend +cd frontend +npm install +npm run dev + +# Validator API +cd api/validator +pip install -e ".[dev]" +uvicorn app.main:app --reload +``` + +### Code Quality + +```bash +cd frontend +npx tsc --noEmit # Type check +npm run lint # Lint +npm run format # Format code +npm run test:run # Run tests +``` + +--- + +## Production Deployment + +### Option 1: Traefik (Recommended for subpath/subdomain) + +For deployment behind an existing Traefik reverse proxy: + +```bash +# Configure environment +cp .env.example .env +# Edit .env - set NEXTAUTH_URL, NEXTAUTH_SECRET, RAFTS_DOMAIN + +# Subdomain deployment (rafts.example.com) +RAFTS_DOMAIN=rafts.example.com docker compose -f docker-compose.traefik.yml up -d --build + +# Subpath deployment (example.com/rafts) +# 1. Set RAFTS_BASE_PATH=/rafts in .env +# 2. Set NEXT_PUBLIC_BASE_PATH=/rafts in .env +# 3. Edit docker-compose.traefik.yml - uncomment PathPrefix router rule +RAFTS_DOMAIN=example.com RAFTS_BASE_PATH=/rafts docker compose -f docker-compose.traefik.yml up -d --build +``` + +**Important for subpath deployment:** +- `NEXT_PUBLIC_BASE_PATH` is baked into the build - rebuild when changing +- Traefik should NOT strip the prefix (Next.js handles it via basePath) +- Set `NEXTAUTH_URL` to the full URL including the base path + +### Option 2: Standalone with Nginx + +For standalone deployment with included nginx: + +```bash +./deploy.sh prod +``` + +### Option 3: With cadc-ui Infrastructure + +For deployment alongside existing cadc-ui infrastructure: + +```bash +cd frontend +docker compose -f docker-compose.prod.yml up -d --build +./scripts/post-deploy.sh +``` + +### SSL Certificates + +For HTTPS with Let's Encrypt (standalone nginx): + +1. Ensure your domain points to the server +2. Uncomment SSL server block in `nginx/conf.d/default.conf` +3. Run certbot: + ```bash + docker compose --profile ssl run certbot certonly \ + --webroot -w /var/www/certbot \ + -d your-domain.com + ``` +4. Restart nginx: `docker compose restart nginx` + +--- + +## Health Endpoints + +| Service | Endpoint | Response | +|---------|----------|----------| +| Frontend | `/api/health` | `{"status":"ok"}` | +| Validator | `/health-check` | `{"status":"healthy",...}` | + +--- + +## Troubleshooting + +### Logs + +```bash +# All services +./deploy.sh logs + +# Specific service +docker logs rafts-frontend-nextjs +docker logs ades-validator-api +``` + +### Common Issues + +**Container won't start** +```bash +docker logs rafts-frontend-nextjs +docker inspect rafts-frontend-nextjs +``` + +**Validator not reachable from frontend** +- Ensure both containers are on the same network +- Check environment variables: `docker exec rafts-frontend-nextjs env | grep VALIDATOR` + +**Authentication issues** +- Verify CADC AC service URLs in `.env` +- Check `NEXTAUTH_DEBUG=true` for detailed logs + +--- + +## License + +[Add license information] + +## Contributing + +[Add contribution guidelines] diff --git a/rafts/api/validator/.dockerignore b/rafts/api/validator/.dockerignore new file mode 100644 index 0000000..c38a1f2 --- /dev/null +++ b/rafts/api/validator/.dockerignore @@ -0,0 +1,111 @@ +# ============================================================================= +# RAFTS Validator API - Docker Build Context Exclusions +# ============================================================================= +# Optimize Docker build context size and speed. +# These files are NOT needed for production builds. +# ============================================================================= + +# ============================================================================= +# VERSION CONTROL +# ============================================================================= +.git +.gitignore +.gitattributes + +# ============================================================================= +# PYTHON CACHE & BYTECODE +# ============================================================================= +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# ============================================================================= +# VIRTUAL ENVIRONMENTS +# ============================================================================= +.env +.venv/ +venv/ +ENV/ +env/ + +# ============================================================================= +# BUILD & DISTRIBUTION +# ============================================================================= +build/ +dist/ +*.egg-info/ +*.egg +wheels/ +sdist/ + +# ============================================================================= +# TESTING +# ============================================================================= +tests/ +.pytest_cache/ +.coverage +coverage.xml +htmlcov/ +.tox/ +.nox/ +pytest.ini +test_main.http +run_tests.sh +test_output/ + +# ============================================================================= +# TYPE CHECKING & LINTING +# ============================================================================= +.mypy_cache/ +.ruff_cache/ + +# ============================================================================= +# IDE & EDITORS +# ============================================================================= +.idea/ +.vscode/ +*.swp +*.swo + +# ============================================================================= +# DOCUMENTATION +# ============================================================================= +*.md +docs/ +LICENSE + +# ============================================================================= +# OS FILES +# ============================================================================= +.DS_Store +Thumbs.db +*.log + +# ============================================================================= +# DOCKER (Prevent recursive context) +# ============================================================================= +Dockerfile* +docker-compose* +.dockerignore + +# ============================================================================= +# SECURITY +# ============================================================================= +*.pem +*.key +*.crt + +# ============================================================================= +# TEMPORARY & VALIDATION ARTIFACTS +# ============================================================================= +tmp/ +temp/ +.cache/ +valsubmit.file +*.80col.xml +*.psv.xml +tmp_*.xml +tmp_*.psv +tmp_*.80col diff --git a/rafts/api/validator/.gitignore b/rafts/api/validator/.gitignore new file mode 100644 index 0000000..27d83b4 --- /dev/null +++ b/rafts/api/validator/.gitignore @@ -0,0 +1,146 @@ +# ============================================================================= +# RAFTS Validator API - Python/FastAPI .gitignore +# ============================================================================= + +# ============================================================================= +# PYTHON BYTECODE & CACHE +# ============================================================================= +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python + +# ============================================================================= +# VIRTUAL ENVIRONMENTS +# ============================================================================= +.env +.venv/ +venv/ +ENV/ +env/ +env.bak/ +venv.bak/ + +# ============================================================================= +# PACKAGE BUILD & DISTRIBUTION +# ============================================================================= +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# ============================================================================= +# TESTING & COVERAGE +# ============================================================================= +.pytest_cache/ +.coverage +.coverage.* +coverage.xml +*.cover +htmlcov/ +.tox/ +.nox/ +nosetests.xml +test_output/ +pytest.ini + +# ============================================================================= +# TYPE CHECKING & LINTING +# ============================================================================= +.mypy_cache/ +.ruff_cache/ +.dmypy.json +dmypy.json + +# ============================================================================= +# IDE & EDITORS +# ============================================================================= +.idea/ +.vscode/ +*.swp +*.swo +*.swn +.project +.pydevproject +.settings/ +.vs/ +.atom/ +.spyderproject +.spyproject +.ropeproject + +# ============================================================================= +# OS FILES +# ============================================================================= +.DS_Store +Thumbs.db +*~ + +# ============================================================================= +# LOGS & TEMPORARY FILES +# ============================================================================= +*.log +logs/ +tmp/ +temp/ +.cache/ + +# ============================================================================= +# FASTAPI / UVICORN +# ============================================================================= +.uvicorn.sock + +# ============================================================================= +# DEPENDENCY MANAGEMENT +# ============================================================================= +# UV lock file (if using uv) +uv.lock + +# Pip +pip-log.txt +pip-delete-this-directory.txt + +# ============================================================================= +# JUPYTER (if used for development) +# ============================================================================= +.ipynb_checkpoints/ +*.ipynb + +# ============================================================================= +# ADES SPECIFIC - Validation Artifacts +# ============================================================================= +# Submission files created during validation +valsubmit.file + +# Converted/temporary ADES files +*.80col.xml +*.psv.xml +tmp_*.xml +tmp_*.psv +tmp_*.80col + +# ============================================================================= +# SECURITY +# ============================================================================= +*.pem +*.key +*.crt + +# ============================================================================= +# ENVIRONMENT FILES +# ============================================================================= +# Keep template +!.env.example diff --git a/rafts/api/validator/Dockerfile b/rafts/api/validator/Dockerfile new file mode 100644 index 0000000..52b874d --- /dev/null +++ b/rafts/api/validator/Dockerfile @@ -0,0 +1,45 @@ +# ADES Validator API - Production Dockerfile +# Multi-stage build for smaller, secure image + +FROM python:3.11-slim AS builder + +WORKDIR /app + +# Copy dependency files first (for better caching) +COPY pyproject.toml ./ +COPY app/ ./app/ + +# Install dependencies (including iau-ades from PyPI) to user location +RUN pip install --no-cache-dir --user . + +# ============================================================================= +# Production Image +# ============================================================================= +FROM python:3.11-slim + +WORKDIR /app + +# Create non-root user for security +RUN groupadd -r -g 1001 validator && \ + useradd -r -u 1001 -g validator validator + +# Copy installed packages from builder (includes iau-ades and its data files) +COPY --from=builder /root/.local /home/validator/.local + +# Copy application code +COPY --chown=validator:validator app/ ./app/ + +# Set Python path +ENV PATH=/home/validator/.local/bin:$PATH +ENV PYTHONPATH=/app + +# Switch to non-root user +USER validator + +EXPOSE 8080 + +# Health check using Python (avoids need for wget/curl) +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health-check')" || exit 1 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/rafts/api/validator/LICENSE b/rafts/api/validator/LICENSE new file mode 100644 index 0000000..c13f991 --- /dev/null +++ b/rafts/api/validator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/rafts/api/validator/README.md b/rafts/api/validator/README.md new file mode 100644 index 0000000..7a37ee2 --- /dev/null +++ b/rafts/api/validator/README.md @@ -0,0 +1,226 @@ +# RAFT ADES Validator + +![Tests](https://github.com/zss1980/ades-val/workflows/Python%20Tests/badge.svg) +[![codecov](https://codecov.io/gh/zss1980/ades-val/branch/main/graph/badge.svg)](https://codecov.io/gh/zss1980/ades-val) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +A FastAPI application for validating [ADES (Astrodynamics Data Exchange Standard)](https://minorplanetcenter.net/iau/info/ADES.html) files in different formats. The validator is built on top of the official [IAU-ADES/ADES-Master](https://github.com/IAU-ADES/ADES-Master) repository. + +## Features + +- **XML Validation**: Direct validation of ADES XML files against general and submission schemas +- **PSV Conversion**: Convert PSV format to XML and validate +- **MPC 80-column Format**: Convert MPC 80-column format to XML and validate +- **Comprehensive Testing**: 83% test coverage with automated CI +- **REST API**: Simple HTTP interface with FastAPI + +## Installation + +### Prerequisites + +- Python 3.10+ +- Git + +### Setup + +```bash +# Clone the repository +git clone https://github.com/zss1980/ades-val.git +cd ades-val + +# Create and activate a virtual environment +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install dependencies with uv (recommended) +uv pip install -e . + +# Or with standard pip +pip install -e . +``` + +## Development + +### Install development dependencies + +```bash +# Using uv +uv pip install -e .[dev] + +# Or using standard pip +pip install -e .[dev] +``` + +This installs tools like **pre-commit**, **black**, and **ruff** used for code formatting and linting. + +### Set up pre-commit hooks + +```bash +pre-commit install +``` + +### Running Tests + +```bash +# Run all tests +pytest + +# Run tests with coverage +pytest --cov=app --cov-report=html + +# Run specific test file +pytest tests/test_xml_validation.py +``` + +## Running the API + +```bash +# Development server with auto-reload +uvicorn app.main:app --reload + +# Production server +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +The API will be available at http://127.0.0.1:8000. + +API documentation is automatically generated and available at: + +- Swagger UI: http://127.0.0.1:8000/docs +- ReDoc: http://127.0.0.1:8000/redoc + +## API Endpoints + +### Health Check + +``` +GET /health-check +``` + +Verifies that the API and ADES tools are working correctly. + +### Validate XML + +``` +POST /validate-xml +``` + +Parameters: +- `file`: XML file to validate (form-data) +- `validation_type`: Type of validation to perform (all, submit, general) + +Example: +```bash +curl -X POST http://localhost:8000/validate-xml \ + -H "Content-Type: multipart/form-data" \ + -F "file=@path/to/your/file.xml" \ + -F "validation_type=all" +``` + +### Validate PSV + +``` +POST /validate-psv +``` + +Parameters: +- `file`: PSV file to convert and validate (form-data) +- `validation_type`: Type of validation to perform (all, submit, general) + +Example: +```bash +curl -X POST http://localhost:8000/validate-psv \ + -H "Content-Type: multipart/form-data" \ + -F "file=@path/to/your/file.psv" \ + -F "validation_type=all" +``` + +### Validate MPC + +``` +POST /validate-mpc +``` + +Parameters: +- `file`: MPC 80-column format file to convert and validate (form-data) +- `validation_type`: Type of validation to perform (all, submit, general) + +Example: +```bash +curl -X POST http://localhost:8000/validate-mpc \ + -H "Content-Type: multipart/form-data" \ + -F "file=@path/to/your/file.txt" \ + -F "validation_type=all" +``` + +### Model Context + +``` +GET /model-context +``` + +Provides metadata about the service, including version and supported validation types. + +## Project Structure + +``` +ades-val/ +├── app/ # Application code +│ ├── __init__.py +│ ├── main.py # FastAPI application entry point +│ ├── config.py # Configuration settings +│ ├── routes/ # API endpoints +│ │ ├── __init__.py +│ │ ├── health.py # Health check endpoints +│ │ ├── xml_validation.py # XML validation endpoints +│ │ ├── psv_validation.py # PSV validation endpoints +│ │ └── mpc_validation.py # MPC validation endpoints +│ └── utils/ # Utility functions +│ ├── __init__.py +│ ├── paths.py # Path definitions +│ ├── validation.py # Validation functions +│ └── conversion.py # Conversion functions +├── ades/ # ADES-Master repository files +│ ├── Python/ # ADES Python implementation +│ ├── xml/ # XML resources +│ ├── xsd/ # XSD schemas +│ └── xslt/ # XSLT transformations +├── tests/ # Test suite +│ ├── __init__.py +│ ├── conftest.py # Test fixtures +│ ├── test_health.py +│ ├── test_xml_validation.py +│ ├── test_psv_validation.py +│ ├── test_mpc_validation.py +│ └── data/ # Test data +├── .github/ # GitHub workflows +│ └── workflows/ +│ └── tests.yml # CI workflow +├── .pre-commit-config.yaml # Pre-commit hooks configuration +├── pyproject.toml # Project configuration +├── pytest.ini # Pytest configuration +├── .gitignore # Git ignore rules +└── README.md # This file +``` + +## Contributing + +Contributions are welcome! Please follow these steps: + +1. Fork the repository +2. Create a feature branch: `git checkout -b feature-name` +3. Make your changes and commit them: `git commit -m 'Add some feature'` +4. Push to the branch: `git push origin feature-name` +5. Submit a pull request + +Please make sure your code passes all tests and pre-commit hooks before submitting a pull request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgements + +- [IAU-ADES/ADES-Master](https://github.com/IAU-ADES/ADES-Master) for the ADES implementation +- [FastAPI](https://fastapi.tiangolo.com/) for the web framework +- [uvicorn](https://www.uvicorn.org/) for the ASGI server diff --git a/rafts/api/validator/app/__init__.py b/rafts/api/validator/app/__init__.py new file mode 100644 index 0000000..24fef06 --- /dev/null +++ b/rafts/api/validator/app/__init__.py @@ -0,0 +1,64 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** diff --git a/rafts/api/validator/app/config.py b/rafts/api/validator/app/config.py new file mode 100644 index 0000000..4af669b --- /dev/null +++ b/rafts/api/validator/app/config.py @@ -0,0 +1,87 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +import logging +from pathlib import Path + +import ades.adesutility + +# Setup logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +# ADES package paths (derived from pip-installed iau-ades) +_ADES_PKG_DIR = Path(ades.adesutility.mypath) +ADES_DATA_DIR = _ADES_PKG_DIR / "data" +ADES_XSD_DIR = ADES_DATA_DIR / "xsd" +ADES_XML_DIR = ADES_DATA_DIR / "xml" +ADES_XSLT_DIR = ADES_DATA_DIR / "xslt" + +# API settings +API_TITLE = "RAFT ADES Validator" +API_DESCRIPTION = "API for validating ADES files in various formats" +API_VERSION = "1.0.0" diff --git a/rafts/api/validator/app/main.py b/rafts/api/validator/app/main.py new file mode 100644 index 0000000..532cb4b --- /dev/null +++ b/rafts/api/validator/app/main.py @@ -0,0 +1,83 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from fastapi import FastAPI +from app.config import API_TITLE, API_DESCRIPTION, API_VERSION +from app.routes import ( + health, + xml_validation, + psv_validation, + mpc_validation, + model_context, +) + +app = FastAPI(title=API_TITLE, description=API_DESCRIPTION, version=API_VERSION) + +# Include routers +app.include_router(health.router) +app.include_router(xml_validation.router) +app.include_router(psv_validation.router) +app.include_router(mpc_validation.router) +app.include_router(model_context.router) diff --git a/rafts/api/validator/app/routes/__init__.py b/rafts/api/validator/app/routes/__init__.py new file mode 100644 index 0000000..24fef06 --- /dev/null +++ b/rafts/api/validator/app/routes/__init__.py @@ -0,0 +1,64 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** diff --git a/rafts/api/validator/app/routes/health.py b/rafts/api/validator/app/routes/health.py new file mode 100644 index 0000000..a7cc196 --- /dev/null +++ b/rafts/api/validator/app/routes/health.py @@ -0,0 +1,212 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +import importlib.util + +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +from app.config import ( + ADES_XML_DIR, + ADES_XSD_DIR, + ADES_XSLT_DIR, + logger, +) +from app.utils.paths import get_converter_path + +router = APIRouter() + + +def _module_exists(module_name): + """Check if a Python module can be imported.""" + return importlib.util.find_spec(module_name) is not None + + +@router.get("/health-check") +async def health_check(): + """ + Health check endpoint to verify the API and ADES tools are working. + """ + try: + # Check iau-ades package is installed and data directories exist + status = { + "ades_package_installed": _module_exists("ades"), + "xml_dir_exists": ADES_XML_DIR.exists(), + "xsd_dir_exists": ADES_XSD_DIR.exists(), + "xslt_dir_exists": ADES_XSLT_DIR.exists(), + } + + # Check for validator modules in the ades package + validator_modules = ["ades.valall", "ades.valsubmit", "ades.valgeneral"] + for mod_name in validator_modules: + status[f"{mod_name.split('.')[-1]}_available"] = _module_exists(mod_name) + + # Check for converter modules + status["psvtoxml_available"] = get_converter_path("psvtoxml") is not None + status["mpc80coltoxml_available"] = ( + get_converter_path("mpc80coltoxml") is not None + ) + + # Check for XSD files + xsd_files = { + "submit.xsd": ADES_XSD_DIR / "submit.xsd", + "general.xsd": ADES_XSD_DIR / "general.xsd", + } + for xsd_name, xsd_path in xsd_files.items(): + status[f"{xsd_name}_exists"] = xsd_path.exists() + + # Check for adesmaster.xml + adesmaster = ADES_XML_DIR / "adesmaster.xml" + status["adesmaster_xml_exists"] = adesmaster.exists() + + # Check LXML availability + status["lxml_available"] = _module_exists("lxml.etree") + + # Feature availability + features = { + "xml_validation": all( + status.get(f"{mod}_available", False) + for mod in ["valall", "valsubmit", "valgeneral"] + ), + "psv_validation": status["psvtoxml_available"], + "mpc_validation": status["mpc80coltoxml_available"], + } + status["features"] = features + + # Overall status + critical_components = [ + status["ades_package_installed"], + status["xsd_dir_exists"], + status["lxml_available"], + any(status.get(f"{xsd}_exists", False) for xsd in xsd_files), + ] + service_ready = all(critical_components) + + status["status"] = "healthy" if service_ready else "degraded" + + if service_ready: + available_features = [ + name for name, available in features.items() if available + ] + missing_features = [ + name for name, available in features.items() if not available + ] + + if all(features.values()): + status["message"] = "Service is fully operational with all features" + else: + status["message"] = ( + f"Service is operational with limited features. " + f"Available: {', '.join(available_features)}. " + f"Missing: {', '.join(missing_features)}" + ) + else: + status["message"] = "Some critical components are missing" + + http_status = 200 if service_ready else 503 + return JSONResponse(status_code=http_status, content=status) + except Exception as e: + logger.error(f"Health check error: {str(e)}") + return JSONResponse( + status_code=500, + content={ + "status": "error", + "message": f"Error during health check: {str(e)}", + }, + ) + + +@router.get("/") +async def home(): + """ + API home page with information about available endpoints. + """ + # Check which features are available + psv_converter_exists = get_converter_path("psvtoxml") is not None + mpc_converter_exists = get_converter_path("mpc80coltoxml") is not None + + # Create the response + endpoints = { + "/validate-xml": {"description": "Validate ADES XML files", "available": True}, + "/validate-psv": { + "description": "Convert PSV to XML and validate", + "available": psv_converter_exists, + }, + "/validate-mpc": { + "description": "Convert MPC 80-column format to XML and validate", + "available": mpc_converter_exists, + }, + "/health-check": { + "description": "Check API and ADES tools health", + "available": True, + }, + } + + return { + "title": "RAFT ADES Validator API", + "description": "API for validating ADES files in various formats", + "version": "1.0.0", + "endpoints": endpoints, + } diff --git a/rafts/api/validator/app/routes/model_context.py b/rafts/api/validator/app/routes/model_context.py new file mode 100644 index 0000000..158968c --- /dev/null +++ b/rafts/api/validator/app/routes/model_context.py @@ -0,0 +1,79 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from fastapi import APIRouter +from app.config import API_TITLE, API_VERSION + +router = APIRouter() + + +@router.get("/model-context") +async def get_model_context(): + """Return metadata about the model context protocol.""" + return { + "service": API_TITLE, + "version": API_VERSION, + "supported_validation_types": ["all", "submit", "general"], + } diff --git a/rafts/api/validator/app/routes/mpc_validation.py b/rafts/api/validator/app/routes/mpc_validation.py new file mode 100644 index 0000000..d4b0a57 --- /dev/null +++ b/rafts/api/validator/app/routes/mpc_validation.py @@ -0,0 +1,151 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from fastapi import APIRouter, UploadFile, File, Form, HTTPException +import tempfile +import os +from app.config import logger +from app.utils.validation import validate_ades_xml, extract_xml_info +from app.utils.conversion import convert_mpc_to_xml + +router = APIRouter() + + +@router.post("/validate-mpc") +async def validate_mpc( + file: UploadFile = File(...), validation_type: str = Form("all") +): + """ + Convert an MPC 80-column format file to XML and then validate it against ADES schemas. + + - validation_type: Type of validation to perform (all, submit, general) + """ + # Check if validation_type is valid + if validation_type not in ["all", "submit", "general"]: + raise HTTPException( + status_code=400, + detail=f"Invalid validation type: {validation_type}. Must be one of: all, submit, general", + ) + + # Check if file extension is appropriate for MPC format + valid_extensions = [ + ".txt", + ".mpc", + ".80col", + "", + ] # Some MPC files might not have an extension + file_ext = os.path.splitext(file.filename)[1].lower() + + if file_ext not in valid_extensions: + raise HTTPException( + status_code=400, + detail=f"File extension '{file_ext}' is not recognized as an MPC 80-column format. Expected: {valid_extensions}", + ) + + # Create temporary files for the conversion process + with tempfile.NamedTemporaryFile(delete=False, suffix=".80col") as mpc_file: + mpc_content = await file.read() + mpc_file.write(mpc_content) + mpc_path = mpc_file.name + + xml_path = f"{mpc_path}.xml" + + try: + # Step 1: Convert MPC 80-column to XML + conversion_success, conversion_message = await convert_mpc_to_xml( + mpc_path, xml_path + ) + + if not conversion_success: + return { + "filename": file.filename, + "conversion": {"success": False, "message": conversion_message}, + "results": [], + } + + # Step 2: Validate the generated XML + validation_results = await validate_ades_xml(xml_path, validation_type) + + # Extract XML information if possible + xml_info = extract_xml_info(xml_path) + + # Return the results + return { + "filename": file.filename, + "validation_type": validation_type, + "conversion": {"success": True, "message": conversion_message}, + "results": validation_results, + "xml_info": xml_info, + } + + except Exception as e: + logger.error(f"MPC validation error: {str(e)}") + raise HTTPException(status_code=500, detail=f"MPC validation error: {str(e)}") + finally: + # Clean up temporary files + if os.path.exists(mpc_path): + os.unlink(mpc_path) + if os.path.exists(xml_path): + os.unlink(xml_path) diff --git a/rafts/api/validator/app/routes/psv_validation.py b/rafts/api/validator/app/routes/psv_validation.py new file mode 100644 index 0000000..15c5a89 --- /dev/null +++ b/rafts/api/validator/app/routes/psv_validation.py @@ -0,0 +1,140 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from fastapi import APIRouter, UploadFile, File, Form, HTTPException +import tempfile +import os +from app.config import logger +from app.utils.validation import validate_ades_xml, extract_xml_info +from app.utils.conversion import convert_psv_to_xml + +router = APIRouter() + + +@router.post("/validate-psv") +async def validate_psv( + file: UploadFile = File(...), validation_type: str = Form("all") +): + """ + Convert a PSV file to XML and then validate it against ADES schemas. + + - validation_type: Type of validation to perform (all, submit, general) + """ + # Check if validation_type is valid + if validation_type not in ["all", "submit", "general"]: + raise HTTPException( + status_code=400, + detail=f"Invalid validation type: {validation_type}. Must be one of: all, submit, general", + ) + + # Check if file is PSV (case-insensitive) + if not file.filename.lower().endswith(".psv"): + raise HTTPException(status_code=400, detail="File must be a PSV document") + + # Create temporary files for the conversion process + with tempfile.NamedTemporaryFile(delete=False, suffix=".psv") as psv_file: + psv_content = await file.read() + psv_file.write(psv_content) + psv_path = psv_file.name + + xml_path = f"{psv_path}.xml" + + try: + # Step 1: Convert PSV to XML + conversion_success, conversion_message = await convert_psv_to_xml( + psv_path, xml_path + ) + + if not conversion_success: + return { + "filename": file.filename, + "conversion": {"success": False, "message": conversion_message}, + "results": [], + } + + # Step 2: Validate the generated XML + validation_results = await validate_ades_xml(xml_path, validation_type) + + # Extract XML information if possible + xml_info = extract_xml_info(xml_path) + + # Return the results + return { + "filename": file.filename, + "validation_type": validation_type, + "conversion": {"success": True, "message": conversion_message}, + "results": validation_results, + "xml_info": xml_info, + } + + except Exception as e: + logger.error(f"PSV validation error: {str(e)}") + raise HTTPException(status_code=500, detail=f"PSV validation error: {str(e)}") + finally: + # Clean up temporary files + if os.path.exists(psv_path): + os.unlink(psv_path) + if os.path.exists(xml_path): + os.unlink(xml_path) diff --git a/rafts/api/validator/app/routes/xml_validation.py b/rafts/api/validator/app/routes/xml_validation.py new file mode 100644 index 0000000..6d42c89 --- /dev/null +++ b/rafts/api/validator/app/routes/xml_validation.py @@ -0,0 +1,120 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from fastapi import APIRouter, UploadFile, File, Form, HTTPException +import tempfile +import os +from app.config import logger +from app.utils.validation import validate_ades_xml, extract_xml_info + +router = APIRouter() + + +@router.post("/validate-xml") +async def validate_xml( + file: UploadFile = File(...), validation_type: str = Form("all") +): + """ + Validate an ADES XML file against XSD schemas. + + - validation_type: Type of validation to perform (all, submit, general) + """ + # Check if validation_type is valid + if validation_type not in ["all", "submit", "general"]: + raise HTTPException( + status_code=400, + detail=f"Invalid validation type: {validation_type}. Must be one of: all, submit, general", + ) + + # Check if file is XML (case-insensitive) + if not file.filename.lower().endswith(".xml"): + raise HTTPException(status_code=400, detail="File must be an XML document") + + # Create temporary file for the uploaded content + with tempfile.NamedTemporaryFile(delete=False, suffix=".xml") as temp_file: + content = await file.read() + temp_file.write(content) + temp_path = temp_file.name + + try: + # Validate the XML file + validation_results = await validate_ades_xml(temp_path, validation_type) + + # Extract XML root information if possible + xml_info = extract_xml_info(temp_path) + + return { + "filename": file.filename, + "validation_type": validation_type, + "results": validation_results, + "xml_info": xml_info, + } + except Exception as e: + logger.error(f"Validation error: {str(e)}") + raise HTTPException(status_code=500, detail=f"Validation error: {str(e)}") + finally: + # Clean up the temporary file + if os.path.exists(temp_path): + os.unlink(temp_path) diff --git a/rafts/api/validator/app/utils/__init__.py b/rafts/api/validator/app/utils/__init__.py new file mode 100644 index 0000000..24fef06 --- /dev/null +++ b/rafts/api/validator/app/utils/__init__.py @@ -0,0 +1,64 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** diff --git a/rafts/api/validator/app/utils/conversion.py b/rafts/api/validator/app/utils/conversion.py new file mode 100644 index 0000000..cc8663e --- /dev/null +++ b/rafts/api/validator/app/utils/conversion.py @@ -0,0 +1,160 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +import asyncio +import os +import sys + +from lxml import etree + +from app.config import logger + + +async def _run_converter(converter_module_path, input_path, output_path, label): + """ + Run an ADES converter script as a subprocess. + + Subprocess isolation is used because the ADES converter scripts call + exit() on errors, which would kill the FastAPI process if imported directly. + + Args: + converter_module_path: Path to the converter module file + input_path: Path to the input file + output_path: Path to save the resulting XML file + label: Human-readable label for log messages + + Returns: + Tuple of (success, message) + """ + try: + logger.info(f"Converting {label}: {input_path} -> {output_path}") + + process = await asyncio.create_subprocess_exec( + sys.executable, + str(converter_module_path), + str(input_path), + str(output_path), + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout_bytes, stderr_bytes = await process.communicate() + result_stdout = stdout_bytes.decode() + result_stderr = stderr_bytes.decode() + + if ( + process.returncode == 0 + and os.path.exists(output_path) + and not (result_stdout.strip() or result_stderr.strip()) + ): + try: + etree.parse(output_path) + except Exception as e: + if os.path.exists(output_path): + os.unlink(output_path) + return False, f"{label} conversion produced invalid XML: {str(e)}" + return True, f"{label} conversion successful" + else: + error_message = result_stdout + "\n" + result_stderr + return False, f"{label} conversion failed: {error_message}" + + except Exception as e: + logger.error(f"{label} conversion error: {str(e)}") + return False, f"Error during {label} conversion: {str(e)}" + + +async def convert_psv_to_xml(psv_file_path, xml_output_path): + """ + Convert a PSV file to XML format using the ADES converter. + + Args: + psv_file_path: Path to the PSV file + xml_output_path: Path to save the resulting XML file + + Returns: + Tuple of (success, message) + """ + import ades.psvtoxml + + return await _run_converter( + ades.psvtoxml.__file__, psv_file_path, xml_output_path, "PSV to XML" + ) + + +async def convert_mpc_to_xml(mpc_file_path, xml_output_path): + """ + Convert an MPC 80-column format file to XML using the ADES converter. + + Args: + mpc_file_path: Path to the MPC 80-column file + xml_output_path: Path to save the resulting XML file + + Returns: + Tuple of (success, message) + """ + import ades.mpc80coltoxml + + return await _run_converter( + ades.mpc80coltoxml.__file__, mpc_file_path, xml_output_path, "MPC 80-col to XML" + ) diff --git a/rafts/api/validator/app/utils/paths.py b/rafts/api/validator/app/utils/paths.py new file mode 100644 index 0000000..39aa452 --- /dev/null +++ b/rafts/api/validator/app/utils/paths.py @@ -0,0 +1,84 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +import importlib +from pathlib import Path + + +def get_converter_path(converter_name): + """ + Get the path to a converter script from the installed iau-ades package. + + Args: + converter_name: Name of the converter module (e.g. "psvtoxml", "mpc80coltoxml") + + Returns: + Path object to the converter module file, or None if not found + """ + try: + mod = importlib.import_module(f"ades.{converter_name}") + return Path(mod.__file__) + except ImportError: + return None diff --git a/rafts/api/validator/app/utils/validation.py b/rafts/api/validator/app/utils/validation.py new file mode 100644 index 0000000..021b92c --- /dev/null +++ b/rafts/api/validator/app/utils/validation.py @@ -0,0 +1,199 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from lxml import etree +from app.config import ADES_XSD_DIR, logger + +# Disable external entity expansion when parsing XML +XML_PARSER = etree.XMLParser(resolve_entities=False) + + +async def validate_ades_xml(xml_file_path, validation_type): + """ + Validate an ADES XML file using the appropriate XSD schema. + + Args: + xml_file_path: Path to the XML file to validate + validation_type: Type of validation to perform + + Returns: + List of validation results as dictionaries + """ + try: + results = [] + + # Determine which schemas to validate against + if validation_type == "all": + schemas_to_validate = ["submit", "general"] + else: + schemas_to_validate = [validation_type] + + # Parse the XML document to be validated + try: + xml_doc = etree.parse(xml_file_path, parser=XML_PARSER) + except etree.XMLSyntaxError as e: + # If there's a syntax error, that's the only result + return [ + { + "type": "xml", + "valid": False, + "message": f"XML syntax error: {str(e)}", + } + ] + + # Validate against each schema + for schema_type in schemas_to_validate: + xsd_path = ADES_XSD_DIR / f"{schema_type}.xsd" + + if not xsd_path.exists(): + results.append( + { + "type": schema_type, + "valid": False, + "message": f"XSD schema file not found: {xsd_path}", + } + ) + continue + + try: + schema_doc = etree.parse(str(xsd_path), parser=XML_PARSER) + schema = etree.XMLSchema(schema_doc) + + is_valid = schema.validate(xml_doc) + + if is_valid: + results.append( + { + "type": schema_type, + "valid": True, + "message": ( + f"Validation against {schema_type} schema passed" + ), + } + ) + else: + # Get detailed error information + error_log = schema.error_log + errors = [] + for error in error_log: + errors.append( + f"Line {error.line}, Column {error.column}: {error.message}" + ) + + results.append( + { + "type": schema_type, + "valid": False, + "message": ( + f"Validation against {schema_type} schema failed:\n" + + "\n".join(errors) + ), + } + ) + except Exception as e: + results.append( + { + "type": schema_type, + "valid": False, + "message": ( + f"Error validating against {schema_type} schema: {str(e)}" + ), + } + ) + + return results + + except Exception as e: + logger.error(f"Validation error: {str(e)}") + return [ + { + "type": "error", + "valid": False, + "message": f"Error during validation: {str(e)}", + } + ] + + +def extract_xml_info(xml_path): + """ + Extract basic information from an XML file. + + Args: + xml_path: Path to the XML file + + Returns: + Dictionary with XML information, or empty dict if extraction fails + """ + xml_info = {} + try: + tree = etree.parse(xml_path, parser=XML_PARSER) + root = tree.getroot() + xml_info["root_element"] = root.tag + xml_info["version"] = root.get("version", "unknown") + xml_info["attributes"] = {k: v for k, v in root.attrib.items()} + except Exception as e: + logger.warning(f"Could not extract XML information: {str(e)}") + + return xml_info diff --git a/rafts/api/validator/pyproject.toml b/rafts/api/validator/pyproject.toml new file mode 100644 index 0000000..d318275 --- /dev/null +++ b/rafts/api/validator/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "raft-validator" +version = "0.1.0" +description = "RAFT ADES Validator" +dependencies = [ + "fastapi>=0.100.0", + "uvicorn>=0.22.0", + "lxml>=4.9.2", + "python-multipart", + "iau-ades>=0.1.1", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "httpx>=0.24.0", + "pytest-asyncio>=0.23.0", + "pytest-mock>=3.10.0", + "pre-commit>=3.5.0", + "ruff>=0.4.4", +] + + +[tool.setuptools.packages.find] +include = ["app*"] + +[tool.black] +line-length = 88 + +[tool.ruff] +line-length = 88 diff --git a/rafts/api/validator/tests/__init__.py b/rafts/api/validator/tests/__init__.py new file mode 100644 index 0000000..24fef06 --- /dev/null +++ b/rafts/api/validator/tests/__init__.py @@ -0,0 +1,64 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** diff --git a/rafts/api/validator/tests/conftest.py b/rafts/api/validator/tests/conftest.py new file mode 100644 index 0000000..5d5ae07 --- /dev/null +++ b/rafts/api/validator/tests/conftest.py @@ -0,0 +1,190 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +import pytest +from fastapi.testclient import TestClient +from pathlib import Path +import shutil +from app.main import app + +# Define test data directory +TEST_DATA_DIR = Path(__file__).parent / "data" + + +# Make the test data directory available to tests +@pytest.fixture +def test_data_dir(): + """Path to the test data directory""" + return TEST_DATA_DIR + + +@pytest.fixture +def client(): + """FastAPI test client""" + with TestClient(app) as client: + yield client + + +# Create test data once per test session +@pytest.fixture(scope="session", autouse=True) +def create_test_data(): + """Create necessary test data files for testing""" + # Ensure test data directory exists + TEST_DATA_DIR.mkdir(exist_ok=True) + + # Sample valid XML + VALID_XML = """ + + + + + F51 + + + J. Smith + + + + +""" + + # Sample invalid XML + INVALID_XML = """ + + + + F51 + + + +""" + + # Sample PSV file + SAMPLE_PSV = """# version=2022 +# observatory +mpcCode|name +F51|Pan-STARRS 1 +# submitter +name|institution +R. Weryk|University of Hawaii +# observers +name +R. Wainscoat +# measurers +name +R. Weryk +# optical +permID|mode|stn|prog|obsTime|ra|dec|mag|rmsMag|band|photCat|notes +00001|CCD|F51|41|2016-04-28T11:15:59.999Z|150.23|30.21|21.9|0.3|r|2MASS|dwin +""" + + # Sample MPC format + SAMPLE_MPC = """ J99001 C2019 04 30.26891 17 47 44.91 +39 03 22.7 20.1 g F51 + J99001 C2019 05 01.30760 17 47 21.04 +39 00 03.7 19.9 g F51 +""" + + # Create/overwrite test data files + test_files = { + "valid.xml": VALID_XML, + "invalid.xml": INVALID_XML, + "valid.psv": SAMPLE_PSV, + "invalid.psv": "Invalid PSV content", + "valid.mpc": SAMPLE_MPC, + "invalid.mpc": "Invalid MPC content", + } + + created_files = [] + for filename, content in test_files.items(): + file_path = TEST_DATA_DIR / filename + with open(file_path, "w") as f: + f.write(content) + created_files.append(file_path) + + # Provide the test data + yield + + # Cleanup created test data after the session + for file_path in created_files: + if file_path.exists(): + file_path.unlink() + + # Remove the directory if it's empty + try: + TEST_DATA_DIR.rmdir() + except OSError: + # Directory not empty or cannot be removed; remove recursively + shutil.rmtree(TEST_DATA_DIR, ignore_errors=True) + + +# Common helper functions for reading test files +@pytest.fixture +def read_test_file(): + """Helper to read a test file""" + + def _read_file(filename): + with open(TEST_DATA_DIR / filename, "rb") as f: + return f.read() + + return _read_file diff --git a/rafts/api/validator/tests/test_health.py b/rafts/api/validator/tests/test_health.py new file mode 100644 index 0000000..a392fbf --- /dev/null +++ b/rafts/api/validator/tests/test_health.py @@ -0,0 +1,97 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + + +def test_home_endpoint(client): + """Test the home endpoint""" + response = client.get("/") + + assert response.status_code == 200 + assert "title" in response.json() + assert "endpoints" in response.json() + + # Check if main endpoints are listed + endpoints = response.json().get("endpoints", {}) + assert "/validate-xml" in endpoints + assert "/health-check" in endpoints + + +def test_health_check_endpoint(client): + """Test the health check endpoint""" + response = client.get("/health-check") + + # Status code should be either 200 (healthy) or 503 (degraded) + assert response.status_code in [200, 503] + + # Basic response structure + data = response.json() + assert "status" in data + assert data["status"] in ["healthy", "degraded", "error"] + + # Check for features section + if "features" in data: + features = data["features"] + assert isinstance(features, dict) + assert "xml_validation" in features diff --git a/rafts/api/validator/tests/test_model_context.py b/rafts/api/validator/tests/test_model_context.py new file mode 100644 index 0000000..8a7cc22 --- /dev/null +++ b/rafts/api/validator/tests/test_model_context.py @@ -0,0 +1,75 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from fastapi.testclient import TestClient + + +def test_model_context_endpoint(client: TestClient): + response = client.get("/model-context") + assert response.status_code == 200 + data = response.json() + assert "service" in data + assert "version" in data + assert "supported_validation_types" in data diff --git a/rafts/api/validator/tests/test_mpc_validation.py b/rafts/api/validator/tests/test_mpc_validation.py new file mode 100644 index 0000000..ad205b4 --- /dev/null +++ b/rafts/api/validator/tests/test_mpc_validation.py @@ -0,0 +1,158 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from pathlib import Path + +TEST_DATA_DIR = Path(__file__).parent / "data" + + +def test_validate_mpc_success(client): + """Test successful validation of MPC file""" + test_file = TEST_DATA_DIR / "valid.mpc" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-mpc", + files={"file": ("valid.mpc", f, "text/plain")}, + data={"validation_type": "all"}, + ) + + # Verify the response structure + assert response.status_code == 200 + assert "conversion" in response.json() + assert "success" in response.json()["conversion"] + assert "results" in response.json() + + +def test_validate_mpc_conversion_failure(client): + """Test validation of an invalid MPC file that fails conversion""" + # For this test, we'll use a more direct approach + # We need to modify the app route temporarily to force a conversion failure + + # Let's create a truly invalid MPC file that will fail conversion naturally + with open(TEST_DATA_DIR / "invalid.mpc", "w") as f: + f.write("This is not a valid MPC 80-column format file") + + # Now make the request with our custom invalid file + with open(TEST_DATA_DIR / "invalid.mpc", "rb") as f: + response = client.post( + "/validate-mpc", + files={"file": ("invalid.mpc", f, "text/plain")}, + data={"validation_type": "all"}, + ) + + # Verify response status code and JSON payload for a failed conversion + assert response.status_code == 200 + data = response.json() + assert data.get("conversion", {}).get("success") is False + assert data.get("results") == [] + + +def test_validate_mpc_specific_type(client): + """Test validation using a specific validation type""" + test_file = TEST_DATA_DIR / "valid.mpc" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-mpc", + files={"file": ("valid.mpc", f, "text/plain")}, + data={"validation_type": "submit"}, + ) + + # Verify that the response has the expected structure + assert response.status_code == 200 + assert "results" in response.json() + + +def test_validate_mpc_invalid_type(client): + """Test validation with an invalid validation type""" + test_file = TEST_DATA_DIR / "valid.mpc" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-mpc", + files={"file": ("valid.mpc", f, "text/plain")}, + data={"validation_type": "invalid"}, + ) + + assert response.status_code == 400 + assert "detail" in response.json() + assert "Invalid validation type" in response.json()["detail"] + + +def test_validate_mpc_unknown_extension(client): + """Test validation with an unknown file extension""" + test_file = TEST_DATA_DIR / "valid.mpc" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-mpc", + files={"file": ("data.unknown", f, "text/plain")}, + data={"validation_type": "all"}, + ) + + assert response.status_code == 400 + assert "detail" in response.json() + assert "File extension" in response.json()["detail"] diff --git a/rafts/api/validator/tests/test_psv_validation.py b/rafts/api/validator/tests/test_psv_validation.py new file mode 100644 index 0000000..891b27e --- /dev/null +++ b/rafts/api/validator/tests/test_psv_validation.py @@ -0,0 +1,170 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from pathlib import Path + +TEST_DATA_DIR = Path(__file__).parent / "data" + + +def test_validate_psv_success(client): + """Test successful validation of PSV file""" + test_file = TEST_DATA_DIR / "valid.psv" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-psv", + files={"file": ("valid.psv", f, "text/plain")}, + data={"validation_type": "all"}, + ) + + # Verify the response structure + assert response.status_code == 200 + assert "conversion" in response.json() + # Note: We're not checking the specific conversion success value + # since it depends on the actual implementation + assert "results" in response.json() + + +def test_validate_psv_uppercase_extension(client): + """File names with upper-case extension should be accepted""" + test_file = TEST_DATA_DIR / "valid.psv" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-psv", + files={"file": ("VALID.PSV", f, "text/plain")}, + data={"validation_type": "all"}, + ) + + assert response.status_code == 200 + assert "results" in response.json() + + +def test_validate_psv_conversion_failure(client): + """Test validation of an invalid PSV file that fails conversion""" + # Create a truly invalid PSV file + with open(TEST_DATA_DIR / "invalid.psv", "w") as f: + f.write("This is not a valid PSV format file") + + with open(TEST_DATA_DIR / "invalid.psv", "rb") as f: + response = client.post( + "/validate-psv", + files={"file": ("invalid.psv", f, "text/plain")}, + data={"validation_type": "all"}, + ) + + # Verify response status code and JSON payload for a failed conversion + assert response.status_code == 200 + data = response.json() + assert data.get("conversion", {}).get("success") is False + assert data.get("results") == [] + + +def test_validate_psv_specific_type(client): + """Test validation using a specific validation type""" + test_file = TEST_DATA_DIR / "valid.psv" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-psv", + files={"file": ("valid.psv", f, "text/plain")}, + data={"validation_type": "submit"}, + ) + + # Verify that the response has the expected structure + assert response.status_code == 200 + assert "results" in response.json() + + +def test_validate_psv_invalid_type(client): + """Test validation with an invalid validation type""" + test_file = TEST_DATA_DIR / "valid.psv" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-psv", + files={"file": ("valid.psv", f, "text/plain")}, + data={"validation_type": "invalid"}, + ) + + assert response.status_code == 400 + assert "detail" in response.json() + assert "Invalid validation type" in response.json()["detail"] + + +def test_validate_psv_non_psv_file(client): + """Test validation with a non-PSV file""" + test_file = TEST_DATA_DIR / "valid.xml" # Using XML file as non-PSV + + with open(test_file, "rb") as f: + response = client.post( + "/validate-psv", + files={"file": ("file.txt", f, "text/plain")}, + data={"validation_type": "all"}, + ) + + assert response.status_code == 400 + assert "detail" in response.json() + assert "File must be a PSV document" in response.json()["detail"] diff --git a/rafts/api/validator/tests/test_xml_security.py b/rafts/api/validator/tests/test_xml_security.py new file mode 100644 index 0000000..0285ccb --- /dev/null +++ b/rafts/api/validator/tests/test_xml_security.py @@ -0,0 +1,84 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +import asyncio + +from app.utils.validation import validate_ades_xml + + +def test_external_entities_disabled(tmp_path): + xml_content = """ +]> +&xxe; +""" + xml_file = tmp_path / "xxe.xml" + xml_file.write_text(xml_content) + + results = asyncio.run(validate_ades_xml(str(xml_file), "submit")) + + assert isinstance(results, list) + assert results + # Parsing should not fail with a general error + assert results[0].get("type") != "error" diff --git a/rafts/api/validator/tests/test_xml_validation.py b/rafts/api/validator/tests/test_xml_validation.py new file mode 100644 index 0000000..4321051 --- /dev/null +++ b/rafts/api/validator/tests/test_xml_validation.py @@ -0,0 +1,168 @@ +# *********************************************************************** +# ****************** CANADIAN ASTRONOMY DATA CENTRE ****************** +# ************* CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************* +# +# (c) 2026. (c) 2026. +# Government of Canada Gouvernement du Canada +# National Research Council Conseil national de recherches +# Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 +# All rights reserved Tous droits réservés +# +# NRC disclaims any warranties, Le CNRC dénie toute garantie +# expressed, implied, or énoncée, implicite ou légale, +# statutory, of any kind with de quelque nature que ce +# respect to the software, soit, concernant le logiciel, +# including without limitation y compris sans restriction +# any warranty of merchantability toute garantie de valeur +# or fitness for a particular marchande ou de pertinence +# purpose. NRC shall not be pour un usage particulier. +# liable in any event for any Le CNRC ne pourra en aucun cas +# damages, whether direct or être tenu responsable de tout +# indirect, special or general, dommage, direct ou indirect, +# consequential or incidental, particulier ou général, +# arising from the use of the accessoire ou fortuit, résultant +# software. Neither the name de l'utilisation du logiciel. Ni +# of the National Research le nom du Conseil National de +# Council of Canada nor the Recherches du Canada ni les noms +# names of its contributors may de ses participants ne peuvent +# be used to endorse or promote être utilisés pour approuver ou +# products derived from this promouvoir les produits dérivés +# software without specific prior de ce logiciel sans autorisation +# written permission. préalable et particulière +# par écrit. +# +# This file is part of the Ce fichier fait partie du projet +# OpenCADC project. OpenCADC. +# +# OpenCADC is free software: OpenCADC est un logiciel libre ; +# you can redistribute it and/or vous pouvez le redistribuer ou le +# modify it under the terms of modifier suivant les termes de +# the GNU Affero General Public la "GNU Affero General Public +# License as published by the License" telle que publiée +# Free Software Foundation, par la Free Software Foundation +# either version 3 of the : soit la version 3 de cette +# License, or (at your option) licence, soit (à votre gré) +# any later version. toute version ultérieure. +# +# OpenCADC is distributed in the OpenCADC est distribué +# hope that it will be useful, dans l'espoir qu'il vous +# but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE +# without even the implied GARANTIE : sans même la garantie +# warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ +# or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF +# PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence +# General Public License for Générale Publique GNU Affero +# more details. pour plus de détails. +# +# You should have received Vous devriez avoir reçu une +# a copy of the GNU Affero copie de la Licence Générale +# General Public License along Publique GNU Affero avec +# with OpenCADC. If not, see OpenCADC ; si ce n'est +# . pas le cas, consultez : +# . +# +# *********************************************************************** + +from pathlib import Path + +TEST_DATA_DIR = Path(__file__).parent / "data" + + +def test_validate_xml_success(client): + """Test successful validation of XML file""" + test_file = TEST_DATA_DIR / "valid.xml" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-xml", + files={"file": ("valid.xml", f, "application/xml")}, + data={"validation_type": "all"}, + ) + + # Verify the response structure + assert response.status_code == 200 + assert "results" in response.json() + assert "xml_info" in response.json() + + +def test_validate_xml_uppercase_extension(client): + """File names with upper-case extension should be accepted""" + test_file = TEST_DATA_DIR / "valid.xml" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-xml", + files={"file": ("VALID.XML", f, "application/xml")}, + data={"validation_type": "all"}, + ) + + assert response.status_code == 200 + assert "results" in response.json() + + +def test_validate_xml_failure(client): + """Test validation of an invalid XML file""" + test_file = TEST_DATA_DIR / "invalid.xml" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-xml", + files={"file": ("invalid.xml", f, "application/xml")}, + data={"validation_type": "all"}, + ) + + # Verify the response structure and that validation failed + assert response.status_code == 200 + data = response.json() + assert "results" in data + assert any(not r.get("valid", True) for r in data["results"]) + + +def test_validate_xml_specific_type(client): + """Test validation using a specific validation type""" + test_file = TEST_DATA_DIR / "valid.xml" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-xml", + files={"file": ("valid.xml", f, "application/xml")}, + data={"validation_type": "submit"}, + ) + + # Verify the response structure + assert response.status_code == 200 + assert "results" in response.json() + + # Print the results for debugging + + +def test_validate_xml_invalid_type(client): + """Test validation with an invalid validation type""" + test_file = TEST_DATA_DIR / "valid.xml" + + with open(test_file, "rb") as f: + response = client.post( + "/validate-xml", + files={"file": ("valid.xml", f, "application/xml")}, + data={"validation_type": "invalid"}, + ) + + assert response.status_code == 400 + assert "detail" in response.json() + assert "Invalid validation type" in response.json()["detail"] + + +def test_validate_xml_non_xml_file(client): + """Test validation with a non-XML file""" + test_file = TEST_DATA_DIR / "valid.psv" # Using PSV file as non-XML + + with open(test_file, "rb") as f: + response = client.post( + "/validate-xml", + files={"file": ("file.txt", f, "text/plain")}, + data={"validation_type": "all"}, + ) + + assert response.status_code == 400 + assert "detail" in response.json() + assert "File must be an XML document" in response.json()["detail"] diff --git a/rafts/deploy.sh b/rafts/deploy.sh new file mode 100755 index 0000000..59361cd --- /dev/null +++ b/rafts/deploy.sh @@ -0,0 +1,455 @@ +#!/bin/bash +# ============================================================================= +# RAFTS Deployment Script +# ============================================================================= +# Usage: +# ./deploy.sh setup - Interactive setup wizard +# ./deploy.sh dev - Start development environment +# ./deploy.sh prod - Start production environment +# ./deploy.sh traefik - Deploy behind Traefik reverse proxy +# ./deploy.sh stop - Stop all services +# ./deploy.sh logs - View logs +# ./deploy.sh health - Check service health +# ./deploy.sh clean - Stop and remove all containers/images +# ============================================================================= + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_step() { echo -e "${CYAN}[STEP]${NC} $1"; } + +# Check prerequisites +check_prerequisites() { + log_info "Checking prerequisites..." + + if ! command -v docker &> /dev/null; then + log_error "Docker is not installed. Please install Docker first." + exit 1 + fi + + # Check for docker compose (v2) or docker-compose (v1) + if docker compose version &> /dev/null; then + DOCKER_COMPOSE="docker compose" + elif docker-compose version &> /dev/null; then + DOCKER_COMPOSE="docker-compose" + else + log_error "Docker Compose is not available. Please install Docker Compose." + exit 1 + fi + + if [ ! -f ".env" ]; then + log_warn ".env file not found. Creating from .env.example..." + if [ -f ".env.example" ]; then + cp .env.example .env + log_warn "Please edit .env and configure your settings, then run this script again." + exit 1 + else + log_error ".env.example not found. Cannot create configuration." + exit 1 + fi + fi + + log_success "Prerequisites check passed" +} + +# Start development environment +start_dev() { + log_info "Starting RAFTS development environment..." + check_prerequisites + + $DOCKER_COMPOSE --profile dev up --build "$@" +} + +# Start production environment +start_prod() { + log_info "Starting RAFTS production environment..." + check_prerequisites + + # Validate critical environment variables + source .env + if [ "$NEXTAUTH_SECRET" = "CHANGE_ME_generate_with_openssl_rand_base64_32" ]; then + log_error "NEXTAUTH_SECRET has not been configured. Please update .env" + exit 1 + fi + + $DOCKER_COMPOSE --profile prod up -d --build "$@" + + log_info "Waiting for services to be healthy..." + sleep 10 + + health_check +} + +# Stop all services +stop_services() { + log_info "Stopping RAFTS services..." + $DOCKER_COMPOSE --profile dev --profile prod down + log_success "Services stopped" +} + +# View logs +view_logs() { + $DOCKER_COMPOSE --profile dev --profile prod logs -f "$@" +} + +# Health check +health_check() { + log_info "Checking service health..." + + echo "" + echo "Container Status:" + echo "=================" + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(rafts|ades|NAMES)" || true + + echo "" + echo "Health Checks:" + echo "==============" + + # Check validator + if curl -sf http://localhost:8080/health-check > /dev/null 2>&1; then + log_success "Validator API: Healthy" + else + log_warn "Validator API: Not responding (may be internal only)" + fi + + # Check frontend (via nginx if prod, direct if dev) + if curl -sf http://localhost:3080/api/health > /dev/null 2>&1; then + log_success "Frontend: Healthy at http://localhost:3080" + elif curl -sf http://localhost/api/health > /dev/null 2>&1; then + log_success "Frontend (prod/nginx): Healthy at http://localhost" + elif curl -sf http://localhost:3000/api/health > /dev/null 2>&1; then + log_success "Frontend (dev): Healthy at http://localhost:3000" + else + log_warn "Frontend: Not responding yet (may still be starting)" + fi + + echo "" +} + +# Clean up everything +clean_all() { + log_warn "This will remove all RAFTS containers and images." + read -p "Are you sure? (y/N) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + log_info "Cleaning up..." + $DOCKER_COMPOSE --profile dev --profile prod --profile ssl down -v --rmi local 2>/dev/null || true + $DOCKER_COMPOSE -f docker-compose.traefik.yml down -v --rmi local 2>/dev/null || true + log_success "Cleanup complete" + else + log_info "Cleanup cancelled" + fi +} + +# Interactive setup wizard +setup_wizard() { + echo "" + echo -e "${BOLD}╔════════════════════════════════════════════════════════════╗${NC}" + echo -e "${BOLD}║ RAFTS Deployment Setup Wizard ║${NC}" + echo -e "${BOLD}╚════════════════════════════════════════════════════════════╝${NC}" + echo "" + + # Check if .env already exists + if [ -f ".env" ]; then + log_warn ".env file already exists." + read -p "Overwrite with fresh configuration? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_info "Setup cancelled. Using existing .env" + return 0 + fi + fi + + # Copy template + if [ ! -f ".env.example" ]; then + log_error ".env.example not found. Cannot proceed with setup." + exit 1 + fi + cp .env.example .env + + echo "" + log_step "Step 1/4: Deployment Type" + echo "" + echo " 1) Development (local testing with hot reload)" + echo " 2) Production with Nginx (standalone server)" + echo " 3) Production with Traefik (behind existing reverse proxy)" + echo "" + read -p "Select deployment type [1-3]: " deploy_type + + case "$deploy_type" in + 1) + DEPLOY_MODE="dev" + log_info "Development mode selected" + ;; + 2) + DEPLOY_MODE="prod" + log_info "Production with Nginx selected" + ;; + 3) + DEPLOY_MODE="traefik" + log_info "Traefik deployment selected" + ;; + *) + log_warn "Invalid selection, defaulting to development" + DEPLOY_MODE="dev" + ;; + esac + + echo "" + log_step "Step 2/4: Domain Configuration" + echo "" + + if [ "$DEPLOY_MODE" = "dev" ]; then + DOMAIN="localhost" + BASE_PATH="" + NEXTAUTH_URL="http://localhost:3000" + else + read -p "Enter your domain (e.g., rafts.example.com): " DOMAIN + DOMAIN=${DOMAIN:-rafts.localhost} + + echo "" + echo "Deployment path options:" + echo " 1) Root domain (https://${DOMAIN}/)" + echo " 2) Subpath (https://${DOMAIN}/rafts)" + echo "" + read -p "Select path option [1-2]: " path_option + + if [ "$path_option" = "2" ]; then + read -p "Enter subpath (e.g., /rafts): " BASE_PATH + BASE_PATH=${BASE_PATH:-/rafts} + NEXTAUTH_URL="https://${DOMAIN}${BASE_PATH}" + else + BASE_PATH="" + NEXTAUTH_URL="https://${DOMAIN}" + fi + fi + + echo "" + log_step "Step 3/4: Security Configuration" + echo "" + + # Generate secret + NEXTAUTH_SECRET=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64) + log_success "Generated secure NEXTAUTH_SECRET" + + echo "" + log_step "Step 4/4: Traefik Configuration (if applicable)" + + if [ "$DEPLOY_MODE" = "traefik" ]; then + echo "" + read -p "Traefik network name [traefik_proxy]: " TRAEFIK_NETWORK + TRAEFIK_NETWORK=${TRAEFIK_NETWORK:-traefik_proxy} + + read -p "Traefik entrypoint [websecure]: " TRAEFIK_ENTRYPOINT + TRAEFIK_ENTRYPOINT=${TRAEFIK_ENTRYPOINT:-websecure} + + read -p "Traefik cert resolver [letsencrypt]: " TRAEFIK_CERTRESOLVER + TRAEFIK_CERTRESOLVER=${TRAEFIK_CERTRESOLVER:-letsencrypt} + + # Verify Traefik network exists + if ! docker network ls | grep -q "$TRAEFIK_NETWORK"; then + log_warn "Network '$TRAEFIK_NETWORK' not found. Make sure it exists before deploying." + fi + else + TRAEFIK_NETWORK="traefik_proxy" + TRAEFIK_ENTRYPOINT="websecure" + TRAEFIK_CERTRESOLVER="letsencrypt" + fi + + # Update .env file + log_info "Writing configuration to .env..." + + sed -i.bak "s|^RAFTS_DOMAIN=.*|RAFTS_DOMAIN=${DOMAIN}|" .env + sed -i.bak "s|^RAFTS_BASE_PATH=.*|RAFTS_BASE_PATH=${BASE_PATH}|" .env + sed -i.bak "s|^NEXTAUTH_URL=.*|NEXTAUTH_URL=${NEXTAUTH_URL}|" .env + sed -i.bak "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=${NEXTAUTH_SECRET}|" .env + sed -i.bak "s|^NEXT_PUBLIC_BASE_PATH=.*|NEXT_PUBLIC_BASE_PATH=${BASE_PATH}|" .env + + if [ "$DEPLOY_MODE" = "traefik" ]; then + sed -i.bak "s|^# TRAEFIK_NETWORK=.*|TRAEFIK_NETWORK=${TRAEFIK_NETWORK}|" .env + sed -i.bak "s|^# TRAEFIK_ENTRYPOINT=.*|TRAEFIK_ENTRYPOINT=${TRAEFIK_ENTRYPOINT}|" .env + sed -i.bak "s|^# TRAEFIK_CERTRESOLVER=.*|TRAEFIK_CERTRESOLVER=${TRAEFIK_CERTRESOLVER}|" .env + fi + + # Clean up backup files + rm -f .env.bak + + echo "" + echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Setup Complete! ║${NC}" + echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}" + echo "" + echo "Configuration saved to .env" + echo "" + echo "Next steps:" + echo " 1. Review and adjust .env if needed (especially CADC settings)" + echo "" + + case "$DEPLOY_MODE" in + dev) + echo " 2. Run: ./deploy.sh dev" + echo " 3. Access: http://localhost:3000" + ;; + prod) + echo " 2. Run: ./deploy.sh prod" + echo " 3. Access: http://${DOMAIN}" + ;; + traefik) + echo " 2. Run: ./deploy.sh traefik" + echo " 3. Access: https://${DOMAIN}${BASE_PATH}" + ;; + esac + echo "" +} + +# Deploy with Traefik +start_traefik() { + log_info "Deploying RAFTS behind Traefik..." + check_prerequisites + + # Validate configuration + source .env + + if [ "$NEXTAUTH_SECRET" = "CHANGE_ME_generate_with_openssl_rand_base64_32" ]; then + log_error "NEXTAUTH_SECRET not configured. Run './deploy.sh setup' first." + exit 1 + fi + + TRAEFIK_NETWORK=${TRAEFIK_NETWORK:-traefik_proxy} + + # Check Traefik network + if ! docker network ls --format '{{.Name}}' | grep -q "^${TRAEFIK_NETWORK}$"; then + log_error "Traefik network '${TRAEFIK_NETWORK}' not found." + log_info "Create it with: docker network create ${TRAEFIK_NETWORK}" + log_info "Or update TRAEFIK_NETWORK in .env" + exit 1 + fi + + log_info "Using Traefik network: ${TRAEFIK_NETWORK}" + log_info "Domain: ${RAFTS_DOMAIN:-rafts.localhost}" + [ -n "${RAFTS_BASE_PATH}" ] && log_info "Base path: ${RAFTS_BASE_PATH}" + + # Build and deploy + $DOCKER_COMPOSE -f docker-compose.traefik.yml up -d --build "$@" + + log_info "Waiting for services to be healthy..." + sleep 15 + + # Health check + echo "" + echo "Container Status:" + echo "=================" + docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "(rafts|ades|NAMES)" || true + + echo "" + log_success "Deployment complete!" + echo "" + + if [ -n "${RAFTS_BASE_PATH}" ]; then + echo "Access your application at: https://${RAFTS_DOMAIN}${RAFTS_BASE_PATH}" + else + echo "Access your application at: https://${RAFTS_DOMAIN}" + fi + echo "" +} + +# Stop Traefik deployment +stop_traefik() { + log_info "Stopping Traefik deployment..." + $DOCKER_COMPOSE -f docker-compose.traefik.yml down + log_success "Traefik deployment stopped" +} + +# Show usage +show_usage() { + echo "" + echo -e "${BOLD}RAFTS Deployment Script${NC}" + echo "" + echo "Usage: $0 [options]" + echo "" + echo -e "${BOLD}Commands:${NC}" + echo " setup Interactive setup wizard (recommended for first-time setup)" + echo " dev Start development environment (with hot reload)" + echo " prod Start production environment (with nginx)" + echo " traefik Deploy behind existing Traefik reverse proxy" + echo " stop Stop all services" + echo " logs View logs (add service name to filter)" + echo " health Check service health status" + echo " clean Remove all containers and images" + echo "" + echo -e "${BOLD}Quick Start:${NC}" + echo " $0 setup # Run setup wizard (first time)" + echo " $0 traefik # Deploy with Traefik" + echo "" + echo -e "${BOLD}Examples:${NC}" + echo " $0 setup # Interactive configuration" + echo " $0 dev # Start dev environment" + echo " $0 dev -d # Start dev in detached mode" + echo " $0 prod # Start production with nginx" + echo " $0 traefik # Deploy behind Traefik" + echo " $0 logs rafts-frontend # View frontend logs only" + echo " $0 health # Check all services" + echo " $0 stop # Stop all running services" + echo "" + echo -e "${BOLD}For Traefik Deployment:${NC}" + echo " 1. Run: $0 setup # Select option 3 for Traefik" + echo " 2. Review .env file" + echo " 3. Run: $0 traefik" + echo "" +} + +# Main +case "${1:-}" in + setup) + setup_wizard + ;; + dev) + shift + start_dev "$@" + ;; + prod) + shift + start_prod "$@" + ;; + traefik) + shift + start_traefik "$@" + ;; + stop) + check_prerequisites # Sets DOCKER_COMPOSE variable + stop_services + # Also stop Traefik deployment if running + $DOCKER_COMPOSE -f docker-compose.traefik.yml down 2>/dev/null || true + ;; + logs) + check_prerequisites # Sets DOCKER_COMPOSE variable + shift + view_logs "$@" + ;; + health) + health_check + ;; + clean) + check_prerequisites # Sets DOCKER_COMPOSE variable + clean_all + ;; + *) + show_usage + exit 1 + ;; +esac diff --git a/rafts/docker-compose.traefik.yml b/rafts/docker-compose.traefik.yml new file mode 100644 index 0000000..d827ad7 --- /dev/null +++ b/rafts/docker-compose.traefik.yml @@ -0,0 +1,108 @@ +# RAFTS Deployment for Traefik Reverse Proxy +# ============================================ +# +# This configuration deploys RAFTS behind an existing Traefik instance. +# +# Usage: +# # For subdomain (rafts.example.com): +# RAFTS_DOMAIN=rafts.example.com docker compose -f docker-compose.traefik.yml up -d --build +# +# # For subpath (example.com/rafts): +# RAFTS_DOMAIN=example.com RAFTS_BASE_PATH=/rafts docker compose -f docker-compose.traefik.yml up -d --build +# +# Prerequisites: +# - Traefik running with network "traefik_proxy" (or adjust TRAEFIK_NETWORK) +# - Copy .env.example to .env and configure +# - For subpath: Set NEXT_PUBLIC_BASE_PATH in .env to match RAFTS_BASE_PATH +# +# IMPORTANT: For subpath deployment, you MUST rebuild the image when changing the path +# because NEXT_PUBLIC_BASE_PATH is baked into the Next.js build. + +services: + # ============================================================================= + # FRONTEND - Next.js Application + # ============================================================================= + rafts-frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + # CRITICAL: This is baked into the build - rebuild when changing! + NEXT_PUBLIC_BASE_PATH: ${RAFTS_BASE_PATH:-} + container_name: rafts-frontend-nextjs + env_file: + - .env + environment: + NODE_ENV: production + PORT: '8080' + NEXT_PUBLIC_BASE_PATH: ${RAFTS_BASE_PATH:-} + # Validator URLs (internal Docker networking) + # Using non-prefixed vars for server actions (runtime) + VALIDATOR_URL_XML: http://ades-validator-api:8080/validate-xml + VALIDATOR_URL_PSV: http://ades-validator-api:8080/validate-psv + VALIDATOR_URL_MPC: http://ades-validator-api:8080/validate-mpc + NEXTAUTH_URL_INTERNAL: http://rafts-frontend:8080 + labels: + - "traefik.enable=true" + # Subdomain routing (rafts.example.com) + - "traefik.http.routers.rafts-frontend.rule=Host(`${RAFTS_DOMAIN:-rafts.localhost}`)" + # Subpath routing (example.com/rafts) - uncomment and adjust if using subpath + # - "traefik.http.routers.rafts-frontend.rule=Host(`${RAFTS_DOMAIN}`) && PathPrefix(`${RAFTS_BASE_PATH:-/rafts}`)" + - "traefik.http.routers.rafts-frontend.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.rafts-frontend.tls=true" + - "traefik.http.routers.rafts-frontend.tls.certresolver=${TRAEFIK_CERTRESOLVER:-letsencrypt}" + - "traefik.http.services.rafts-frontend.loadbalancer.server.port=8080" + # Priority (higher = matches first, important if main app has catch-all) + - "traefik.http.routers.rafts-frontend.priority=100" + healthcheck: + test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:8080/api/health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + restart: unless-stopped + networks: + - traefik_proxy + - rafts-internal + depends_on: + ades-validator-api: + condition: service_healthy + deploy: + resources: + limits: + cpus: '1' + memory: 512M + + # ============================================================================= + # VALIDATOR API - ADES Validation Service + # ============================================================================= + ades-validator-api: + build: + context: ./api/validator + dockerfile: Dockerfile + container_name: ades-validator-api + labels: + - "traefik.enable=false" # Internal service only + healthcheck: + test: ['CMD', 'python', '-c', "import urllib.request; urllib.request.urlopen('http://localhost:8080/health-check')"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 10s + restart: unless-stopped + networks: + - rafts-internal + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + +networks: + # External Traefik network - must exist + traefik_proxy: + external: true + name: ${TRAEFIK_NETWORK:-traefik_proxy} + # Internal network for frontend <-> validator communication + rafts-internal: + driver: bridge diff --git a/rafts/docker-compose.yml b/rafts/docker-compose.yml new file mode 100644 index 0000000..4f95f39 --- /dev/null +++ b/rafts/docker-compose.yml @@ -0,0 +1,136 @@ +# RAFTS Full Stack Deployment +# This is the main deployment file for running RAFTS with all services +# +# Usage: +# Development: docker compose --profile dev up --build +# Production: docker compose --profile prod up -d --build +# With SSL: docker compose --profile prod --profile ssl up -d --build +# +# Prerequisites: +# - Copy .env.example to .env and configure +# - For SSL: Ensure domain DNS points to this server + +services: + # ============================================================================= + # FRONTEND - Next.js Application + # ============================================================================= + rafts-frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + - NEXT_PUBLIC_BASE_PATH=${NEXT_PUBLIC_BASE_PATH:-} + - NEXT_PUBLIC_TURNSTILE_SITE_KEY=${NEXT_PUBLIC_TURNSTILE_SITE_KEY:-} + container_name: rafts-frontend-nextjs + ports: + - '3080:8080' + env_file: + - .env + environment: + NODE_ENV: production + PORT: '8080' + # Internal service URLs (Docker networking) + # Using non-prefixed vars for server actions (runtime) + VALIDATOR_URL_XML: http://ades-validator-api:8080/validate-xml + VALIDATOR_URL_PSV: http://ades-validator-api:8080/validate-psv + VALIDATOR_URL_MPC: http://ades-validator-api:8080/validate-mpc + NEXTAUTH_URL_INTERNAL: http://rafts-frontend:8080 + healthcheck: + test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:8080/api/health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + restart: unless-stopped + networks: + - rafts-network + depends_on: + ades-validator-api: + condition: service_healthy + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + profiles: + - dev + - prod + + # ============================================================================= + # VALIDATOR API - ADES Validation Service + # ============================================================================= + ades-validator-api: + build: + context: ./api/validator + dockerfile: Dockerfile + container_name: ades-validator-api + ports: + - '8080:8080' + healthcheck: + test: ['CMD', 'python', '-c', "import urllib.request; urllib.request.urlopen('http://localhost:8080/health-check')"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 10s + restart: unless-stopped + networks: + - rafts-network + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.1' + memory: 128M + profiles: + - dev + - prod + + # ============================================================================= + # NGINX - Reverse Proxy (Production) + # ============================================================================= + nginx: + image: nginx:alpine + container_name: rafts-nginx + ports: + - '80:80' + - '443:443' + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./certbot/www:/var/www/certbot:ro + - ./certbot/conf:/etc/letsencrypt:ro + healthcheck: + test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://localhost/health'] + interval: 30s + timeout: 5s + retries: 3 + restart: unless-stopped + networks: + - rafts-network + depends_on: + rafts-frontend: + condition: service_healthy + profiles: + - prod + + # ============================================================================= + # CERTBOT - SSL Certificate Management (Optional) + # ============================================================================= + certbot: + image: certbot/certbot + container_name: rafts-certbot + volumes: + - ./certbot/www:/var/www/certbot + - ./certbot/conf:/etc/letsencrypt + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + profiles: + - ssl + +networks: + rafts-network: + driver: bridge diff --git a/rafts/docker-swarm-deployment/ENV_VARS_GUIDE.md b/rafts/docker-swarm-deployment/ENV_VARS_GUIDE.md new file mode 100644 index 0000000..3d18925 --- /dev/null +++ b/rafts/docker-swarm-deployment/ENV_VARS_GUIDE.md @@ -0,0 +1,170 @@ +# RAFTS Environment Variables Guide for Docker Swarm Deployment + +## Overview + +RAFTS frontend is a Next.js application with three categories of environment variables: +- **Build-time**: Baked into the JS bundle during `docker build` +- **Runtime**: Injected at container start via Swarm stack environment +- **Internal networking**: Pre-configured in the stack for service-to-service communication + +--- + +## A) Build-Time Variables + +These MUST be set during `docker build` — they get inlined into the client-side JS bundle. + +| Variable | Description | Default | Example | +|----------|-------------|---------|---------| +| `NEXT_PUBLIC_BASE_PATH` | Subpath prefix for reverse proxy deployment | `/rafts` | `/rafts` | +| `NEXT_PUBLIC_TURNSTILE_SITE_KEY` | Cloudflare Turnstile bot protection key | `` | `0x4AAAA...` | + +### How to set + +```bash +docker build \ + --build-arg NEXT_PUBLIC_BASE_PATH=/rafts \ + --build-arg NEXT_PUBLIC_TURNSTILE_SITE_KEY=your-key \ + -t bucket.canfar.net/rafts-frontend:latest \ + ./frontend +``` + +**IMPORTANT**: `NEXT_PUBLIC_BASE_PATH` is baked into the Next.js bundle. Changing it requires a full image rebuild. `NEXTAUTH_URL` must also include the subpath (e.g., `https://rc-www.canfar.net/rafts`). + +--- + +## B) Runtime Variables + +Set these in the Swarm stack environment or via shell exports before `docker stack deploy`. + +### Authentication & Session + +| Variable | Required | Description | Production Value | +|----------|----------|-------------|-----------------| +| `NEXTAUTH_URL` | YES | Public URL of the application (must include subpath) | `https://rc-www.canfar.net/rafts` | +| `NEXTAUTH_SECRET` | YES | Session encryption key (generate with `openssl rand -base64 32`) | *unique per environment* | +| `NEXTAUTH_DEBUG` | no | Enable debug logging | `false` | + +### DOI Service + +| Variable | Required | Description | Production Value | +|----------|----------|-------------|-----------------| +| `NEXT_DOI_BASE_URL` | YES | DOI backend endpoint | `https://rc-ws-cadc.canfar.net/rdoi/instances` | + +### CADC Access Control (AC) Service + +| Variable | Required | Description | Production Value | +|----------|----------|-------------|-----------------| +| `NEXT_CANFAR_AC_LOGIN_URL` | YES | CADC login endpoint | `https://ws-cadc.canfar.net/ac/login` | +| `NEXT_CANFAR_AC_SEARCH_URL` | YES | CADC user search endpoint | `https://ws-cadc.canfar.net/ac/search` | +| `NEXT_CANFAR_AC_WHOAMI_URL` | YES | CADC identity endpoint | `https://ws-cadc.canfar.net/ac/whoami` | +| `NEXT_CANFAR_AC_GROUPS_URL` | YES | CADC groups endpoint | `https://ws-cadc.canfar.net/ac/groups` | +| `NEXT_CANFAR_RAFT_GROUP_NAME` | YES | Reviewer group name in AC | `RAFTS-reviewers` | + +### Storage (CANFAR Vault/VOSpace) + +| Variable | Required | Description | Production Value | +|----------|----------|-------------|-----------------| +| `NEXT_CANFAR_STORAGE_BASE_URL` | YES | Vault file storage URL | `https://ws-cadc.canfar.net/vault/files` | +| `NEXT_VAULT_BASE_ENDPOINT` | YES | Vault files endpoint | `https://ws-cadc.canfar.net/vault/files` | +| `NEXT_CITE_URL` | YES | Storage path prefix for RAFT data | `DOItest/rafts` | + +### SSO Cookie Configuration + +| Variable | Required | Description | Production Value | +|----------|----------|-------------|-----------------| +| `NEXT_COOKIE_SSO_KEY` | YES | SSO cookie key name | `CADC_SSO` | +| `NEXT_CANFAR_COOKIE_DOMAIN` | YES | CANFAR cookie domain | `canfar.net` | +| `NEXT_CANFAR_COOKIE_URL` | YES | CANFAR SSO cookie URL | `https://www.canfar.net/access/sso?cookieValue=` | +| `NEXT_CADC_COOKIE_DOMAIN` | YES | CADC cookie domain | `cadc-ccda.hia-iha.nrc-cnrc.gc.ca` | +| `NEXT_CADC_COOKIE_URL` | YES | CADC SSO cookie URL | `https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/access/sso?cookieValue=` | + +### Application Settings + +| Variable | Required | Description | Production Value | +|----------|----------|-------------|-----------------| +| `UI_REVIEW_ENABLED` | no | Enable review workflow feature | `true` | + +--- + +## C) Internal Networking Variables (pre-configured in stack) + +These are already set in `rafts-stack.yml` and should NOT be changed unless service names change. + +| Variable | Value | Notes | +|----------|-------|-------| +| `NODE_ENV` | `production` | Fixed | +| `PORT` | `8080` | HAProxy expects 8080 | +| `NEXTAUTH_URL_INTERNAL` | `http://rafts-frontend:8080` | Internal NextAuth callback URL | +| `VALIDATOR_URL_XML` | `http://rafts-ades-validator:8080/validate-xml` | ADES XML validation | +| `VALIDATOR_URL_PSV` | `http://rafts-ades-validator:8080/validate-psv` | ADES PSV validation | +| `VALIDATOR_URL_MPC` | `http://rafts-ades-validator:8080/validate-mpc` | ADES MPC validation | + +--- + +## Deployment + +### Build images + +```bash +# Frontend +docker build -t bucket.canfar.net/rafts-frontend:latest ./frontend + +# Validator +docker build -t bucket.canfar.net/ades-validator-api:latest ./api/validator +``` + +### Deploy to Swarm + +Option 1 - Export variables then deploy: + +```bash +export NEXTAUTH_URL=https://rafts.canfar.net +export NEXTAUTH_SECRET=$(openssl rand -base64 32) +export NEXT_DOI_BASE_URL=https://ws-cadc.canfar.net/doi/instances +# ... set all required variables from section B above + +docker stack deploy -c rafts-stack.yml rafts +``` + +Option 2 - Use an env file with the deploy script: + +```bash +# Create .env with all variables from section B +# Then use the deploy script +./deploy.sh deploy +``` + +### Image override variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `RAFTS_FRONTEND_IMAGE` | Frontend Docker image | `bucket.canfar.net/rafts-frontend:latest` | +| `ADES_VALIDATOR_IMAGE` | Validator Docker image | `bucket.canfar.net/ades-validator-api:latest` | + +--- + +## Environment-Specific Values + +### RC (Release Candidate) + +``` +NEXTAUTH_URL=https://rc-www.canfar.net/rafts +NEXT_DOI_BASE_URL=https://rc-ws-cadc.canfar.net/rdoi/instances +NEXT_CITE_URL=DOItest/rafts +``` + +### Production + +``` +NEXTAUTH_URL=https://ws-cadc.canfar.net/rafts +NEXT_DOI_BASE_URL=https://ws-cadc.canfar.net/doi/instances +NEXT_CITE_URL=AstroDataCitationDOI/CISTI.CANFAR +``` + +### Local Development + +``` +NEXTAUTH_URL=http://localhost:3000 +NEXT_DOI_BASE_URL=http://localhost:8080/rafts/instances +NEXT_CITE_URL=rafts-test +``` diff --git a/rafts/docker-swarm-deployment/build_ades.md b/rafts/docker-swarm-deployment/build_ades.md new file mode 100644 index 0000000..0ef4411 --- /dev/null +++ b/rafts/docker-swarm-deployment/build_ades.md @@ -0,0 +1,19 @@ +# Build ADES Validator Docker Image + +## Build Command + +```bash +docker build -t bucket.canfar.net/ades-validator-api:latest ./api/validator +``` + +## Build Arguments + +None. The ADES validator has no build-time configuration. + +## Notes + +- Base image: `python:3.11-slim` +- Exposes port `8080` +- Runs as non-root user `validator` (uid 1001) +- Includes `iau-ades` package for ADES format validation +- Supports XML, PSV, and MPC validation formats diff --git a/rafts/docker-swarm-deployment/build_rafts.md b/rafts/docker-swarm-deployment/build_rafts.md new file mode 100644 index 0000000..cc9db4d --- /dev/null +++ b/rafts/docker-swarm-deployment/build_rafts.md @@ -0,0 +1,54 @@ +# Build RAFTS Frontend Docker Image + +## Build Command (with subpath /rafts) + +```bash +docker build --build-arg NEXT_PUBLIC_BASE_PATH=/rafts -t bucket.canfar.net/rafts-frontend:latest ./frontend +``` + +## Build Arguments + +| Argument | Required | Description | Default | +|----------|----------|-------------|---------| +| `NEXT_PUBLIC_BASE_PATH` | yes | Subpath prefix for deployment behind a reverse proxy. Set to `/rafts` for RC/production. | `/rafts` | +| `NEXT_PUBLIC_TURNSTILE_SITE_KEY` | no | Cloudflare Turnstile site key for bot protection. | `` (empty) | + +## Build with Subpath + +If deploying at a subpath (e.g., `https://example.com/rafts`): + +```bash +docker build \ + --build-arg NEXT_PUBLIC_BASE_PATH=/rafts \ + -t bucket.canfar.net/rafts-frontend:latest \ + ./frontend +``` + +**IMPORTANT**: `NEXT_PUBLIC_BASE_PATH` is baked into the Next.js bundle at build time. Changing it requires a full image rebuild. + +## Build with Turnstile + +```bash +docker build \ + --build-arg NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAA... \ + -t bucket.canfar.net/rafts-frontend:latest \ + ./frontend +``` + +## Build with All Arguments + +```bash +docker build \ + --build-arg NEXT_PUBLIC_BASE_PATH=/rafts \ + --build-arg NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAA... \ + -t bucket.canfar.net/rafts-frontend:latest \ + ./frontend +``` + +## Notes + +- Base image: `node:22-alpine` +- Exposes port `8080` +- Runs as non-root user `nextjs` (uid 1001) +- Uses `dumb-init` for proper signal handling +- Output mode: Next.js standalone (minimal production bundle) diff --git a/rafts/docker-swarm-deployment/deploy.sh b/rafts/docker-swarm-deployment/deploy.sh new file mode 100755 index 0000000..58bc2cb --- /dev/null +++ b/rafts/docker-swarm-deployment/deploy.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# ============================================================================= +# RAFTS Docker Swarm Deployment +# ============================================================================= +# Usage: +# ./deploy.sh deploy - Deploy or update the stack +# ./deploy.sh remove - Remove the stack +# ./deploy.sh status - Show service status +# ./deploy.sh logs - Tail service logs +# ============================================================================= + +set -e + +STACK_NAME="rafts" +STACK_FILE="$(dirname "$0")/rafts-stack.yml" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "[INFO] $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +deploy_stack() { + log_info "Deploying stack '${STACK_NAME}'..." + + if [ ! -f "$STACK_FILE" ]; then + log_error "Stack file not found: ${STACK_FILE}" + exit 1 + fi + + docker stack deploy -c "$STACK_FILE" "$STACK_NAME" + log_success "Stack '${STACK_NAME}' deployed" + + log_info "Waiting for services to converge..." + sleep 10 + show_status +} + +remove_stack() { + log_warn "Removing stack '${STACK_NAME}'..." + docker stack rm "$STACK_NAME" + log_success "Stack '${STACK_NAME}' removed" +} + +show_status() { + echo "" + echo "Services:" + echo "=========" + docker stack services "$STACK_NAME" 2>/dev/null || log_warn "Stack not running" + + echo "" + echo "Tasks:" + echo "======" + docker stack ps "$STACK_NAME" --no-trunc 2>/dev/null || true + echo "" +} + +show_logs() { + local service="${2:-}" + if [ -n "$service" ]; then + docker service logs -f "${STACK_NAME}_${service}" + else + log_info "Specify a service: rafts-frontend | rafts-ades-validator" + docker stack services "$STACK_NAME" 2>/dev/null + fi +} + +case "${1:-}" in + deploy) deploy_stack ;; + remove) remove_stack ;; + status) show_status ;; + logs) show_logs "$@" ;; + *) + echo "Usage: $0 {deploy|remove|status|logs [service]}" + echo "" + echo "Commands:" + echo " deploy - Deploy or update the RAFTS stack" + echo " remove - Remove the RAFTS stack" + echo " status - Show service status and tasks" + echo " logs - Tail logs (specify service name)" + echo "" + echo "Environment variables:" + echo " RAFTS_FRONTEND_IMAGE - Frontend image (default: bucket.canfar.net/rafts-frontend:latest)" + echo " ADES_VALIDATOR_IMAGE - Validator image (default: bucket.canfar.net/ades-validator-api:latest)" + exit 1 + ;; +esac diff --git a/rafts/docker-swarm-deployment/docker_run_ades.md b/rafts/docker-swarm-deployment/docker_run_ades.md new file mode 100644 index 0000000..fbdd5be --- /dev/null +++ b/rafts/docker-swarm-deployment/docker_run_ades.md @@ -0,0 +1,68 @@ +# Run ADES Validator Docker Container + +## Quick Start + +```bash +docker run --rm -p 8080:8080 \ + --name ades-validator \ + bucket.canfar.net/ades-validator-api:latest +``` + +## Runtime Environment Variables + +The ADES validator requires no environment variables. All configuration is built into the image. + +| Variable | Description | Default | Notes | +|----------|-------------|---------|-------| +| (none) | No runtime configuration needed | — | Service is stateless | + +## Run Examples + +### Production (with Swarm network) + +```bash +docker run --rm \ + --network rafts-network \ + --name ades-validator \ + bucket.canfar.net/ades-validator-api:latest +``` + +No port publishing needed in production — the frontend reaches the validator via the Docker network on port `8080`. + +### Local Development (standalone) + +```bash +docker run --rm -p 8085:8080 \ + --name ades-validator \ + ades-validator:local +``` + +### Local Development (shared network with frontend) + +```bash +# Create network +docker network create rafts-network + +# Run validator +docker run --rm -p 8085:8080 \ + --network rafts-network \ + --name ades-validator \ + ades-validator:local +``` + +The frontend can then reach the validator at `http://ades-validator:8080` via the shared network. + +## Health Check + +```bash +curl http://localhost:8085/health-check +``` + +## Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health-check` | GET | Health check | +| `/validate-xml` | POST | Validate ADES XML format | +| `/validate-psv` | POST | Validate ADES PSV format | +| `/validate-mpc` | POST | Validate ADES MPC format | diff --git a/rafts/docker-swarm-deployment/docker_run_rafts.md b/rafts/docker-swarm-deployment/docker_run_rafts.md new file mode 100644 index 0000000..4d60615 --- /dev/null +++ b/rafts/docker-swarm-deployment/docker_run_rafts.md @@ -0,0 +1,104 @@ +# Run RAFTS Frontend Docker Container + +## Quick Start + +```bash +docker run --rm -p 3080:8080 \ + --name rafts-frontend \ + bucket.canfar.net/rafts-frontend:latest +``` + +## Runtime Environment Variables + +### Required + +| Variable | Description | Example | +|----------|-------------|---------| +| `NEXTAUTH_URL` | Public URL where the app is accessible (must include subpath if built with one) | `https://rc-www.canfar.net/rafts` | +| `NEXTAUTH_SECRET` | Session encryption key. Generate: `openssl rand -base64 32` | `VPDZsUHEBl...` | +| `NEXT_DOI_BASE_URL` | DOI backend API endpoint | `https://rc-ws-cadc.canfar.net/rdoi/instances` | +| `NEXT_CANFAR_AC_LOGIN_URL` | CADC login endpoint | `https://ws-cadc.canfar.net/ac/login` | +| `NEXT_CANFAR_AC_SEARCH_URL` | CADC user search endpoint | `https://ws-cadc.canfar.net/ac/search` | +| `NEXT_CANFAR_AC_WHOAMI_URL` | CADC identity endpoint | `https://ws-cadc.canfar.net/ac/whoami` | +| `NEXT_CANFAR_AC_GROUPS_URL` | CADC groups endpoint | `https://ws-cadc.canfar.net/ac/groups` | +| `NEXT_CANFAR_RAFT_GROUP_NAME` | Reviewer group name in AC service | `RAFTS-reviewers` | +| `NEXT_CANFAR_STORAGE_BASE_URL` | Vault file storage URL | `https://ws-cadc.canfar.net/vault/files` | +| `NEXT_VAULT_BASE_ENDPOINT` | Vault files endpoint | `https://ws-cadc.canfar.net/vault/files` | +| `NEXT_CITE_URL` | Storage path prefix for RAFT data | `DOItest/rafts` | +| `NEXT_COOKIE_SSO_KEY` | SSO cookie key name | `CADC_SSO` | +| `NEXT_CANFAR_COOKIE_DOMAIN` | CANFAR cookie domain | `canfar.net` | +| `NEXT_CANFAR_COOKIE_URL` | CANFAR SSO cookie URL | `https://www.canfar.net/access/sso?cookieValue=` | +| `NEXT_CADC_COOKIE_DOMAIN` | CADC cookie domain | `cadc-ccda.hia-iha.nrc-cnrc.gc.ca` | +| `NEXT_CADC_COOKIE_URL` | CADC SSO cookie URL | `https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/access/sso?cookieValue=` | + +### Internal Networking (set by Swarm stack, override for standalone) + +| Variable | Description | Default in Swarm | +|----------|-------------|-----------------| +| `PORT` | Container listen port | `8080` | +| `NEXTAUTH_URL_INTERNAL` | Internal NextAuth callback URL | `http://rafts-frontend:8080` | +| `VALIDATOR_URL_XML` | ADES XML validation endpoint | `http://rafts-ades-validator:8080/validate-xml` | +| `VALIDATOR_URL_PSV` | ADES PSV validation endpoint | `http://rafts-ades-validator:8080/validate-psv` | +| `VALIDATOR_URL_MPC` | ADES MPC validation endpoint | `http://rafts-ades-validator:8080/validate-mpc` | + +### Optional + +| Variable | Description | Default | +|----------|-------------|---------| +| `NEXTAUTH_DEBUG` | Enable NextAuth debug logging | `false` | +| `UI_REVIEW_ENABLED` | Enable review workflow feature | `true` | +| `NODE_TLS_REJECT_UNAUTHORIZED` | Disable TLS verification (dev only!) | `1` | + +## Run Examples + +### Production (with Swarm network) + +```bash +docker run --rm -p 8080:8080 \ + --network rafts-network \ + -e NEXTAUTH_URL=https://rc-www.canfar.net/rafts \ + -e NEXTAUTH_SECRET=$(openssl rand -base64 32) \ + -e NEXT_DOI_BASE_URL=https://rc-ws-cadc.canfar.net/rdoi/instances \ + -e NEXT_CANFAR_AC_LOGIN_URL=https://ws-cadc.canfar.net/ac/login \ + -e NEXT_CANFAR_AC_SEARCH_URL=https://ws-cadc.canfar.net/ac/search \ + -e NEXT_CANFAR_AC_WHOAMI_URL=https://ws-cadc.canfar.net/ac/whoami \ + -e NEXT_CANFAR_AC_GROUPS_URL=https://ws-cadc.canfar.net/ac/groups \ + -e NEXT_CANFAR_RAFT_GROUP_NAME=RAFTS-reviewers \ + -e NEXT_CANFAR_STORAGE_BASE_URL=https://ws-cadc.canfar.net/vault/files \ + -e NEXT_VAULT_BASE_ENDPOINT=https://ws-cadc.canfar.net/vault/files \ + -e NEXT_CITE_URL=DOItest/rafts \ + -e NEXT_COOKIE_SSO_KEY=CADC_SSO \ + -e NEXT_CANFAR_COOKIE_DOMAIN=canfar.net \ + -e NEXT_CANFAR_COOKIE_URL=https://www.canfar.net/access/sso?cookieValue= \ + -e NEXT_CADC_COOKIE_DOMAIN=cadc-ccda.hia-iha.nrc-cnrc.gc.ca \ + -e NEXT_CADC_COOKIE_URL=https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/access/sso?cookieValue= \ + -e VALIDATOR_URL_XML=http://ades-validator:8080/validate-xml \ + -e VALIDATOR_URL_PSV=http://ades-validator:8080/validate-psv \ + -e VALIDATOR_URL_MPC=http://ades-validator:8080/validate-mpc \ + --name rafts-frontend \ + bucket.canfar.net/rafts-frontend:latest +``` + +### Local Development (with env file) + +```bash +docker run --rm -p 3080:8080 \ + --env-file ./frontend/.env.local \ + -e PORT=8080 \ + -e NODE_ENV=production \ + -e NEXTAUTH_SECRET=your-secret-here \ + -e VALIDATOR_URL_XML=http://host.docker.internal:8085/validate-xml \ + -e VALIDATOR_URL_PSV=http://host.docker.internal:8085/validate-psv \ + -e VALIDATOR_URL_MPC=http://host.docker.internal:8085/validate-mpc \ + -e NEXT_DOI_BASE_URL=http://host.docker.internal:8083/rafts/instances \ + --name rafts-frontend \ + rafts-frontend:local +``` + +**Note**: Use `host.docker.internal` to reach services running on the host machine from inside the container. + +## Health Check + +```bash +curl http://localhost:3080/api/health +``` diff --git a/rafts/docker-swarm-deployment/rafts-stack.yml b/rafts/docker-swarm-deployment/rafts-stack.yml new file mode 100644 index 0000000..5b2631b --- /dev/null +++ b/rafts/docker-swarm-deployment/rafts-stack.yml @@ -0,0 +1,130 @@ +version: '3.8' + +# RAFTS Docker Swarm Stack +# Deploy: docker stack deploy -c rafts-stack.yml rafts +# Remove: docker stack rm rafts +# +# IMPORTANT: The frontend image must be built with --build-arg NEXT_PUBLIC_BASE_PATH=/rafts +# for subpath deployment. NEXTAUTH_URL must also include the subpath (e.g., https://host/rafts) + +services: + # ============================================================================= + # rafts-frontend - Next.js Application + # ============================================================================= + rafts-frontend: + image: ${RAFTS_FRONTEND_IMAGE:-bucket.canfar.net/rafts-frontend:latest} + environment: + # Internal (fixed) + NODE_ENV: 'production' + PORT: '8080' + NEXTAUTH_URL_INTERNAL: 'http://rafts-frontend:8080' + VALIDATOR_URL_XML: 'http://rafts-ades-validator:8080/validate-xml' + VALIDATOR_URL_PSV: 'http://rafts-ades-validator:8080/validate-psv' + VALIDATOR_URL_MPC: 'http://rafts-ades-validator:8080/validate-mpc' + # Authentication (REQUIRED - set before deploy) + NEXTAUTH_URL: '${NEXTAUTH_URL}' + NEXTAUTH_SECRET: '${NEXTAUTH_SECRET}' + NEXTAUTH_DEBUG: '${NEXTAUTH_DEBUG:-false}' + # DOI Service + NEXT_DOI_BASE_URL: '${NEXT_DOI_BASE_URL:-https://rc-ws-cadc.canfar.net/rdoi/instances}' + # CADC Access Control + NEXT_CANFAR_AC_LOGIN_URL: '${NEXT_CANFAR_AC_LOGIN_URL:-https://ws-cadc.canfar.net/ac/login}' + NEXT_CANFAR_AC_SEARCH_URL: '${NEXT_CANFAR_AC_SEARCH_URL:-https://ws-cadc.canfar.net/ac/search}' + NEXT_CANFAR_AC_WHOAMI_URL: '${NEXT_CANFAR_AC_WHOAMI_URL:-https://ws-cadc.canfar.net/ac/whoami}' + NEXT_CANFAR_AC_GROUPS_URL: '${NEXT_CANFAR_AC_GROUPS_URL:-https://ws-cadc.canfar.net/ac/groups}' + NEXT_CANFAR_RAFT_GROUP_NAME: '${NEXT_CANFAR_RAFT_GROUP_NAME:-RAFTS-reviewers}' + # Storage (Vault) + NEXT_CANFAR_STORAGE_BASE_URL: '${NEXT_CANFAR_STORAGE_BASE_URL:-https://ws-cadc.canfar.net/vault/files}' + NEXT_VAULT_BASE_ENDPOINT: '${NEXT_VAULT_BASE_ENDPOINT:-https://ws-cadc.canfar.net/vault/files}' + NEXT_CITE_URL: '${NEXT_CITE_URL:-DOItest/rafts}' + # SSO Cookies + NEXT_COOKIE_SSO_KEY: '${NEXT_COOKIE_SSO_KEY:-CADC_SSO}' + NEXT_CANFAR_COOKIE_DOMAIN: '${NEXT_CANFAR_COOKIE_DOMAIN:-canfar.net}' + NEXT_CANFAR_COOKIE_URL: '${NEXT_CANFAR_COOKIE_URL:-https://www.canfar.net/access/sso?cookieValue=}' + NEXT_CADC_COOKIE_DOMAIN: '${NEXT_CADC_COOKIE_DOMAIN:-cadc-ccda.hia-iha.nrc-cnrc.gc.ca}' + NEXT_CADC_COOKIE_URL: '${NEXT_CADC_COOKIE_URL:-https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/access/sso?cookieValue=}' + # Features + UI_REVIEW_ENABLED: '${UI_REVIEW_ENABLED:-true}' + ports: + - target: 8080 + published: 8080 + protocol: tcp + mode: ingress + healthcheck: + test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/api/health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - rafts-network + deploy: + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + update_config: + parallelism: 1 + delay: 10s + failure_action: rollback + order: start-first + rollback_config: + parallelism: 1 + delay: 5s + order: start-first + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M + + # ============================================================================= + # rafts-ades-validator - ADES Validation Service + # ============================================================================= + rafts-ades-validator: + image: ${ADES_VALIDATOR_IMAGE:-bucket.canfar.net/ades-validator-api:latest} + healthcheck: + test: + [ + 'CMD', + 'python', + '-c', + "import urllib.request; urllib.request.urlopen('http://localhost:8080/health-check')", + ] + interval: 15s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - rafts-network + deploy: + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + update_config: + parallelism: 1 + delay: 10s + failure_action: rollback + order: start-first + rollback_config: + parallelism: 1 + delay: 5s + order: start-first + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + cpus: '0.1' + memory: 128M + +networks: + rafts-network: + driver: overlay diff --git a/rafts/frontend/.dockerignore b/rafts/frontend/.dockerignore new file mode 100644 index 0000000..a516450 --- /dev/null +++ b/rafts/frontend/.dockerignore @@ -0,0 +1,140 @@ +# ============================================================================= +# RAFTS Frontend - Docker Build Context Exclusions +# ============================================================================= +# Optimize Docker build context size and speed. +# These files are NOT needed for production builds. +# ============================================================================= + +# ============================================================================= +# VERSION CONTROL +# ============================================================================= +.git +.gitignore +.gitattributes + +# ============================================================================= +# DEPENDENCIES (Will be installed fresh in container) +# ============================================================================= +node_modules +.pnp +.pnp.js +.yarn/cache +.yarn/unplugged +.yarn/install-state.gz + +# ============================================================================= +# BUILD OUTPUT (Will be generated in container) +# ============================================================================= +.next +out +build +dist +coverage +.nyc_output + +# ============================================================================= +# ENVIRONMENT FILES +# ============================================================================= +# Don't include local env files - use Docker environment variables +.env +.env.local +.env.development +.env.development.local +.env.test +.env.test.local +.env.production.local + +# Keep .env.example for reference (optional, remove if not needed) +# !.env.example + +# ============================================================================= +# DEVELOPMENT & TESTING +# ============================================================================= +# Test files +**/*.test.ts +**/*.test.tsx +**/*.spec.ts +**/*.spec.tsx +**/__tests__ +**/__mocks__ +jest.config.* +vitest.config.* +playwright.config.* +cypress/ +cypress.config.* + +# Development tools +.husky +.eslintrc* +.eslintignore +.prettierrc* +.prettierignore +.editorconfig +tsconfig.tsbuildinfo + +# Storybook +.storybook +storybook-static + +# ============================================================================= +# IDE & EDITORS +# ============================================================================= +.idea +.vscode +*.sublime-project +*.sublime-workspace +*.swp +*.swo + +# ============================================================================= +# DOCUMENTATION +# ============================================================================= +*.md +!README.md +docs +doc_n_dev +CLAUDE.md +CHANGELOG* +LICENSE* + +# ============================================================================= +# CI/CD +# ============================================================================= +.gitlab-ci.yml +.github +.circleci +.travis.yml +azure-pipelines.yml +Jenkinsfile +bitbucket-pipelines.yml + +# ============================================================================= +# OS FILES +# ============================================================================= +.DS_Store +Thumbs.db +*.log + +# ============================================================================= +# DOCKER (Prevent recursive context) +# ============================================================================= +Dockerfile* +docker-compose* +.dockerignore + +# ============================================================================= +# SECURITY +# ============================================================================= +*.pem +*.key +*.crt +*.p12 + +# ============================================================================= +# MISCELLANEOUS +# ============================================================================= +tmp +temp +.cache +*.bak +*.backup diff --git a/rafts/frontend/.env.example b/rafts/frontend/.env.example new file mode 100644 index 0000000..ad34d8b --- /dev/null +++ b/rafts/frontend/.env.example @@ -0,0 +1,57 @@ +# NextAuth Configuration +NEXTAUTH_URL=https://your-production-domain.com +NEXTAUTH_SECRET=your-secret-key-here + +# API Configuration (if connecting to a backend) +# NEXT_PUBLIC_API_URL=https://your-api-domain.com/api + +# ============================================================================= +# DOI SERVICE CONFIGURATION +# Default: https://ws-cadc.canfar.net/doi/instances (CANFAR production) +# Local dev: https://haproxy.cadc.dao.nrc.ca/doi/instances +# ============================================================================= +# NEXT_DOI_BASE_URL=https://ws-cadc.canfar.net/doi/instances + +# ============================================================================= +# STORAGE SERVICE CONFIGURATION +# Default: CANFAR production vault +# Local dev: Local cavern service +# ============================================================================= +# NEXT_CANFAR_STORAGE_BASE_URL=https://www.canfar.net/storage/vault/file +# NEXT_VAULT_BASE_ENDPOINT=https://ws-cadc.canfar.net/vault/files + +# ============================================================================= +# CANFAR AC (Access Control) Integration +# Default: CANFAR production AC service +# Local dev: Local mock-ac service +# ============================================================================= +# NEXT_CANFAR_AC_LOGIN_URL=https://ws-cadc.canfar.net/ac/login +# NEXT_CANFAR_AC_SEARCH_URL=https://ws-cadc.canfar.net/ac/search +# NEXT_CANFAR_AC_WHOAMI_URL=https://ws-cadc.canfar.net/ac/whoami +# NEXT_CANFAR_AC_GROUPS_URL=https://ws-cadc.canfar.net/ac/groups +# NEXT_CANFAR_RAFT_GROUP_NAME=RAFTS-reviewers + +# ============================================================================= +# SSO Cookie Configuration +# ============================================================================= +# NEXT_COOKIE_SSO_KEY=CADC_SSO +# NEXT_CANFAR_COOKIE_DOMAIN=canfar.net +# NEXT_CANFAR_COOKIE_URL=https://www.canfar.net/access/sso?cookieValue= +# NEXT_CADC_COOKIE_DOMAIN=cadc-ccda.hia-iha.nrc-cnrc.gc.ca +# NEXT_CADC_COOKIE_URL=https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/access/sso?cookieValue= + +# Node Environment +NODE_ENV=production + +# ============================================================================= +# CLOUDFLARE TURNSTILE (Bot Protection) +# Get keys from: https://dash.cloudflare.com/?to=/:account/turnstile +# ============================================================================= +# NEXT_PUBLIC_TURNSTILE_SITE_KEY=your-site-key-here +# TURNSTILE_SECRET_KEY=your-secret-key-here + +# ============================================================================= +# LOCAL DEVELOPMENT NOTES +# ============================================================================= +# For local development with local DOI service, copy .env.local.dev to .env.local +# See LOCAL_DEVELOPMENT_SETUP.md in the doi project for service setup instructions \ No newline at end of file diff --git a/rafts/frontend/.gitignore b/rafts/frontend/.gitignore new file mode 100644 index 0000000..0054094 --- /dev/null +++ b/rafts/frontend/.gitignore @@ -0,0 +1,128 @@ +# ============================================================================= +# RAFTS Frontend - Next.js .gitignore +# ============================================================================= + +# ============================================================================= +# DEPENDENCIES +# ============================================================================= +node_modules/ +.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# ============================================================================= +# NEXT.JS BUILD OUTPUT +# ============================================================================= +.next/ +out/ +build/ +dist/ + +# Next.js standalone output +.next/standalone/ + +# ============================================================================= +# TESTING & COVERAGE +# ============================================================================= +coverage/ +.nyc_output/ +*.lcov +test-results/ +playwright-report/ +playwright/.cache/ + +# ============================================================================= +# ENVIRONMENT FILES +# ============================================================================= +# Local environment files contain secrets +.env +.env.* +!.env.example +!.env.template + +# ============================================================================= +# TYPESCRIPT +# ============================================================================= +*.tsbuildinfo +next-env.d.ts + +# ============================================================================= +# DEBUG & LOGS +# ============================================================================= +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +*.log + +# ============================================================================= +# IDE & EDITORS +# ============================================================================= +.idea/ +.vscode/ +*.sublime-project +*.sublime-workspace +*.swp +*.swo + +# ============================================================================= +# OS FILES +# ============================================================================= +.DS_Store +Thumbs.db + +# ============================================================================= +# SECURITY +# ============================================================================= +*.pem +*.key +*.crt + +# ============================================================================= +# VERCEL +# ============================================================================= +.vercel + +# ============================================================================= +# STORYBOOK (if used) +# ============================================================================= +storybook-static/ + +# ============================================================================= +# AI ASSISTANT CONFIGS & PROMPTING +# ============================================================================= +.claude/ +CLAUDE.md +.copilot/ +.cursor/ +.codex/ +.aider* + +# ============================================================================= +# DEVELOPMENT DOCUMENTATION (Internal) +# ============================================================================= +doc_n_dev/ + +# ============================================================================= +# MISCELLANEOUS +# ============================================================================= +# Temporary files +tmp/ +temp/ +.cache/ + +# ESLint cache +.eslintcache + +# Stylelint cache +.stylelintcache + +# Prettier cache +.prettiercache + +# Turbo +.turbo/ diff --git a/rafts/frontend/.husky/pre-commit b/rafts/frontend/.husky/pre-commit new file mode 100755 index 0000000..59d9b50 --- /dev/null +++ b/rafts/frontend/.husky/pre-commit @@ -0,0 +1,23 @@ +#!/bin/sh + +# Store the git root directory +GIT_ROOT=$(pwd) + +# Change to frontend directory where package.json and scripts are located +cd rafts/frontend || exit 0 + +# Ensure we have the right PATH for node and npm +export PATH="/Users/szautkin/.nvm/versions/node/v22.16.0/bin:$PATH" + +# Clear any problematic git config parameters +unset GIT_CONFIG_PARAMETERS + +# Run lint-staged on staged files +npx lint-staged + +# Update version info +npx tsx scripts/update-version.ts + +# Add updated version file (use path relative to git root) +cd "$GIT_ROOT" +git add rafts/frontend/src/version.json diff --git a/rafts/frontend/.prettierignore b/rafts/frontend/.prettierignore new file mode 100644 index 0000000..f8a2446 --- /dev/null +++ b/rafts/frontend/.prettierignore @@ -0,0 +1,6 @@ +node_modules +.next +out +dist +public +package-lock.json diff --git a/rafts/frontend/.prettierrc b/rafts/frontend/.prettierrc new file mode 100644 index 0000000..af99dbc --- /dev/null +++ b/rafts/frontend/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "arrowParens": "always" +} diff --git a/rafts/frontend/Dockerfile b/rafts/frontend/Dockerfile new file mode 100644 index 0000000..4452727 --- /dev/null +++ b/rafts/frontend/Dockerfile @@ -0,0 +1,66 @@ +# Build Stage +FROM node:22-alpine AS builder + +WORKDIR /app + +# Build arguments for subpath deployment +# Set NEXT_PUBLIC_BASE_PATH to deploy on a subpath (e.g., /rafts) +ARG NEXT_PUBLIC_BASE_PATH="" + +# Cloudflare Turnstile site key (for bot protection) +ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY="" + +# Copy package files +COPY package*.json ./ + +# Temporarily remove the prepare script to prevent husky errors during install +RUN npm pkg delete scripts.prepare && \ + npm ci --ignore-scripts + +# Copy source code +COPY . . + +# Build the Next.js application +# IMPORTANT: NEXT_PUBLIC_BASE_PATH must be set at BUILD time for subpath routing +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_ENV=production +ENV NEXTAUTH_DEBUG=true +ENV NEXT_PUBLIC_BASE_PATH=${NEXT_PUBLIC_BASE_PATH} +ENV NEXT_PUBLIC_TURNSTILE_SITE_KEY=${NEXT_PUBLIC_TURNSTILE_SITE_KEY} + +RUN npm run build + +# Production Stage +FROM node:22-alpine AS runner + +WORKDIR /app + +# Install dumb-init for proper signal handling +RUN apk add --no-cache dumb-init + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nextjs -u 1001 + +# Copy built application +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static + +# Set environment variables +ENV NODE_ENV=production +ENV NEXTAUTH_DEBUG=true +ENV UI_REVIEW_ENABLED=true +ENV PORT=8080 +ENV HOSTNAME="0.0.0.0" + +# Change ownership to non-root user +RUN chown -R nextjs:nodejs /app + +USER nextjs + +EXPOSE 8080 + +# Use dumb-init to handle signals properly +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "server.js"] \ No newline at end of file diff --git a/rafts/frontend/Dockerfile.can b/rafts/frontend/Dockerfile.can new file mode 100644 index 0000000..3c57815 --- /dev/null +++ b/rafts/frontend/Dockerfile.can @@ -0,0 +1,55 @@ +# Dockerfile.can +FROM node:22-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Temporarily remove the prepare script +RUN npm pkg delete scripts.prepare && npm install + +# Copy the source code +COPY . . + +# Build the frontend application +RUN npm run build + +# Production image +FROM node:22-alpine AS runner + +WORKDIR /app + +# Set environment to production +ENV NODE_ENV=production +ENV PORT=5000 +ENV NEXT_PUBLIC_BASE_PATH="/sessions/contrib" + +# Create the /skaha directory for CANFAR requirements +RUN mkdir -p /skaha + +# Copy necessary files for running the application +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/next.config.ts ./ +COPY --from=builder /app/next-env.d.ts ./ + +# Copy package.json but remove the prepare script +COPY --from=builder /app/package.json ./ +RUN npm pkg delete scripts.prepare + +# Install only runtime dependencies +RUN npm install --omit=dev + +# Create the initialization and startup scripts for CANFAR +COPY canfar-init.sh /skaha/init.sh +COPY canfar-startup.sh /skaha/startup.sh +RUN chmod +x /skaha/init.sh /skaha/startup.sh + +# Expose port 5000 as required by CANFAR +EXPOSE 5000 + +# Add a simple entry point for testing outside CANFAR +# This makes the container keep running when testing locally +ENTRYPOINT ["/bin/sh", "-c"] +CMD ["/skaha/init.sh"] \ No newline at end of file diff --git a/rafts/frontend/Dockerfile.dev b/rafts/frontend/Dockerfile.dev new file mode 100644 index 0000000..92678e3 --- /dev/null +++ b/rafts/frontend/Dockerfile.dev @@ -0,0 +1,17 @@ +# Dockerfile.dev +FROM node:22-alpine + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +RUN npm install + +# Copy source code +COPY . . + +# Expose port 3000 for Next.js +EXPOSE 3000 + +CMD ["npm", "run", "dev"] diff --git a/rafts/frontend/README.md b/rafts/frontend/README.md new file mode 100644 index 0000000..6282243 --- /dev/null +++ b/rafts/frontend/README.md @@ -0,0 +1,106 @@ +# RAFTS - Research Announcement for Transient Sources + +A submission and review system for astronomical transient observations, enabling researchers to submit, review, and publish RAFTs (Research Announcements for Transient Sources) with DOI integration. + +## Features + +- **RAFT Submission** - Multi-step form with validation and draft saving +- **Review System** - Workflow for reviewers to approve/reject submissions +- **DOI Integration** - DataCite DOI generation for published RAFTs +- **File Upload** - ADES file validation and storage via CANFAR VOSpace +- **Internationalization** - English/French language support +- **Role-Based Access** - Contributor, Reviewer, and Admin roles + +## Tech Stack + +- **Next.js 15** with App Router and Server Actions +- **TypeScript** with strict mode +- **Material-UI (MUI)** for components +- **NextAuth.js** for CADC authentication +- **next-intl** for i18n +- **Zod** for schema validation +- **React Hook Form** for form management +- **TanStack Table** for data tables + +## Quick Start + +### Prerequisites + +- Node.js >= 20.0.0 +- npm >= 10.0.0 + +### Development + +```bash +# Install dependencies +npm install + +# Start development server +npm run dev + +# Open http://localhost:3000 +``` + +### Available Scripts + +| Command | Description | +| ----------------------- | --------------------------------------- | +| `npm run dev` | Start development server with Turbopack | +| `npm run build` | Build for production | +| `npm run start` | Start production server | +| `npm run lint` | Run ESLint | +| `npm run format` | Format code with Prettier | +| `npm run typecheck` | Run TypeScript type checking | +| `npm run test` | Run tests with Vitest | +| `npm run test:coverage` | Run tests with coverage report | +| `npm run validate` | Run typecheck, lint, and tests | + +## Project Structure + +``` +src/ +├── app/[locale]/ # Next.js App Router pages with i18n +├── actions/ # Server actions for data operations +├── auth/ # CADC authentication (NextAuth.js) +├── components/ # React components organized by feature +├── context/ # React Context providers +├── services/ # External service integrations +├── shared/ # Shared types and constants +├── styles/ # MUI theming +└── utilities/ # Helper functions +``` + +## Documentation + +See [doc_n_dev/](./doc_n_dev/) for detailed documentation: + +- [Development Setup](./doc_n_dev/DEVELOPMENT.md) +- [Deployment Guide](./doc_n_dev/deployment/) +- [Technical Guides](./doc_n_dev/guides/) + +## Route Structure + +| Route | Description | Access | +| -------------------- | -------------------- | ------------- | +| `/` | Dashboard | Authenticated | +| `/form/create` | RAFT submission form | Contributor+ | +| `/form/edit/[id]` | Edit existing RAFT | Owner | +| `/view/rafts` | View published RAFTs | Authenticated | +| `/review/rafts` | Review system | Reviewer+ | +| `/admin` | Admin panel | Admin | +| `/public-view/rafts` | Public RAFT viewing | Public | + +## Environment Variables + +Key environment variables (see `.env.example` for full list): + +```env +NEXTAUTH_URL=https://your-domain.com +NEXTAUTH_SECRET=your-secret +NEXT_DOI_BASE_URL=https://doi-service/instances +NEXT_CANFAR_STORAGE_BASE_URL=https://storage-service/files +``` + +## License + +Copyright (c) National Research Council Canada diff --git a/rafts/frontend/canfar-init.sh b/rafts/frontend/canfar-init.sh new file mode 100644 index 0000000..1e12850 --- /dev/null +++ b/rafts/frontend/canfar-init.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# This initialization script runs at container startup + +# Set up environment variables needed for Next.js +export NEXT_PUBLIC_BASE_PATH="/sessions/contrib" +export NEXTAUTH_URL="https://ws-uv.canfar.net/sessions/contrib" + +# Log initialization +echo "CANFAR container initialization complete" + +# For testing outside of CANFAR, we need to keep the container running +# When run by CANFAR, this script exits and control goes to CANFAR's process +if [ "$RUNNING_IN_CANFAR" != "true" ]; then + # Start Next.js in the background and capture its PID + cd /app/packages/frontend + npm run start & + NEXT_PID=$! + + # Log the PID for debugging + echo "Next.js started with PID: $NEXT_PID" + + # Wait for the Next.js process + wait $NEXT_PID +else + # Just exit and let CANFAR take over + exit 0 +fi \ No newline at end of file diff --git a/rafts/frontend/canfar-startup.sh b/rafts/frontend/canfar-startup.sh new file mode 100644 index 0000000..5c8af80 --- /dev/null +++ b/rafts/frontend/canfar-startup.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# This script is called by CANFAR with the sessionid as a parameter + +# Store the session ID +SESSION_ID=$1 +echo "Starting with session ID: $SESSION_ID" + +# Set environment variable to indicate we're running in CANFAR +export RUNNING_IN_CANFAR="true" + +# Set any session-specific configuration +export NEXTAUTH_URL="https://ws-uv.canfar.net/sessions/contrib/${SESSION_ID}" + +# Change to the application directory +cd /app/packages/frontend + +# Modify Next.js to run on port 5000 +export PORT=5000 + +# Optionally log some diagnostic info +echo "Starting Next.js application in CANFAR environment" +echo "Node version: $(node -v)" +echo "NPM version: $(npm -v)" +echo "PORT: $PORT" +echo "NEXTAUTH_URL: $NEXTAUTH_URL" + +# Start the Next.js application in the foreground +exec npm run start \ No newline at end of file diff --git a/rafts/frontend/eslint.config.mjs b/rafts/frontend/eslint.config.mjs new file mode 100644 index 0000000..7d86c88 --- /dev/null +++ b/rafts/frontend/eslint.config.mjs @@ -0,0 +1,26 @@ +import { dirname } from 'path' +import { fileURLToPath } from 'url' +import { FlatCompat } from '@eslint/eslintrc' +import prettierPlugin from 'eslint-plugin-prettier' +import prettierConfig from 'eslint-config-prettier' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}) + +const eslintConfig = [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + { + name: 'prettier', + plugins: { prettier: prettierPlugin }, + rules: { + ...prettierConfig.rules, + 'prettier/prettier': 'error', + }, + }, +] + +export default eslintConfig diff --git a/rafts/frontend/messages/en.json b/rafts/frontend/messages/en.json new file mode 100644 index 0000000..380f691 --- /dev/null +++ b/rafts/frontend/messages/en.json @@ -0,0 +1,423 @@ +{ + "buttons": { + "getStarted": "Get Started", + "learnMore": "Learn More", + "submit": "Submit" + }, + "navigation": { + "home": "Home", + "about": "About", + "contact": "Contact" + }, + "landing_page": { + "create": "Create", + "view": "View", + "rapidPublications": "Rapid Publication", + "hero_title": "Research Announcements For The Solar System", + "hero_subtitle": "A publication system for short solar system science announcements in the era of large surveys like Rubin Observatory's LSST", + "hero_description": "RAFTS provide a means for publishing preliminary but meaningful analyses of solar system science discoveries, facilitating community follow-up observations and collaboration.", + "rapid_publication_desc": "Quickly issue short announcements with a transparent review process, nominally published within 1 day", + "solar_system_science": "Solar System Science", + "solar_system_science_desc": "Focused on solar system discoveries including comets, asteroids, unusual objects, and time-sensitive observations", + "community_access": "Community Access", + "community_access_desc": "Freely accessible to all users with citable DOIs and community discussion threads", + "what_to_do": "What would you like to do?", + "create_raft": "Create a RAFTS", + "create_raft_desc": "Submit a new research announcement", + "view_rafts": "View Your RAFTSs", + "view_rafts_desc": "Browse published announcements", + "review_rafts": "Review RAFTSs", + "review_rafts_desc": "Review submitted RAFTSs", + "browse_published": "Browse Published RAFTSs", + "browse_published_desc": "View all published research announcements", + "published_info": "Published RAFTSs are freely accessible via their DOI landing pages and can be cited using their assigned DOIs.", + "footer_text": "RAFTS is a collaborative project supported by CADC and the solar system science community" + }, + "submission_form": { + "title": "Title", + "title_helper": "Choose a clear, descriptive title that summarizes your observation or discovery", + "is_required": "Required", + "invalid_orcid": "Invalid ORCID iD format (expected: 0000-0000-0000-0000)", + "valid_email_required": "A valid email required", + "invalid_number": "Please enter a valid number", + "field_error": "This field has an error", + "author_info": "Author Information", + "author_info_helper": "There is no limit on the number of authors", + "cor_author": "Corresponding Author", + "author_ORCID": "Author's ORCID (e.g. 0000-0000-0000-0000)", + "con_authors": "Contributing Authors", + "first_name": "First Name", + "last_name": "Last Name", + "affiliation": "Affiliation", + "email": "Email", + "add_author": "Add Author", + "collaborations": "Collaborations", + "add_collaboration": "Add Collaboration", + "collaboration_name": "Collaboration Name", + "save": "Validate", + "remove": "Remove", + "optional": "Optional", + "observation_info": "Announcement Info", + "character_limit": "Abstract must be less than 2000 characters", + "topic": "Topic", + "object_name": "Object Name", + "abstract": "Abstract", + "enter_abstract": "Enter Abstract", + "figure": "Figure", + "figure_upload": "Observation Image", + "figure_upload_hint": "Click to open a dialog", + "acknowledgements": "Acknowledgements", + "acknowledgements_helper": "No character limit", + "previouslyPublishedRafts": "Previously Reported RAFTSs", + "previouslyPublishedRafts_helper": "Refer to previous RAFTSs by their DOI", + "enter_acknowledgements": "Enter Acknowledgements", + "comet": "Comet", + "near_earth_object": "Near Earth Object", + "trans_neptunian_object": "Trans-Neptunian Object", + "asteroid": "Asteroid", + "potentially_hazardous_asteroid": "Potentially Hazardous Asteroid", + "interstellar_object": "Interstellar Object", + "temporarily_captured_earth_orbiter": "Temporarily Captured Earth Orbiter", + "active_object": "Active Object", + "outburst": "Outburst", + "multi_component_system": "Multi-component system", + "unusual_rotation_properties": "Unusual rotation properties", + "unusual_colour_spectra": "Unusual colour or spectra", + "non_detection": "Non-detection", + "non_gravitational_perturbations": "Non-gravitational perturbations", + "trojans": "Trojans", + "centaurs": "Centaurs", + "satellites": "Satellites", + "errata": "Errata", + "retraction": "Retraction", + "other": "Other", + "technical_info": "Observation Information", + "ephemeris": "Ephemeris", + "orbital_elements": "Orbital Elements", + "mpc_id": "MPC Designation", + "alert_id": "Alert ID", + "alert_id_helper": "e.g. NEOCP designation, preliminary designation", + "mjd": "Date observed (MJD UTC)", + "mjd_helper": "Upload photometry file for multiple dates", + "invalid_mjd_format": "Invalid MJD format", + "telescope": "Telescope", + "instrument": "Instrument", + "enter_ephemeris": "Enter Ephemeris", + "enter_orbital_elements": "Enter Orbital Elements", + "enter_mpc_id": "Enter MPC Designation", + "enter_alert_id": "Enter Alert ID", + "enter_mjd": "Enter MJD", + "enter_telescope": "Enter Telescope Name", + "enter_instrument": "Enter Instrument Name", + "measurement_info": "Measurement Information", + "photometry": "Photometry", + "spectroscopy": "Spectroscopy", + "astrometry": "Astrometry", + "wavelength": "Wavelength", + "wavelength_helper": "Can be specified via filter reference (e.g. V, R, I, g', r', i')", + "brightness": "Brightness", + "brightness_helper": "Specify apparent, reduced, or absolute magnitude. For non-detections, indicate the limiting magnitude.", + "flux": "Flux", + "errors": "Uncertainty", + "position": "Position", + "time_observed": "Time Observed", + "enter_wavelength": "Enter wavelength", + "enter_brightness": "Enter brightness", + "enter_flux": "Enter flux value", + "enter_errors": "Enter uncertainty values", + "enter_position": "Enter position", + "enter_time": "Enter observation time", + "identifiers_helper": "MPC designation OR ephemeris OR orbital elements required", + "multiple_observations_helper": "Upload file in Miscellaneous section for multiple observations", + "miscellaneous_info": "Miscellaneous Information", + "misc_key": "Key", + "misc_value": "Value", + "misc_key_helper": "Enter a descriptive key for this information", + "misc_value_helper": "Enter the corresponding value", + "add_misc_item": "Add Additional Information", + "add_text_item": "Add Text", + "add_file_item": "Add File", + "misc_text_entry": "Text entry", + "misc_file_entry": "File entry", + "misc_file_label": "File Label", + "misc_file_label_helper": "Enter a descriptive label for this file", + "misc_upload_file": "Upload File", + "misc_upload_hint": "Select or drag & drop a file (max 5MB)", + "misc_file": "file", + "misc_files": "files", + "at_least_one_identifier_required": "At least one identifier required", + "raft_form_title": "Research Announcements For The Solar System (RAFTS)", + "create_new_raft": "Create New RAFTS", + "edit_raft": "Edit RAFTS", + "edit": "Edit", + "submitting": "Submitting...", + "saving": "Saving...", + "save_raft": "Save RAFTS", + "save_as_draft": "Save as draft", + "save_as_draft_helper": "You must save as a draft before uploading files", + "revert_to_draft": "Revert to draft", + "submit_raft": "Submit RAFTS", + "submit": "Submit", + "update": "Update", + "back": "Back", + "reset_form": "Reset Form", + "modal_reset_title": "Reset Form", + "confirm_reset": "Are you sure you want to reset the form? All your progress will be lost.", + "submission_success": "Your RAFTS has been submitted successfully!", + "submission_error": "There was an error submitting your RAFTS. Please try again.", + "validation_incomplete": "Please complete all required sections before submitting.", + "form_reset": "Form has been reset.", + "corresponding": "Corresponding", + "review_title": "Review Your RAFTS Submission", + "review_step": "Review", + "author_info_step": "Author Info", + "announcement_step": "Announcement", + "observation_step": "Observation", + "technical_info_step": "Technical", + "measurement_info_step": "Measurement", + "miscellaneous_step": "Miscellaneous", + "json_import_export": "JSON Import/Export", + "import_json": "Import RAFTS Data", + "export_json": "Export Current Data", + "import_json_hint": "Upload a RAFTS JSON file to import", + "export_json_hint": "Save your current form data as a JSON file", + "import_info_title": "RAFTS JSON Import/Export", + "import_info_description": "You can import a previously exported RAFTS JSON file to pre-fill the form, or export your current form data as JSON.", + "import_error": "Import Error", + "import_success": "Data imported successfully", + "confirm_import": "Confirm Data Import", + "confirm_import_message": "Importing will replace any existing data in your form. This action cannot be undone. Do you want to continue?", + "confirm_import_button": "Import Data", + "cancel": "Cancel", + "file_upload": "File Upload", + "file_size_limit": "Maximum file size: 5MB", + "spectrum_file": "Spectrum file", + "astrometry_file": "Astrometry file", + "ephemeris_upload_hint": "Present Ephemeris file as a txt file", + "orbital_elements_upload_hint": "Present Orbital Elements file as a txt file", + "spectrum_upload_hint": "Present Spectrum file as a txt file", + "astrometry_upload_hint": "Present Astrometry file as a txt with mpc extension", + "ades_upload_hint": "Present Astrometry file as an xml, psv, or mpc file complied with ADES", + "drop_file_here": "Drop your file here, or click to browse", + "file_too_large": "File is too large. Maximum size is", + "invalid_file_type": "Invalid file type. Please upload a JSON file.", + "processing_file": "Processing file...", + "modal_changes_title": "Unsaved changes", + "modal_changes_message": "You have changed this section. Stay and save this section or discard changes and proceed", + "modal_changes_cancel_caption": "Stay", + "modal_changes_ok_caption": "Discard and Navigate", + "modal_cancel_title": "Leave form?", + "modal_cancel_message": "You have unsaved changes. Are you sure you want to leave? Your changes will be lost.", + "modal_cancel_stay": "Stay", + "modal_cancel_leave": "Leave", + "author_form_message_one": "The workflow for submitting a RAFTS begins with the author entering all relevant information for a RAFTS in the form fields. The minimum required information is: author details, target name, topic type, abstract, an ephemeris or MPC Designation, and brightness information. Optional items are marked appropriately. Once the requisite information is entered into the form, the user saves the form data. The penultimate step is to review the submitted information. If the saved content looks complete, the RAFTS can be submitted.", + "author_form_message_two": "Submission of a RAFTS will trigger the review process. The review team will make its best effort to respond within 24 hours. The reviewer may approve a completed post, request edits, or reject it outright. Communications will be conducted via the provided email address.", + "announcement_form_message_one": "Please provide all information here that is helpful for the discussion and follow-up on the target of interest. The minimum requirement is brightness information, along with either an ephemeris or an MPC Object ID if the object is known. Extended data, such as spectroscopy and astrometry, should be uploaded as a CSV-formatted file matching the format provided in each section. To maximize the utility of the submission, please provide as much information as possible.", + "misc_form_message_one": "Please provide any additional helpful information about the target of interest. This can come in the form of key:value pairs to record useful information such as a Yarkovsky drift rate, an outburst magnitude, lightcurve period, etc. Additionally, data tables such as light curve tables or fits imagery can be provided here.", + "review_form_message_one": "Please carefully review the submission for accuracy and completeness.", + "help_tutorial": "Show Tutorial", + "opt_out_community_post": "Opt out of community forum post generation" + }, + "exim_form": { + "import_export_json": "Import/Export RAFTS Data", + "json_import_export": "Import/Export RAFTS Data", + "import_info_title": "RAFTS JSON Import/Export", + "import_info_description": "You can import a previously exported RAFTS JSON file to pre-fill the form, or export your current form data as JSON.", + "import": "Import", + "export": "Export", + "select_file_step": "Select File", + "confirm_step": "Confirm", + "complete_step": "Complete", + "select_file_to_import": "Select a RAFTS JSON file to import", + "import_json_description": "The file should be a valid RAFTS JSON export. Importing will replace your current form data.", + "select_file": "Select JSON File", + "drag_drop_hint": "Drag and drop a file here or click to browse", + "confirm_import": "Confirm Data Import", + "confirm_import_message": "Importing will replace any existing data in your form. This action cannot be undone.", + "file_preview": "File Preview", + "back": "Back", + "confirm_import_button": "Import Data", + "import_success": "Import Successful!", + "import_success_message": "Your RAFTS data has been successfully imported and applied to the form.", + "continue": "Continue", + "export_raft_data": "Export Your RAFTS Data", + "export_json_description": "Download your current form data as a JSON file. You can use this file later to import and restore your form.", + "no_data_to_export": "No form data available to export", + "fill_form_first": "Please fill in some form data before exporting", + "download_json_file": "Download JSON File", + "click_to_download": "Click to download your form data as a JSON file", + "close": "Close", + "cancel": "Cancel" + }, + "login": { + "sign_in_with_keycloak": "Sign in with Keycloak", + "sign_in": "Sign in", + "username": "User name", + "required": "Required", + "password": "Password", + "email": "User Email", + "signing_in": "Signing in", + "forgot_password": "Forgot password?" + }, + "profile": { + "name": "Name", + "email": "Email", + "affiliation": "Affiliation", + "user_id": "User ID", + "role": "Role", + "edit_profile": "Edit Profile", + "save": "Save", + "cancel": "Cancel", + "account_actions": "Account Actions", + "change_password": "Change Password", + "privacy_settings": "Privacy Settings", + "profile_updated": "Profile updated successfully", + "update_failed": "Failed to update profile", + "personal_info": "Personal Information", + "account_info": "Account Information", + "profile_picture": "Profile Picture", + "upload_new_picture": "Upload new picture", + "remove_picture": "Remove picture", + "last_login": "Last Login", + "member_since": "Member Since" + }, + "registration": { + "create_account": "Create an Account", + "first_name": "First Name", + "last_name": "Last Name", + "email": "Email", + "password": "Password", + "affiliation": "Affiliation (Optional)", + "affiliation_helper": "Your institution or organization (optional)", + "register": "Register", + "registering": "Registering...", + "already_have_account": "Already have an account?", + "sign_in": "Sign in", + "registration_success": "Registration successful! Please check your email to verify your account.", + "email_verification": "Email verification is required to access the system." + }, + "app_bar": { + "profile": "Profile", + "user": "User", + "sign_in": "Sign In", + "register": "Register", + "sign_out": "Sign Out", + "nav_home": "Home", + "nav_rafts": "My RAFTSs", + "nav_review": "Review" + }, + "language_selector": { + "change_language": "Change Language" + }, + "theme_toggle": { + "tooltip": "Change theme", + "light": "Light", + "dark": "Dark", + "system": "System" + }, + "raft_table": { + "draft": "Draft", + "in progress": "Draft", + "review_ready": "Review Ready", + "review ready": "Review Ready", + "in review": "In Review", + "under_review": "Under Review", + "approved": "Approved", + "rejected": "Rejected", + "published": "Published", + "minted": "Published", + "unknown": "Unknown", + "tooltip_published": "This RAFTS has been published and is publicly accessible", + "tooltip_approved": "This RAFTS has been approved and is ready for publishing", + "tooltip_under_review": "This RAFTS is under review by moderators", + "tooltip_rejected": "This RAFTS was rejected and needs revision", + "tooltip_review_ready": "This RAFTS is ready for review", + "tooltip_in_review": "This RAFTS has been submitted and is awaiting review", + "tooltip_draft": "This is a draft RAFTS that has not been submitted for review" + }, + "review_page": { + "filter_by_status": "Filter by status:", + "status_review_ready": "Ready for Review", + "status_under_review": "Under Review", + "status_approved": "Approved", + "status_rejected": "Rejected" + }, + "raft_details": { + "overview": "Overview", + "announcement": "Announcement", + "observation": "Observation", + "misc": "Miscellaneous" + }, + "password_reset": { + "request_reset_title": "Reset Password", + "request_reset_description": "Enter your email address and we'll send you a link to reset your password.", + "email": "Email", + "email_required": "Email is required", + "valid_email_required": "A valid email is required", + "submitting": "Sending...", + "request_reset_button": "Send Reset Link", + "back_to_login": "Back to Login", + "create_account": "Create an account" + }, + "tutorial": { + "step_title": "Welcome to the RAFTS submission form! This is where you'll enter the title of your astronomical observation or discovery. Make your title clear, descriptive, and specific to help others understand your work.", + "step_navigation": "This navigation bar shows your progress through the 5-step submission process. Green circles indicate completed sections, and you can click on any completed step to return to it. The connecting lines show your progression through the form.", + "step_author_info": "Step 1: Author Information - Enter details about the corresponding author (primary contact) and any contributing authors. Include names, affiliations, emails, and ORCID identifiers when available. All required fields must be completed.", + "step_announcement": "Step 2: Announcement - Provide the core information about your astronomical observation including the topic classification, object name/designation, a detailed abstract, and any supporting figures or images.", + "step_observation": "Step 3: Observation Details - Add technical information about your observation including telescope specifications, coordinates, photometric measurements, and upload supporting data files like spectra or astrometry.", + "step_miscellaneous": "Step 4: Miscellaneous - Include any additional information using key-value pairs for parameters like orbital periods, magnitudes, or drift rates. This section helps provide complete context for your observation.", + "step_review": "Step 5: Review - Final step to carefully review all your information before submitting. You can save as a draft to continue later, or submit for peer review when ready.", + "step_progress": "The progress indicator shows which sections are complete. Green circles indicate finished sections, blue shows your current location, and gray indicates incomplete sections.", + "step_save_as_draft": "Save as Draft - Use this button to save your work at any time. Your progress will be preserved and you can continue editing later. Note: This button is disabled until you enter minimal author information and a RAFTS title.", + "step_submit": "Submit - When all required sections are complete, use this button to submit your RAFTS for peer review. This button is only enabled when all mandatory fields are filled out.", + "button_back": "Back", + "button_close": "Close", + "button_last": "Finish Tutorial", + "button_next": "Next", + "button_skip": "Skip Tutorial", + "author_section_welcome": "Welcome to the Author Information section! Here you'll provide contact details about yourself as the corresponding author and any colleagues who contributed to this work. Accurate author information is essential for proper attribution and communication.", + "author_corresponding": "The corresponding author is the primary contact for this RAFTS submission. This should typically be you. Enter your full name, institutional email address, affiliation, and ORCID identifier if you have one. ORCID helps ensure proper attribution of your work.", + "author_contributing": "Contributing authors are colleagues who participated in the observation, analysis, or interpretation of the data. Add each author with their complete information including name, email, and institutional affiliation. ORCID IDs are highly recommended for all authors.", + "author_collaborations": "Collaborations are institutional or organizational groups that contributed to this work. Unlike individual authors, collaborations are listed by name only (e.g., 'NEOWISE Team', 'Catalina Sky Survey'). Add collaboration names that should be credited for this observation.", + "author_add_button": "Click this 'Add Author' button to include additional contributing authors to your submission. You can add as many authors as necessary and remove them if needed using the delete button next to each entry.", + "author_save": "Important: Click 'Validate' to store your author information before proceeding to the next section. Your progress will be lost if you navigate away without saving.", + "announcement_section_welcome": "Welcome to the Announcement section! This is the heart of your RAFTS submission where you'll describe your astronomical observation or discovery. Provide clear, comprehensive information to help the scientific community understand your findings.", + "announcement_topic": "Select the topic category that best describes your observation. Choose from options like 'Comet', 'Near Earth Object', 'Trans-Neptunian Object', or other celestial object types. This helps categorize your submission appropriately.", + "announcement_object": "Enter the official name, designation, or identifier of the astronomical object you observed. Use standard nomenclature when possible (e.g., 'C/2023 A1', '2023 AB', 'NGC 1234'). If it's a new discovery, provide your provisional designation.", + "announcement_abstract": "Write a comprehensive abstract describing your observation, methodology, key findings, and their scientific significance. Include important details like observation dates, instruments used, and notable characteristics. Maximum 2000 characters - make every word count!", + "announcement_figure": "Upload supporting images, charts, or figures that illustrate your observation. This could include photographs, light curves, spectra, or finder charts. Images help reviewers and readers better understand your work.", + "announcement_save": "Save your announcement information to lock in your progress and proceed to the technical details section. Review your abstract carefully before saving as this is a key part of your submission.", + "observation_section_welcome": "Welcome to the Observation Details section! Here you'll provide the technical specifications and measurements that support your astronomical observation. This data is crucial for scientific verification and follow-up studies.", + "observation_coordinates": "Provide precise coordinates and positional information for your observed object. Include MPC ID numbers for known objects, alert IDs for transient events, and Modified Julian Date (MJD) for time-sensitive observations.", + "observation_brightness": "Enter photometric measurements including wavelength/filter information, brightness values, and associated uncertainties. Use standard astronomical magnitude systems and specify your photometric bands (e.g., V, R, I, g', r', i').", + "observation_telescope": "Specify the telescope and instrumentation used for your observation. Include telescope aperture, focal ratio, detector type, and any relevant technical specifications that affect the quality and interpretation of your data.", + "observation_files": "Upload supporting data files that validate your observation. This can include ephemeris files, orbital elements, spectroscopic data, or astrometric measurements. Use standard formats when possible (ADES for astrometry, etc.).", + "observation_save": "Save your technical observation details to preserve your work. Ensure all critical measurements and data files are included before proceeding to the final section.", + "misc_section_welcome": "Welcome to the Miscellaneous Information section! This is where you can add any additional context, parameters, or details that don't fit in the previous sections but are important for understanding your observation.", + "misc_key_value": "Use the key-value pair system to record specific parameters relevant to your observation. Examples: 'Period' = '6.2 hours', 'Absolute Magnitude' = '15.3', 'Yarkovsky drift' = '0.01 au/Myr'. Each pair should contain meaningful scientific information.", + "misc_additional_files": "Click 'Add Additional Information' to include more key-value pairs for parameters like rotational periods, color indices, orbital elements, outburst magnitudes, or any other quantitative data relevant to your observation.", + "misc_save": "Save your miscellaneous information to complete this section. Review all your key-value pairs to ensure they're accurate and provide meaningful scientific context for your submission.", + "section_help": "Show Section Tutorial", + "welcome_message": "Welcome to RAFTS - Research Announcements For The Solar System! This tutorial will guide you through submitting your astronomical observation or discovery.", + "getting_started": "Let's get started with your RAFTS submission. Follow the step-by-step process to ensure your observation is properly documented and ready for scientific review.", + "need_help": "Need help? Click the help button (?) in any section to see specific guidance for that part of the form.", + "save_progress": "Your progress is automatically saved as you work. You can return to complete your submission at any time.", + "review_before_submit": "Before submitting, carefully review all sections to ensure accuracy and completeness. Once submitted for review, changes will require approval." + }, + "auth": { + "login_required_title": "Sign In Required", + "login_required_message": "You need to sign in to access this page. Please log in with your CADC credentials to continue.", + "sign_in": "Sign In", + "or_browse": "or", + "explore_without_login": "Explore public content without signing in:", + "go_home": "Go to Home Page", + "browse_public_rafts": "Browse Published RAFTSs", + "requested_page": "Requested page" + }, + "not_found": { + "title": "Page Not Found", + "description": "The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.", + "go_home": "Go to Home", + "go_back": "Go Back" + } +} diff --git a/rafts/frontend/messages/fr.json b/rafts/frontend/messages/fr.json new file mode 100644 index 0000000..18db670 --- /dev/null +++ b/rafts/frontend/messages/fr.json @@ -0,0 +1,432 @@ +{ + "buttons": { + "getStarted": "Commencer", + "learnMore": "En savoir plus", + "submit": "Soumettre" + }, + "navigation": { + "home": "Accueil", + "about": "À propos", + "contact": "Contact" + }, + "landing_page": { + "create": "Créer", + "view": "Visualiser", + "rapidPublications": "Publications rapides", + "hero_title": "Annonces de Recherche pour le Système Solaire", + "hero_subtitle": "Un système de publication pour de courtes annonces scientifiques sur le système solaire à l'ère des grands relevés comme le LSST de l'Observatoire Rubin", + "hero_description": "Les RAFTS offrent un moyen de publier des analyses préliminaires mais significatives des découvertes scientifiques du système solaire, facilitant les observations de suivi et la collaboration communautaire.", + "rapid_publication_desc": "Publiez rapidement de courtes annonces avec un processus de révision transparent, nominalement publiées dans les 24 heures", + "solar_system_science": "Science du Système Solaire", + "solar_system_science_desc": "Axé sur les découvertes du système solaire incluant comètes, astéroïdes, objets inhabituels et observations sensibles au temps", + "community_access": "Accès Communautaire", + "community_access_desc": "Librement accessible à tous les utilisateurs avec des DOI citables et des fils de discussion communautaires", + "what_to_do": "Que souhaitez-vous faire?", + "create_raft": "Créer un RAFTS", + "create_raft_desc": "Soumettre une nouvelle annonce de recherche", + "view_rafts": "Voir vos RAFTSs", + "view_rafts_desc": "Parcourir les annonces publiées", + "review_rafts": "Réviser les RAFTSs", + "review_rafts_desc": "Réviser les RAFTSs soumis", + "browse_published": "Parcourir les RAFTSs publiés", + "browse_published_desc": "Voir toutes les annonces de recherche publiées", + "published_info": "Les RAFTSs publiés sont librement accessibles via leurs pages d'atterrissage DOI et peuvent être cités à l'aide de leurs DOI attribués.", + "footer_text": "RAFTS est un projet collaboratif soutenu par CADC et la communauté scientifique du système solaire" + }, + "submission_form": { + "title": "Titre", + "title_helper": "Choisissez un titre clair et descriptif résumant votre observation ou découverte", + "is_required": "Requis", + "invalid_orcid": "Format ORCID iD invalide (attendu : 0000-0000-0000-0000)", + "valid_email_required": "Une adresse de courriel valide est requise", + "invalid_number": "Veuillez entrer un nombre valide", + "field_error": "Ce champ contient une erreur", + "author_info": "Informations sur l'auteur", + "author_info_helper": "Il n'y a pas de limite au nombre d'auteurs", + "cor_author": "Auteur correspondant", + "author_ORCID": "ORCID de l'auteur", + "con_authors": "Auteurs contributeurs", + "first_name": "Prénom", + "last_name": "Nom de famille", + "affiliation": "Affiliation", + "email": "Courriel", + "add_author": "Ajouter un auteur", + "collaborations": "Collaborations", + "add_collaboration": "Ajouter une collaboration", + "collaboration_name": "Nom de la collaboration", + "save": "Valider", + "remove": "Supprimer", + "optional": "Facultatif", + "observation_info": "Informations sur l'observation", + "character_limit": "Le résumé doit contenir moins de 2000 caractères", + "topic": "Sujet", + "object_name": "Nom de l'objet", + "abstract": "Résumé", + "enter_abstract": "Entrez le résumé", + "figure": "Figure", + "acknowledgements": "Remerciements", + "acknowledgements_helper": "Aucune limite de caractères", + "enter_acknowledgements": "Entrez les remerciements", + "previouslyPublishedRafts": "RAFTSs précédemment rapportés", + "previouslyPublishedRafts_helper": "Référez-vous aux RAFTSs précédents par leur DOI", + "comet": "Comète", + "near_earth_object": "Objet géocroiseur", + "trans_neptunian_object": "Objet transneptunien", + "asteroid": "Astéroïde", + "potentially_hazardous_asteroid": "Astéroïde potentiellement dangereux", + "interstellar_object": "Objet interstellaire", + "temporarily_captured_earth_orbiter": "Objet temporairement capturé par la Terre", + "active_object": "Objet actif", + "outburst": "Sursaut", + "multi_component_system": "Système à composantes multiples", + "unusual_rotation_properties": "Propriétés de rotation inhabituelles", + "unusual_colour_spectra": "Couleur ou spectre inhabituel", + "non_detection": "Non-détection", + "non_gravitational_perturbations": "Perturbations non gravitationnelles", + "trojans": "Troyens", + "centaurs": "Centaures", + "satellites": "Satellites", + "errata": "Errata", + "retraction": "Rétractation", + "other": "Autre", + "technical_info": "Informations techniques", + "ephemeris": "Éphémérides", + "orbital_elements": "Éléments orbitaux", + "mpc_id": "Identifiant MPC", + "alert_id": "Identifiant d'alerte", + "alert_id_helper": "p.ex. désignation NEOCP, désignation préliminaire", + "mjd": "Date Julienne Modifiée (MJD)", + "mjd_helper": "Téléversez un fichier photométrique pour plusieurs dates", + "invalid_mjd_format": "Format MJD invalide", + "telescope": "Télescope", + "instrument": "Instrument", + "enter_ephemeris": "Entrez les éphémérides", + "enter_orbital_elements": "Entrez les éléments orbitaux", + "enter_mpc_id": "Entrez l'identifiant MPC", + "enter_alert_id": "Entrez l'identifiant d'alerte", + "enter_mjd": "Entrez la MJD", + "enter_telescope": "Entrez le nom du télescope", + "enter_instrument": "Entrez le nom de l'instrument", + "measurement_info": "Informations sur les mesures", + "photometry": "Photométrie", + "spectroscopy": "Spectroscopie", + "astrometry": "Astrométrie", + "wavelength": "Longueur d'onde", + "wavelength_helper": "Peut être spécifié via une référence de filtre (p.ex. V, R, I, g', r', i')", + "brightness": "Luminosité", + "brightness_helper": "Spécifiez la magnitude apparente, réduite ou absolue. Pour les non-détections, indiquez la magnitude limite.", + "flux": "Flux", + "errors": "Incertitude", + "position": "Position", + "time_observed": "Heure d'observation", + "enter_wavelength": "Entrez la longueur d'onde", + "enter_brightness": "Entrez la luminosité", + "enter_flux": "Entrez la valeur du flux", + "enter_errors": "Entrez les valeurs d'incertitude", + "enter_position": "Entrez la position", + "enter_time": "Entrez l'heure d'observation", + "identifiers_helper": "Désignation MPC OU éphémérides OU éléments orbitaux requis", + "multiple_observations_helper": "Téléversez un fichier dans la section Divers pour plusieurs observations", + "miscellaneous_info": "Informations diverses", + "misc_key": "Clé", + "misc_value": "Valeur", + "misc_key_helper": "Entrez une clé descriptive pour cette information", + "misc_value_helper": "Entrez la valeur correspondante", + "add_misc_item": "Ajouter une information additionnelle", + "add_text_item": "Ajouter du texte", + "add_file_item": "Ajouter un fichier", + "misc_text_entry": "Entrée texte", + "misc_file_entry": "Entrée fichier", + "misc_file_label": "Étiquette du fichier", + "misc_file_label_helper": "Entrez une étiquette descriptive pour ce fichier", + "misc_upload_file": "Téléverser un fichier", + "misc_upload_hint": "Sélectionnez ou glissez-déposez un fichier (max 5 Mo)", + "misc_file": "fichier", + "misc_files": "fichiers", + "at_least_one_identifier_required": "Au moins un identifiant est requis", + "figure_upload": "Image d'observation", + "figure_upload_hint": "Cliquez pour ouvrir une fenêtre de dialogue", + "invalid_mjd_format": "Format MJD invalide", + "spectrum_file": "Fichier de spectre", + "astrometry_file": "Fichier d'astrométrie", + "ephemeris_upload_hint": "Présentez le fichier d'éphémérides en tant que fichier txt", + "orbital_elements_upload_hint": "Présentez le fichier d'éléments orbitaux en tant que fichier txt", + "spectrum_upload_hint": "Présentez le fichier de spectre en tant que fichier txt", + "astrometry_upload_hint": "Présentez le fichier d'astrométrie en tant que txt avec extension mpc", + "ades_upload_hint": "Présentez le fichier d'astrométrie en tant que fichier xml, psv ou mpc conforme à ADES", + "raft_form_title": "Annonces de Recherche pour le Système Solaire (RAFTS)", + "create_new_raft": "Créer un nouveau RAFTS", + "edit_raft": "Modifier le RAFTS", + "edit": "Modifier", + "submitting": "Soumission en cours...", + "saving": "Enregistrement...", + "save_raft": "Enregistrer le RAFTS", + "submit_raft": "Soumettre le RAFTS", + "submit": "Soumettre", + "update": "Mettre à jour", + "save_as_draft": "Sauvegarder comme brouillon", + "save_as_draft_helper": "Vous devez sauvegarder comme brouillon avant de téléverser des fichiers", + "revert_to_draft": "Rétablir en brouillon", + "back": "Retour", + "reset_form": "Réinitialiser le formulaire", + "modal_reset_title": "Réinitialiser le formulaire", + "confirm_reset": "Êtes-vous certain(e) de vouloir réinitialiser le formulaire? Toute votre progression sera perdue.", + "submission_success": "Votre RAFTS a été soumis avec succès!", + "submission_error": "Une erreur s'est produite lors de la soumission de votre RAFTS. Veuillez réessayer.", + "validation_incomplete": "Veuillez compléter toutes les sections requises avant de soumettre.", + "form_reset": "Le formulaire a été réinitialisé.", + "corresponding": "Correspondant", + "review_title": "Réviser votre soumission RAFTS", + "review_step": "Révision", + "author_info_step": "Infos Auteur", + "announcement_step": "Annonce", + "observation_step": "Observation", + "technical_info_step": "Technique", + "measurement_info_step": "Mesure", + "miscellaneous_step": "Divers", + "json_import_export": "Importation/Exportation JSON", + "import_json": "Importer les données RAFTS", + "export_json": "Exporter les données actuelles", + "import_json_hint": "Téléverser un fichier JSON RAFTS pour importer", + "export_json_hint": "Enregistrer les données actuelles du formulaire en tant que fichier JSON", + "import_info_title": "Importation/Exportation JSON RAFTS", + "import_info_description": "Vous pouvez importer un fichier JSON RAFTS préalablement exporté pour pré-remplir le formulaire, ou exporter les données actuelles de votre formulaire en JSON.", + "import_error": "Erreur d'importation", + "import_success": "Données importées avec succès", + "confirm_import": "Confirmer l'importation des données", + "confirm_import_message": "L'importation remplacera toutes les données existantes dans votre formulaire. Cette action est irréversible. Voulez-vous continuer?", + "confirm_import_button": "Importer les données", + "cancel": "Annuler", + "file_upload": "Téléversement de fichier", + "file_size_limit": "Taille maximale du fichier : 5 Mo", + "drop_file_here": "Glissez votre fichier ici, ou cliquez pour parcourir", + "file_too_large": "Le fichier est trop volumineux. La taille maximale est", + "invalid_file_type": "Type de fichier invalide. Veuillez téléverser un fichier JSON.", + "processing_file": "Traitement du fichier en cours...", + "modal_changes_title": "Modifications non sauvegardées", + "modal_changes_message": "Vous avez modifié cette section. Restez et sauvegardez cette section ou abandonnez les modifications et continuez", + "modal_changes_cancel_caption": "Rester", + "modal_changes_ok_caption": "Abandonner et naviguer", + "modal_cancel_title": "Quitter le formulaire?", + "modal_cancel_message": "Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir partir? Vos modifications seront perdues.", + "modal_cancel_stay": "Rester", + "modal_cancel_leave": "Quitter", + "author_form_message_one": "Le processus de soumission d'un RAFTS commence par l'auteur qui saisit toutes les informations pertinentes pour un RAFTS dans les champs du formulaire. Les informations minimales requises sont : les détails de l'auteur, le nom de la cible, le type de sujet, le résumé, des éphémérides ou une désignation MPC, et des informations de luminosité. Les éléments facultatifs sont marqués de manière appropriée. Une fois les informations requises saisies dans le formulaire, l'utilisateur sauvegarde les données du formulaire. L'avant-dernière étape consiste à réviser les informations soumises. Si le contenu sauvegardé semble complet, le RAFTS peut être soumis.", + "author_form_message_two": "La soumission d'un RAFTS déclenchera le processus de révision. L'équipe de révision fera de son mieux pour répondre dans les 24 heures. Le réviseur peut approuver une publication complète, demander des modifications ou la rejeter directement. Les communications seront effectuées via l'adresse courriel fournie.", + "announcement_form_message_one": "Veuillez fournir toutes les informations utiles pour la discussion et le suivi de la cible d'intérêt. L'exigence minimale est les informations de luminosité, ainsi que des éphémérides ou un identifiant MPC si l'objet est connu. Les données étendues, telles que la spectroscopie et l'astrométrie, doivent être téléversées sous forme de fichier au format CSV correspondant au format fourni dans chaque section. Pour maximiser l'utilité de la soumission, veuillez fournir autant d'informations que possible.", + "misc_form_message_one": "Veuillez fournir toute information supplémentaire utile sur la cible d'intérêt. Cela peut prendre la forme de paires clé:valeur pour enregistrer des informations utiles telles qu'un taux de dérive Yarkovsky, une magnitude d'éruption, une période de courbe de lumière, etc. De plus, des tableaux de données tels que des tableaux de courbes de lumière ou des images FITS peuvent être fournis ici.", + "review_form_message_one": "Veuillez réviser attentivement la soumission pour vérifier l'exactitude et la complétude.", + "help_tutorial": "Afficher le tutoriel", + "opt_out_community_post": "Refuser la génération d'un message sur le forum communautaire" + }, + "exim_form": { + "import_export_json": "Importer/Exporter les données RAFTS", + "json_import_export": "Importer/Exporter les données RAFTS", + "import_info_title": "Importation/Exportation JSON RAFTS", + "import_info_description": "Vous pouvez importer un fichier JSON RAFTS préalablement exporté pour pré-remplir le formulaire, ou exporter les données actuelles de votre formulaire en JSON.", + "import": "Importer", + "export": "Exporter", + "select_file_step": "Sélectionner le fichier", + "confirm_step": "Confirmer", + "complete_step": "Terminé", + "select_file_to_import": "Sélectionner un fichier JSON RAFTS à importer", + "import_json_description": "Le fichier doit être une exportation JSON RAFTS valide. L'importation remplacera les données actuelles de votre formulaire.", + "select_file": "Sélectionner le fichier JSON", + "drag_drop_hint": "Glissez-déposez un fichier ici ou cliquez pour parcourir", + "confirm_import": "Confirmer l'importation des données", + "confirm_import_message": "L'importation remplacera toutes les données existantes dans votre formulaire. Cette action est irréversible.", + "file_preview": "Aperçu du fichier", + "back": "Retour", + "confirm_import_button": "Importer les données", + "import_success": "Importation réussie!", + "import_success_message": "Vos données RAFTS ont été importées avec succès et appliquées au formulaire.", + "continue": "Continuer", + "export_raft_data": "Exporter vos données RAFTS", + "export_json_description": "Téléchargez les données actuelles de votre formulaire en tant que fichier JSON. Vous pourrez utiliser ce fichier ultérieurement pour importer et restaurer votre formulaire.", + "no_data_to_export": "Aucune donnée de formulaire disponible à exporter", + "fill_form_first": "Veuillez d'abord remplir des données dans le formulaire avant d'exporter", + "download_json_file": "Télécharger le fichier JSON", + "click_to_download": "Cliquez pour télécharger les données de votre formulaire en tant que fichier JSON", + "close": "Fermer", + "cancel": "Annuler", + "modal_changes_title": "Modifications non sauvegardées", + "modal_changes_message": "Vous avez modifié cette section. Restez et sauvegardez cette section ou abandonnez les modifications et continuez", + "modal_changes_cancel_caption": "Rester", + "modal_changes_ok_caption": "Abandonner et naviguer", + "modal_cancel_title": "Quitter le formulaire?", + "modal_cancel_message": "Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir partir? Vos modifications seront perdues.", + "modal_cancel_stay": "Rester", + "modal_cancel_leave": "Quitter" + }, + "login": { + "sign_in_with_keycloak": "Se connecter avec Keycloak", + "sign_in": "Se connecter", + "username": "Nom d'utilisateur", + "required": "Requis", + "password": "Mot de passe", + "email": "Courriel de l'utilisateur", + "signing_in": "Connexion en cours...", + "forgot_password": "Mot de passe oublié?" + }, + "profile": { + "name": "Nom", + "email": "Courriel", + "affiliation": "Affiliation", + "user_id": "Identifiant d'utilisateur", + "role": "Rôle", + "edit_profile": "Modifier le profil", + "save": "Enregistrer", + "cancel": "Annuler", + "account_actions": "Actions du compte", + "change_password": "Changer le mot de passe", + "privacy_settings": "Paramètres de confidentialité", + "profile_updated": "Profil mis à jour avec succès", + "update_failed": "Échec de la mise à jour du profil", + "personal_info": "Informations personnelles", + "account_info": "Informations du compte", + "profile_picture": "Photo de profil", + "upload_new_picture": "Téléverser une nouvelle photo", + "remove_picture": "Supprimer la photo", + "last_login": "Dernière connexion", + "member_since": "Membre depuis" + }, + "registration": { + "create_account": "Créer un compte", + "first_name": "Prénom", + "last_name": "Nom de famille", + "email": "Courriel", + "password": "Mot de passe", + "affiliation": "Affiliation (Facultatif)", + "affiliation_helper": "Votre institution ou organisation (facultatif)", + "register": "S'inscrire", + "registering": "Inscription en cours...", + "already_have_account": "Vous avez déjà un compte?", + "sign_in": "Se connecter", + "registration_success": "Inscription réussie! Veuillez vérifier votre courriel pour valider votre compte.", + "email_verification": "La vérification par courriel est requise pour accéder au système." + }, + "app_bar": { + "profile": "Profil", + "user": "Utilisateur", + "sign_in": "Connexion", + "register": "Inscription", + "sign_out": "Déconnexion", + "nav_home": "Accueil", + "nav_rafts": "Mes RAFTSs", + "nav_review": "Révision" + }, + "language_selector": { + "change_language": "Changer la langue" + }, + "theme_toggle": { + "tooltip": "Changer le thème", + "light": "Clair", + "dark": "Sombre", + "system": "Système" + }, + "raft_table": { + "draft": "Brouillon", + "in progress": "Brouillon", + "review_ready": "Prêt pour révision", + "review ready": "Prêt pour révision", + "in review": "En révision", + "under_review": "En cours de révision", + "approved": "Approuvé", + "rejected": "Rejeté", + "published": "Publié", + "minted": "Publié", + "unknown": "Inconnu", + "tooltip_published": "Ce RAFTS a été publié et est accessible publiquement", + "tooltip_approved": "Ce RAFTS a été approuvé et est prêt à être publié", + "tooltip_under_review": "Ce RAFTS est en cours de révision par les modérateurs", + "tooltip_rejected": "Ce RAFTS a été rejeté et nécessite une révision", + "tooltip_review_ready": "Ce RAFTS est prêt pour révision", + "tooltip_in_review": "Ce RAFTS a été soumis et attend la révision", + "tooltip_draft": "Ceci est un brouillon RAFTS qui n'a pas été soumis pour révision" + }, + "review_page": { + "filter_by_status": "Filtrer par statut :", + "status_review_ready": "Prêt pour révision", + "status_under_review": "En cours de révision", + "status_approved": "Approuvé", + "status_rejected": "Rejeté" + }, + "raft_details": { + "overview": "Aperçu", + "announcement": "Annonce", + "observation": "Observation", + "misc": "Divers" + }, + "password_reset": { + "request_reset_title": "Réinitialiser le mot de passe", + "request_reset_description": "Entrez votre adresse de courriel et nous vous enverrons un lien pour réinitialiser votre mot de passe.", + "email": "Courriel", + "email_required": "Le courriel est requis", + "valid_email_required": "Un courriel valide est requis", + "submitting": "Envoi en cours...", + "request_reset_button": "Envoyer le lien de réinitialisation", + "back_to_login": "Retour à la connexion", + "create_account": "Créer un compte" + }, + "tutorial": { + "step_title": "Bienvenue dans le formulaire de soumission RAFTS ! C'est ici que vous saisirez le titre de votre observation ou découverte astronomique. Rendez votre titre clair, descriptif et spécifique pour aider les autres à comprendre votre travail.", + "step_navigation": "Cette barre de navigation montre votre progression dans le processus de soumission en 5 étapes. Les cercles verts indiquent les sections complétées, et vous pouvez cliquer sur n'importe quelle étape terminée pour y retourner. Les lignes de connexion montrent votre progression dans le formulaire.", + "step_author_info": "Étape 1 : Informations sur l'auteur - Saisissez les détails sur l'auteur correspondant (contact principal) et tous les auteurs contributeurs. Incluez les noms, affiliations, courriels et identifiants ORCID lorsqu'ils sont disponibles. Tous les champs requis doivent être complétés.", + "step_announcement": "Étape 2 : Annonce - Fournissez les informations principales sur votre observation astronomique incluant la classification du sujet, le nom/désignation de l'objet, un résumé détaillé et toutes figures ou images de support.", + "step_observation": "Étape 3 : Détails de l'observation - Ajoutez les informations techniques sur votre observation incluant les spécifications du télescope, les coordonnées, les mesures photométriques et téléversez les fichiers de données de support comme les spectres ou l'astrométrie.", + "step_miscellaneous": "Étape 4 : Divers - Incluez toute information supplémentaire en utilisant des paires clé-valeur pour des paramètres comme les périodes orbitales, magnitudes ou taux de dérive. Cette section aide à fournir un contexte complet pour votre observation.", + "step_review": "Étape 5 : Révision - Dernière étape pour réviser soigneusement toutes vos informations avant la soumission. Vous pouvez sauvegarder comme brouillon pour continuer plus tard, ou soumettre pour révision par les pairs quand prêt.", + "step_progress": "L'indicateur de progression montre quelles sections sont complètes. Les cercles verts indiquent les sections terminées, le bleu montre votre emplacement actuel, et le gris indique les sections incomplètes.", + "step_save_as_draft": "Sauvegarder comme brouillon - Utilisez ce bouton pour sauvegarder votre travail à tout moment. Votre progression sera préservée et vous pourrez continuer l'édition plus tard. Note : Ce bouton est désactivé jusqu'à ce que vous entriez les informations minimales de l'auteur et un titre RAFTS.", + "step_submit": "Soumettre - Lorsque toutes les sections requises sont complètes, utilisez ce bouton pour soumettre votre RAFTS pour révision par les pairs. Ce bouton n'est activé que lorsque tous les champs obligatoires sont remplis.", + "button_back": "Retour", + "button_close": "Fermer", + "button_last": "Terminer le tutoriel", + "button_next": "Suivant", + "button_skip": "Ignorer le tutoriel", + "author_section_welcome": "Bienvenue dans la section Informations sur l'auteur ! Ici vous fournirez les coordonnées de vous-même en tant qu'auteur correspondant et de tous les collègues qui ont contribué à ce travail. Les informations d'auteur précises sont essentielles pour l'attribution appropriée et la communication.", + "author_corresponding": "L'auteur correspondant est le contact principal pour cette soumission RAFTS. Cela devrait typiquement être vous. Entrez votre nom complet, adresse courriel institutionnelle, affiliation et identifiant ORCID si vous en avez un. ORCID aide à assurer l'attribution appropriée de votre travail.", + "author_contributing": "Les auteurs contributeurs sont des collègues qui ont participé à l'observation, l'analyse ou l'interprétation des données. Ajoutez chaque auteur avec leurs informations complètes incluant nom, courriel et affiliation institutionnelle. Les identifiants ORCID sont fortement recommandés pour tous les auteurs.", + "author_collaborations": "Les collaborations sont des groupes institutionnels ou organisationnels qui ont contribué à ce travail. Contrairement aux auteurs individuels, les collaborations sont listées par nom seulement (ex. 'Équipe NEOWISE', 'Catalina Sky Survey'). Ajoutez les noms de collaboration qui devraient être crédités pour cette observation.", + "author_add_button": "Cliquez sur ce bouton 'Ajouter Auteur' pour inclure des auteurs contributeurs supplémentaires à votre soumission. Vous pouvez ajouter autant d'auteurs que nécessaire et les supprimer si besoin en utilisant le bouton supprimer à côté de chaque entrée.", + "author_save": "Important : Cliquez sur 'Valider' pour stocker vos informations d'auteur avant de procéder à la section suivante. Votre progression sera perdue si vous naviguez ailleurs sans sauvegarder.", + "announcement_section_welcome": "Bienvenue dans la section Annonce ! C'est le cœur de votre soumission RAFTS où vous décrirez votre observation ou découverte astronomique. Fournissez des informations claires et complètes pour aider la communauté scientifique à comprendre vos découvertes.", + "announcement_topic": "Sélectionnez la catégorie de sujet qui décrit le mieux votre observation. Choisissez parmi des options comme 'Comète', 'Objet géocroiseur', 'Objet transneptunien', ou autres types d'objets célestes. Cela aide à catégoriser votre soumission de manière appropriée.", + "announcement_object": "Entrez le nom officiel, la désignation ou l'identifiant de l'objet astronomique que vous avez observé. Utilisez la nomenclature standard quand possible (ex. 'C/2023 A1', '2023 AB', 'NGC 1234'). Si c'est une nouvelle découverte, fournissez votre désignation provisoire.", + "announcement_abstract": "Rédigez un résumé complet décrivant votre observation, méthodologie, découvertes clés et leur importance scientifique. Incluez des détails importants comme les dates d'observation, instruments utilisés et caractéristiques notables. Maximum 2000 caractères - faites que chaque mot compte !", + "announcement_figure": "Téléversez des images, graphiques ou figures de support qui illustrent votre observation. Cela pourrait inclure des photographies, courbes de lumière, spectres ou cartes de repérage. Les images aident les réviseurs et lecteurs à mieux comprendre votre travail.", + "announcement_save": "Sauvegardez vos informations d'annonce pour verrouiller votre progression et procéder à la section des détails techniques. Révisez votre résumé soigneusement avant de sauvegarder car c'est une partie clé de votre soumission.", + "observation_section_welcome": "Bienvenue dans la section Détails de l'observation ! Ici vous fournirez les spécifications techniques et mesures qui soutiennent votre observation astronomique. Ces données sont cruciales pour la vérification scientifique et les études de suivi.", + "observation_coordinates": "Fournissez les coordonnées précises et informations de position pour votre objet observé. Incluez les numéros MPC ID pour les objets connus, identifiants d'alerte pour les événements transitoires, et Date Julienne Modifiée (MJD) pour les observations sensibles au temps.", + "observation_brightness": "Entrez les mesures photométriques incluant les informations de longueur d'onde/filtre, valeurs de luminosité et incertitudes associées. Utilisez les systèmes de magnitude astronomiques standards et spécifiez vos bandes photométriques (ex. V, R, I, g', r', i').", + "observation_telescope": "Spécifiez le télescope et l'instrumentation utilisés pour votre observation. Incluez l'ouverture du télescope, rapport focal, type de détecteur et toutes spécifications techniques pertinentes qui affectent la qualité et l'interprétation de vos données.", + "observation_files": "Téléversez les fichiers de données de support qui valident votre observation. Cela peut inclure des fichiers d'éphémérides, éléments orbitaux, données spectroscopiques ou mesures astrométriques. Utilisez des formats standards quand possible (ADES pour l'astrométrie, etc.).", + "observation_save": "Sauvegardez vos détails techniques d'observation pour préserver votre travail. Assurez-vous que toutes les mesures critiques et fichiers de données sont inclus avant de procéder à la section finale.", + "misc_section_welcome": "Bienvenue dans la section Informations diverses ! C'est ici que vous pouvez ajouter tout contexte, paramètres ou détails supplémentaires qui ne s'intègrent pas dans les sections précédentes mais sont importants pour comprendre votre observation.", + "misc_key_value": "Utilisez le système de paires clé-valeur pour enregistrer des paramètres spécifiques pertinents à votre observation. Exemples : 'Période' = '6,2 heures', 'Magnitude absolue' = '15,3', 'Dérive Yarkovsky' = '0,01 au/Man'. Chaque paire devrait contenir des informations scientifiques significatives.", + "misc_additional_files": "Cliquez sur 'Ajouter Information Supplémentaire' pour inclure plus de paires clé-valeur pour des paramètres comme les périodes rotationnelles, indices de couleur, éléments orbitaux, magnitudes d'éruption, ou toute autre donnée quantitative pertinente à votre observation.", + "misc_save": "Sauvegardez vos informations diverses pour compléter cette section. Révisez toutes vos paires clé-valeur pour vous assurer qu'elles sont précises et fournissent un contexte scientifique significatif pour votre soumission.", + "section_help": "Afficher le tutoriel de section", + "welcome_message": "Bienvenue dans RAFTS - Annonces de Recherche pour le Système Solaire ! Ce tutoriel vous guidera dans la soumission de votre observation ou découverte astronomique.", + "getting_started": "Commençons avec votre soumission RAFTS. Suivez le processus étape par étape pour vous assurer que votre observation est correctement documentée et prête pour la révision scientifique.", + "need_help": "Besoin d'aide ? Cliquez sur le bouton d'aide (?) dans n'importe quelle section pour voir des conseils spécifiques pour cette partie du formulaire.", + "save_progress": "Votre progression est automatiquement sauvegardée pendant que vous travaillez. Vous pouvez revenir pour compléter votre soumission à tout moment.", + "review_before_submit": "Avant de soumettre, révisez soigneusement toutes les sections pour assurer l'exactitude et la complétude. Une fois soumis pour révision, les changements nécessiteront une approbation." + }, + "auth": { + "login_required_title": "Connexion Requise", + "login_required_message": "Vous devez vous connecter pour accéder à cette page. Veuillez vous connecter avec vos identifiants CADC pour continuer.", + "sign_in": "Se Connecter", + "or_browse": "ou", + "explore_without_login": "Explorez le contenu public sans vous connecter:", + "go_home": "Aller à la Page d'Accueil", + "browse_public_rafts": "Parcourir les RAFTSs Publiés", + "requested_page": "Page demandée" + }, + "not_found": { + "title": "Page Non Trouvée", + "description": "La page que vous recherchez a peut-être été supprimée, a changé de nom ou est temporairement indisponible.", + "go_home": "Aller à l'Accueil", + "go_back": "Retour" + } +} diff --git a/rafts/frontend/next.config.ts b/rafts/frontend/next.config.ts new file mode 100644 index 0000000..89691d9 --- /dev/null +++ b/rafts/frontend/next.config.ts @@ -0,0 +1,85 @@ +import createNextIntlPlugin from 'next-intl/plugin' +import path from 'path' +import type { NextConfig } from 'next' + +const withNextIntl = createNextIntlPlugin() + +const securityHeaders = [ + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'Strict-Transport-Security', + value: 'max-age=63072000; includeSubDomains; preload', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'X-Frame-Options', + value: 'DENY', + }, + { + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=()', + }, +] + +const nextConfig: NextConfig = { + // Add these for CANFAR compatibility + // Use the BASE_PATH from environment if available + basePath: process.env.NEXT_PUBLIC_BASE_PATH || '', + assetPrefix: process.env.NEXT_PUBLIC_BASE_PATH || '', + // Trust the proxy headers + poweredByHeader: false, + + // Enable standalone output for Docker deployment + output: 'standalone', + + // Disable linting during build for production + eslint: { + ignoreDuringBuilds: true, + }, + + // Webpack configuration for path aliases + webpack: (config) => { + config.resolve.alias['@'] = path.resolve(__dirname, 'src') + return config + }, + + // Original env variables + env: { + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, + NEXT_PUBLIC_BASE_PATH: process.env.NEXT_PUBLIC_BASE_PATH, + }, + + // Security headers + async headers() { + return [ + { + source: '/:path*', + headers: securityHeaders, + }, + ] + }, +} + +// next-intl 3.x sets experimental.turbo (deprecated in Next.js 15.5+) +// Move it to turbopack until next-intl is upgraded to v4 +const config = withNextIntl(nextConfig) as NextConfig +if (config.experimental?.turbo) { + config.turbopack = { ...config.turbopack, ...config.experimental.turbo } + delete config.experimental.turbo +} + +export default config diff --git a/rafts/frontend/package-lock.json b/rafts/frontend/package-lock.json new file mode 100644 index 0000000..f0366fc --- /dev/null +++ b/rafts/frontend/package-lock.json @@ -0,0 +1,11780 @@ +{ + "name": "@raft/frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@raft/frontend", + "version": "0.1.0", + "dependencies": { + "@emotion/cache": "^11.14.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@fontsource/roboto": "^5.1.1", + "@hookform/resolvers": "^4.1.0", + "@mui/icons-material": "^7.0.2", + "@mui/material": "^7.0.2", + "@mui/material-nextjs": "^7.0.2", + "@tanstack/react-table": "^8.21.2", + "dayjs": "^1.11.13", + "lodash": "^4.17.21", + "lucide-react": "^0.475.0", + "next": "^15.5.12", + "next-auth": "^5.0.0-beta.30", + "next-intl": "^3.26.4", + "next-themes": "^0.4.6", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-joyride": "^3.0.0-7", + "xml2js": "^0.6.2", + "zod": "^3.24.2" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", + "@types/lodash": "^4.17.13", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/xml2js": "^0.4.14", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/ui": "^4.0.18", + "eslint": "^9", + "eslint-config-next": "15.1.7", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.3", + "husky": "^9.1.7", + "jsdom": "^25.0.1", + "lint-staged": "^15.4.3", + "msw": "^2.7.0", + "postcss": "^8", + "prettier": "^3.5.1", + "tailwindcss": "^3.4.1", + "typescript": "^5", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=20.0.0", + "npm": ">=10.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@auth/core": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz", + "integrity": "sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fontsource/roboto": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.6.tgz", + "integrity": "sha512-hzarG7yAhMoP418smNgfY4fO7UmuUEm5JUtbxCoCcFHT0hOJB+d/qAEyoNjz7YkPU5OjM2LM8rJnW8hfm0JLaA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", + "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/intl-localematcher": "0.6.1", + "decimal.js": "^10.4.3", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", + "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", + "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", + "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/icu-skeleton-parser": "1.8.14", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", + "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", + "license": "MIT", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "tslib": "^2.8.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", + "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", + "license": "MIT", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@gilbarbara/deep-equal": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz", + "integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==", + "license": "MIT" + }, + "node_modules/@gilbarbara/types": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@gilbarbara/types/-/types-0.2.2.tgz", + "integrity": "sha512-QuQDBRRcm1Q8AbSac2W1YElurOhprj3Iko/o+P1fJxUWS4rOGKMVli98OXS7uo4z+cKAif6a+L9bcZFSyauQpQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^4.1.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", + "integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz", + "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.2.0.tgz", + "integrity": "sha512-d49s7kEgI5iX40xb2YPazANvo7Bx0BLg/MNRwv+7BVpZUzXj1DaVCKlQTDex3gy/0jsCb4w7AY2uH4t4AJvSog==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.2.0.tgz", + "integrity": "sha512-gRCspp3pfjHQyTmSOmYw7kUQTd9Udpdan4R8EnZvqPeoAtHnPzkvjBrBqzKaoAbbBp5bGF7BcD18zZJh4nwu0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.2.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.2.0.tgz", + "integrity": "sha512-NTuyFNen5Z2QY+I242MDZzXnFIVIR6ERxo7vntFi9K1wCgSwvIl0HcAO2OOydKqqKApE6omRiYhpny1ZhGuH7Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "@mui/core-downloads-tracker": "^7.2.0", + "@mui/system": "^7.2.0", + "@mui/types": "^7.4.4", + "@mui/utils": "^7.2.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.1.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.2.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material-nextjs": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/material-nextjs/-/material-nextjs-7.2.0.tgz", + "integrity": "sha512-/W2iKkjeOdaYBu5xNYi/w5HUX2C4HHefSMW7UgCvTKl90yy1puE7kmAgv/gxBghqhEE27cNWdevRrnvVhNRaUA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.4", + "@emotion/server": "^11.11.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "next": "^13.0.0 || ^14.0.0 || ^15.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/cache": { + "optional": true + }, + "@emotion/server": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.2.0.tgz", + "integrity": "sha512-y6N1Yt3T5RMxVFnCh6+zeSWBuQdNDm5/UlM0EAYZzZR/1u+XKJWYQmbpx4e+F+1EpkYi3Nk8KhPiQDi83M3zIw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "@mui/utils": "^7.2.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.2.0.tgz", + "integrity": "sha512-yq08xynbrNYcB1nBcW9Fn8/h/iniM3ewRguGJXPIAbHvxEF7Pz95kbEEOAAhwzxMX4okhzvHmk0DFuC5ayvgIQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.2.0.tgz", + "integrity": "sha512-PG7cm/WluU6RAs+gNND2R9vDwNh+ERWxPkqTaiXQJGIFAyJ+VxhyKfzpdZNk0z0XdmBxxi9KhFOpgxjehf/O0A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "@mui/private-theming": "^7.2.0", + "@mui/styled-engine": "^7.2.0", + "@mui/types": "^7.4.4", + "@mui/utils": "^7.2.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.4.tgz", + "integrity": "sha512-p63yhbX52MO/ajXC7hDHJA5yjzJekvWD3q4YDLl1rSg+OXLczMYPvTuSuviPRCgRX8+E42RXz1D/dz9SxPSlWg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.2.0.tgz", + "integrity": "sha512-O0i1GQL6MDzhKdy9iAu5Yr0Sz1wZjROH1o3aoztuivdCXqEeQYnEjTDiRLGuFxI9zrUbTHBwobMyQH5sNtyacw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "@mui/types": "^7.4.4", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz", + "integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.7.tgz", + "integrity": "sha512-kRP7RjSxfTO13NE317ek3mSGzoZlI33nc/i5hs1KaWpK+egs85xg0DJ4p32QEiHnR0mVjuUfhRIun7awqfL7pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz", + "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz", + "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz", + "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz", + "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz", + "integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz", + "integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz", + "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz", + "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz", + "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz", + "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.5.tgz", + "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz", + "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.38.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz", + "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.13.tgz", + "integrity": "sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001763", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz", + "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-next": { + "version": "15.1.7", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.1.7.tgz", + "integrity": "sha512-zXoMnYUIy3XHaAoOhrcYkT9UQWvXqWju2K7NNsmb5wd/7XESDwof61eUdW4QhERr3eJ9Ko/vnXqIrj8kk/drYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.1.7", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", + "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/graphql": { + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.16", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", + "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.4", + "@formatjs/fast-memoize": "2.2.7", + "@formatjs/icu-messageformat-parser": "2.11.2", + "tslib": "^2.8.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lite": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-1.2.1.tgz", + "integrity": "sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==", + "license": "MIT" + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lint-staged": { + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.2.tgz", + "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/listr2": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/lucide-react": { + "version": "0.475.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.7.tgz", + "integrity": "sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.40.0", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.0.2", + "graphql": "^16.12.0", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.7.0", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^5.2.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/msw/node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw/node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/msw/node_modules/type-fest": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz", + "integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", + "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/next": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", + "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.12", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.12", + "@next/swc-darwin-x64": "15.5.12", + "@next/swc-linux-arm64-gnu": "15.5.12", + "@next/swc-linux-arm64-musl": "15.5.12", + "@next/swc-linux-x64-gnu": "15.5.12", + "@next/swc-linux-x64-musl": "15.5.12", + "@next/swc-win32-arm64-msvc": "15.5.12", + "@next/swc-win32-x64-msvc": "15.5.12", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-auth": { + "version": "5.0.0-beta.30", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.30.tgz", + "integrity": "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-intl": { + "version": "3.26.5", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-3.26.5.tgz", + "integrity": "sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/amannn" + } + ], + "license": "MIT", + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "negotiator": "^1.0.0", + "use-intl": "^3.26.5" + }, + "peerDependencies": { + "next": "^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/oauth4webapi": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.3.tgz", + "integrity": "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-floater": { + "version": "0.9.5-4", + "resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.9.5-4.tgz", + "integrity": "sha512-3CBOgMfqD18A5HvQRKRNR6pKT5rOCzcdqDzyOU7RYNFgpiGm6BrMjDTXJrstEpRjJ4fyL65dQ6wTRIE2UMQTmQ==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.11.8", + "deepmerge-ts": "^7.1.0", + "is-lite": "^1.2.1", + "tree-changes-hook": "^0.11.2" + }, + "peerDependencies": { + "react": "16.8 - 19", + "react-dom": "16.8 - 19" + } + }, + "node_modules/react-hook-form": { + "version": "7.60.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz", + "integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-innertext": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz", + "integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">=0.0.0 <=99", + "react": ">=0.0.0 <=99" + } + }, + "node_modules/react-is": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", + "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "license": "MIT" + }, + "node_modules/react-joyride": { + "version": "3.0.0-7", + "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-3.0.0-7.tgz", + "integrity": "sha512-NBgtdm8QehHEVI/Qkakb4EJ/WjKN7bQaZgZmO/01v1p2yBlzAcXyKM36FeS1YZaywX8v8R79bF5Z0OcV5BK1og==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "@gilbarbara/hooks": "^0.8.2", + "@gilbarbara/types": "^0.2.2", + "deepmerge": "^4.3.1", + "is-lite": "^1.2.1", + "react-floater": "^0.9.5-4", + "react-innertext": "^1.1.5", + "scroll": "^3.0.1", + "scrollparent": "^2.1.0", + "tree-changes-hook": "^0.11.2" + }, + "peerDependencies": { + "react": "16.8 - 19", + "react-dom": "16.8 - 19" + } + }, + "node_modules/react-joyride/node_modules/@gilbarbara/hooks": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@gilbarbara/hooks/-/hooks-0.8.2.tgz", + "integrity": "sha512-aWXlJFCrqmasGaDd6IhSpqOFeOD4pSBpRtILKw0WxWQzWE+HYCA0adLf0P18BNztR/G0byWnpkGupeGx+NFnuw==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1" + }, + "peerDependencies": { + "react": "16.8 - 18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rettime": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", + "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/scroll": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scroll/-/scroll-3.0.1.tgz", + "integrity": "sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==", + "license": "MIT" + }, + "node_modules/scrollparent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.1.0.tgz", + "integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/tailwindcss/node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tree-changes": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.11.3.tgz", + "integrity": "sha512-r14mvDZ6tqz8PRQmlFKjhUVngu4VZ9d92ON3tp0EGpFBE6PAHOq8Bx8m8ahbNoGE3uI/npjYcJiqVydyOiYXag==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "is-lite": "^1.2.1" + } + }, + "node_modules/tree-changes-hook": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/tree-changes-hook/-/tree-changes-hook-0.11.3.tgz", + "integrity": "sha512-cNHPuFc5Qbi2B74VqSqL/Ee/l4n0SFfzYKTnXYViJW1yCFZ0bl97QsgUIw9vdQtqpWDwo83mpNkGUvcjeQc0Xw==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "tree-changes": "0.11.3" + }, + "peerDependencies": { + "react": "16.8 - 19" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-intl": { + "version": "3.26.5", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-3.26.5.tgz", + "integrity": "sha512-OdsJnC/znPvHCHLQH/duvQNXnP1w0hPfS+tkSi3mAbfjYBGh4JnyfdwkQBfIVf7t8gs9eSX/CntxUMvtKdG2MQ==", + "license": "MIT", + "dependencies": { + "@formatjs/fast-memoize": "^2.2.0", + "intl-messageformat": "^10.5.14" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/rafts/frontend/package.json b/rafts/frontend/package.json new file mode 100644 index 0000000..5eecabd --- /dev/null +++ b/rafts/frontend/package.json @@ -0,0 +1,90 @@ +{ + "name": "@raft/frontend", + "version": "0.1.0", + "private": true, + "engines": { + "node": ">=20.0.0", + "npm": ">=10.0.0" + }, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "typecheck": "tsc --noEmit", + "test": "vitest", + "test:run": "vitest run", + "test:coverage": "vitest run --coverage", + "test:ui": "vitest --ui", + "prepare": "husky", + "validate": "npm run typecheck && npm run lint && npm run test:run" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{json,css,md}": [ + "prettier --write" + ] + }, + "dependencies": { + "@emotion/cache": "^11.14.0", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@fontsource/roboto": "^5.1.1", + "@hookform/resolvers": "^4.1.0", + "@mui/icons-material": "^7.0.2", + "@mui/material": "^7.0.2", + "@mui/material-nextjs": "^7.0.2", + "@tanstack/react-table": "^8.21.2", + "dayjs": "^1.11.13", + "lodash": "^4.17.21", + "lucide-react": "^0.475.0", + "next": "^15.5.12", + "next-auth": "^5.0.0-beta.30", + "next-intl": "^3.26.4", + "next-themes": "^0.4.6", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-hook-form": "^7.54.2", + "react-joyride": "^3.0.0-7", + "xml2js": "^0.6.2", + "zod": "^3.24.2" + }, + "overrides": { + "@gilbarbara/hooks": { + "react": "$react" + } + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.1.0", + "@testing-library/user-event": "^14.5.2", + "@types/lodash": "^4.17.13", + "@types/node": "^22", + "@types/react": "^19", + "@types/react-dom": "^19", + "@types/xml2js": "^0.4.14", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/ui": "^4.0.18", + "eslint": "^9", + "eslint-config-next": "15.1.7", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.3", + "husky": "^9.1.7", + "jsdom": "^25.0.1", + "lint-staged": "^15.4.3", + "msw": "^2.7.0", + "postcss": "^8", + "prettier": "^3.5.1", + "tailwindcss": "^3.4.1", + "typescript": "^5", + "vitest": "^4.0.18" + } +} diff --git a/rafts/frontend/postcss.config.mjs b/rafts/frontend/postcss.config.mjs new file mode 100644 index 0000000..0dc456a --- /dev/null +++ b/rafts/frontend/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +} + +export default config diff --git a/rafts/frontend/public/file.svg b/rafts/frontend/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/rafts/frontend/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rafts/frontend/public/globe.svg b/rafts/frontend/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/rafts/frontend/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rafts/frontend/public/next.svg b/rafts/frontend/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/rafts/frontend/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rafts/frontend/public/rafts_all_flat.svg b/rafts/frontend/public/rafts_all_flat.svg new file mode 100644 index 0000000..0fb603a --- /dev/null +++ b/rafts/frontend/public/rafts_all_flat.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/rafts/frontend/public/rafts_full.svg b/rafts/frontend/public/rafts_full.svg new file mode 100644 index 0000000..76ece1d --- /dev/null +++ b/rafts/frontend/public/rafts_full.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/rafts/frontend/public/rafts_layered.svg b/rafts/frontend/public/rafts_layered.svg new file mode 100644 index 0000000..8b15828 --- /dev/null +++ b/rafts/frontend/public/rafts_layered.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rafts/frontend/public/rafts_structured.svg b/rafts/frontend/public/rafts_structured.svg new file mode 100644 index 0000000..1265d82 --- /dev/null +++ b/rafts/frontend/public/rafts_structured.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rafts/frontend/public/solar_tr.png b/rafts/frontend/public/solar_tr.png new file mode 100644 index 0000000..03d6f53 Binary files /dev/null and b/rafts/frontend/public/solar_tr.png differ diff --git a/rafts/frontend/public/window.svg b/rafts/frontend/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/rafts/frontend/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rafts/frontend/src/__tests__/setup.ts b/rafts/frontend/src/__tests__/setup.ts new file mode 100644 index 0000000..1830ee7 --- /dev/null +++ b/rafts/frontend/src/__tests__/setup.ts @@ -0,0 +1,111 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import '@testing-library/jest-dom/vitest' +import { beforeAll, afterAll, afterEach, vi } from 'vitest' +import { server } from '@/tests/mocks/server' + +// Start MSW server before all tests +beforeAll(() => server.listen({ onUnhandledRequest: 'warn' })) + +// Reset handlers after each test +afterEach(() => server.resetHandlers()) + +// Close server after all tests +afterAll(() => server.close()) + +// Mock next/navigation +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + back: vi.fn(), + forward: vi.fn(), + refresh: vi.fn(), + prefetch: vi.fn(), + }), + usePathname: () => '/en', + useSearchParams: () => new URLSearchParams(), + redirect: vi.fn(), +})) + +// Mock next-intl +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string) => key, + useLocale: () => 'en', +})) + +// Mock localStorage +const localStorageMock = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + length: 0, + key: vi.fn(), +} +Object.defineProperty(window, 'localStorage', { value: localStorageMock }) diff --git a/rafts/frontend/src/actions/ac/ac.login.ts b/rafts/frontend/src/actions/ac/ac.login.ts new file mode 100644 index 0000000..195cd55 --- /dev/null +++ b/rafts/frontend/src/actions/ac/ac.login.ts @@ -0,0 +1,131 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +/** + * Server action for user login using CADC login service + * Based on cap.xml "ivo://ivoa.net/sso#tls-with-password" capability + */ + +// URL from cap.xml for login +const LOGIN_URL = 'https://ws-cadc.canfar.net/ac/login' + +export interface LoginData { + username: string + password: string +} + +export async function loginUser(formData: LoginData) { + try { + // Convert the form data to application/x-www-form-urlencoded format + const formBody = new URLSearchParams({ + username: formData.username, + password: formData.password, + }).toString() + + // Call the CADC login endpoint + const response = await fetch(LOGIN_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': 'RAFT-System/1.0', + }, + body: formBody, + }) + + if (!response.ok) { + return { + success: false, + error: `Login failed with status ${response.status}`, + } + } + + // Extract the authentication token from the response + const token = await response.text() + + if (!token) { + return { + success: false, + error: 'No authentication token received', + } + } + + // Return the token for session creation + return { + success: true, + token: token.trim(), + username: formData.username, + } + } catch (error) { + console.error('Login error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error during login', + } + } +} diff --git a/rafts/frontend/src/actions/ac/cap.xml b/rafts/frontend/src/actions/ac/cap.xml new file mode 100644 index 0000000..552c8cb --- /dev/null +++ b/rafts/frontend/src/actions/ac/cap.xml @@ -0,0 +1,119 @@ + + + + https://ws-cadc.canfar.net/ac/capabilities + + + + + https://ws-cadc.canfar.net/ac/availability + + + + + https://ws-cadc.canfar.net/ac/logControl + + + + + + https://ws-cadc.canfar.net/ac/users + + + + + + + + https://ws-cadc.canfar.net/ac/userRequests + + + + + + + + + https://ws-cadc.canfar.net/ac/resetPassword + + + + + + + + https://ws-cadc.canfar.net/ac/whoami + + + + + + + + https://ws-cadc.canfar.net/ac/groups + + + + + + + + https://ws-cadc.canfar.net/ac/search + + + + + + + + https://ws-cadc.canfar.net/ac/search + + + + + + + + https://ws-cadc.canfar.net/ac/authorize + + + + + + + + + https://ws-cadc.canfar.net/ac + + + + + + + + + https://ws-cadc.canfar.net/ac/login + + + + + https://ws-cadc.canfar.net/ac/login + + + + + https://ws-cadc.canfar.net/ac/gidmap + + + + + + + + https://ws-cadc.canfar.net/ac/uidmap + + + + + + \ No newline at end of file diff --git a/rafts/frontend/src/actions/adesValidation.ts b/rafts/frontend/src/actions/adesValidation.ts new file mode 100644 index 0000000..3e56777 --- /dev/null +++ b/rafts/frontend/src/actions/adesValidation.ts @@ -0,0 +1,130 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import type { ADESFileKind, ADESValidationResult } from './adesValidation.types' + +// Use environment variables with fallbacks for different validator endpoints +// Server actions use runtime env vars (no NEXT_PUBLIC_ prefix needed) +// Check both prefixed and non-prefixed for compatibility +const VALIDATOR_URLS: Record = { + xml: + process.env.VALIDATOR_URL_XML || + process.env.NEXT_PUBLIC_VALIDATOR_URL_XML || + 'http://localhost:8000/validate-xml', + psv: + process.env.VALIDATOR_URL_PSV || + process.env.NEXT_PUBLIC_VALIDATOR_URL_PSV || + 'http://localhost:8000/validate-psv', + mpc: + process.env.VALIDATOR_URL_MPC || + process.env.NEXT_PUBLIC_VALIDATOR_URL_MPC || + 'http://localhost:8000/validate-mpc', +} + +/** + * Validates ADES format files against an internal validator + * + * @param formData - FormData containing the file to validate + * @param kind - Type of ADES file (xml, psv, or mpc) + * @returns Object containing success status and either result or error message + */ +export const validateADESFile = async ( + formData: FormData, + kind: ADESFileKind, +): Promise<{ success: boolean; result?: ADESValidationResult; error?: string }> => { + try { + const validatorURL = VALIDATOR_URLS[kind] + if (!validatorURL) { + return { success: false, error: `Invalid file kind: ${kind}` } + } + // Make the API call to the internal validator service + const backendRes = await fetch(validatorURL, { + method: 'POST', + body: formData, + }) + + if (!backendRes.ok) { + const errorData = await backendRes.json().catch(() => ({})) + console.error('Validation error:', errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${backendRes.status}`, + } + } + + const result: ADESValidationResult = await backendRes.json() + return { success: true, result } + } catch (error) { + console.error('Error validating ADES file:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/adesValidation.types.ts b/rafts/frontend/src/actions/adesValidation.types.ts new file mode 100644 index 0000000..3667b87 --- /dev/null +++ b/rafts/frontend/src/actions/adesValidation.types.ts @@ -0,0 +1,83 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export type ADESFileKind = 'xml' | 'psv' | 'mpc' + +export interface ADESValidationResult { + filename: string + validation_type: string + results: Array<{ + type: string + valid: boolean + message: string + }> + xml_info?: { + root_element: string + version: string + attributes?: Record + } +} diff --git a/rafts/frontend/src/actions/assignReviewer.ts b/rafts/frontend/src/actions/assignReviewer.ts new file mode 100644 index 0000000..825f2d8 --- /dev/null +++ b/rafts/frontend/src/actions/assignReviewer.ts @@ -0,0 +1,361 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { SUBMIT_DOI_URL, MESSAGE, SUCCESS } from '@/actions/constants' +import { createDoiFormData } from '@/actions/utils/doiFormData' +import { IResponseData } from '@/actions/types' +import { BACKEND_STATUS } from '@/shared/backendStatus' +import { updateRaftMetadata } from '@/services/canfarStorage' +import { RaftStatusChange } from '@/types/doi' + +/** + * Assigns a reviewer to a RAFT/DOI. + * Only publishers (members of RAFTS-test-reviewers group) can assign reviewers. + * + * The reviewer is stored as ivo://cadc.nrc.ca/vospace/doi#reviewer property. + * + * @param doiId - The DOI identifier suffix (e.g., "RAFTS-7rtut-gkryn.test") + * @param reviewerUsername - The username of the reviewer to assign + * @returns Response with success/error information + */ +export const assignReviewer = async ( + doiId: string, + reviewerUsername: string, +): Promise> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + const url = `${SUBMIT_DOI_URL}/${doiId}` + + // Backend expects multipart form data with JSON blob labeled 'doiNodeData' + const formData = createDoiFormData({ nodeData: { reviewer: reviewerUsername } }) + + const response = await fetch(url, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + body: formData, + redirect: 'manual', // Backend returns 303 on success + }) + + // 303 redirect means success + if (response.status === 303) { + return { + [SUCCESS]: true, + data: `Reviewer "${reviewerUsername}" assigned successfully`, + } + } + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[assignReviewer] Error response:', response.status, errorText) + return { + [SUCCESS]: false, + [MESSAGE]: `Failed to assign reviewer: ${response.status} ${errorText}`, + } + } + + return { + [SUCCESS]: true, + data: `Reviewer "${reviewerUsername}" assigned successfully`, + } + } catch (error) { + console.error('[assignReviewer] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} + +/** + * Claims a RAFT for review by assigning the current user as the reviewer + * AND changing the status to "in review" in a single API call. + * + * This does two things in one call: + * 1. Assigns the reviewer + * 2. Changes status from "review ready" → "in review" + * + * @param doiId - The DOI identifier suffix (e.g., "RAFTS-7rtut-gkryn.test") + * @returns Response with success/error information + */ +export const claimForReview = async ( + doiId: string, + dataDirectory?: string, +): Promise> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + if (!session?.user?.name) { + return { [SUCCESS]: false, [MESSAGE]: 'Username not available' } + } + + const reviewerName = session.user.name + const url = `${SUBMIT_DOI_URL}/${doiId}` + + // Backend supports setting both reviewer and status in one call + const formData = createDoiFormData({ + nodeData: { reviewer: reviewerName, status: BACKEND_STATUS.IN_REVIEW }, + }) + + const response = await fetch(url, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + body: formData, + redirect: 'manual', + }) + + if (response.status === 303 || response.ok) { + // Update RAFT.json metadata with status history + if (dataDirectory) { + try { + const statusChange: RaftStatusChange = { + fromStatus: BACKEND_STATUS.REVIEW_READY, + toStatus: BACKEND_STATUS.IN_REVIEW, + changedBy: reviewerName, + changedAt: new Date().toISOString(), + } + + await updateRaftMetadata( + dataDirectory, + { + updatedAt: new Date().toISOString(), + updatedBy: reviewerName, + statusHistory: [statusChange], + }, + accessToken, + ) + } catch (metaError) { + console.warn('[claimForReview] Metadata update failed (non-critical):', metaError) + } + } + + return { + [SUCCESS]: true, + data: `RAFT claimed for review by "${reviewerName}"`, + } + } + + const errorText = await response.text().catch(() => '') + console.error('[claimForReview] Error response:', response.status, errorText) + return { + [SUCCESS]: false, + [MESSAGE]: `Failed to claim for review: ${response.status} ${errorText}`, + } + } catch (error) { + console.error('[claimForReview] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} + +/** + * Releases a review by unassigning the reviewer AND changing status back to "review ready". + * Only publishers can release reviews. + * + * This does two things in one call: + * 1. Removes the reviewer assignment + * 2. Changes status from "in review" → "review ready" + * + * @param doiId - The DOI identifier suffix (e.g., "RAFTS-7rtut-gkryn.test") + * @returns Response with success/error information + */ +export const releaseReview = async ( + doiId: string, + dataDirectory?: string, +): Promise> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + const url = `${SUBMIT_DOI_URL}/${doiId}` + + // Send empty reviewer and status back to review ready + const formData = createDoiFormData({ + nodeData: { reviewer: '', status: BACKEND_STATUS.REVIEW_READY }, + }) + + const response = await fetch(url, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + body: formData, + redirect: 'manual', + }) + + if (response.status === 303 || response.ok) { + // Update RAFT.json metadata with status history + if (dataDirectory) { + try { + const statusChange: RaftStatusChange = { + fromStatus: BACKEND_STATUS.IN_REVIEW, + toStatus: BACKEND_STATUS.REVIEW_READY, + changedBy: session?.user?.name || '', + changedAt: new Date().toISOString(), + } + + await updateRaftMetadata( + dataDirectory, + { + updatedAt: new Date().toISOString(), + updatedBy: session?.user?.name || '', + statusHistory: [statusChange], + }, + accessToken, + ) + } catch (metaError) { + console.warn('[releaseReview] Metadata update failed (non-critical):', metaError) + } + } + + return { [SUCCESS]: true, data: 'Review released successfully' } + } + + const errorText = await response.text().catch(() => '') + return { + [SUCCESS]: false, + [MESSAGE]: `Failed to release review: ${response.status} ${errorText}`, + } + } catch (error) { + console.error('[releaseReview] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} + +/** + * Unassigns the reviewer from a RAFT/DOI (without changing status). + * Only publishers can unassign reviewers. + * + * @param doiId - The DOI identifier suffix (e.g., "RAFTS-7rtut-gkryn.test") + * @returns Response with success/error information + */ +export const unassignReviewer = async (doiId: string): Promise> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + const url = `${SUBMIT_DOI_URL}/${doiId}` + + // Send empty reviewer to unassign + const formData = createDoiFormData({ nodeData: { reviewer: '' } }) + + const response = await fetch(url, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + body: formData, + redirect: 'manual', + }) + + if (response.status === 303 || response.ok) { + return { [SUCCESS]: true, data: 'Reviewer unassigned successfully' } + } + + const errorText = await response.text().catch(() => '') + return { + [SUCCESS]: false, + [MESSAGE]: `Failed to unassign reviewer: ${response.status} ${errorText}`, + } + } catch (error) { + console.error('[unassignReviewer] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/attachments.ts b/rafts/frontend/src/actions/attachments.ts new file mode 100644 index 0000000..085dc2e --- /dev/null +++ b/rafts/frontend/src/actions/attachments.ts @@ -0,0 +1,273 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +/** + * Server Actions for Attachment Operations + * + * These actions wrap the attachmentService functions to be callable from + * client components. They handle authentication via the session. + */ + +import { auth } from '@/auth/cadc-auth/credentials' +import { + uploadAttachment as uploadToVOSpace, + downloadAttachment as downloadFromVOSpace, + downloadAttachmentAsBase64 as downloadBase64FromVOSpace, + deleteAttachment as deleteFromVOSpace, +} from '@/services/attachmentService' +import { FileReference, getMimeTypeFromExtension } from '@/types/attachments' + +// ============================================================================ +// Upload Action +// ============================================================================ + +export interface UploadAttachmentResult { + success: boolean + fileReference?: FileReference + error?: string +} + +/** + * Server action to upload an attachment to VOSpace + * + * @param doiIdentifier - The DOI identifier (e.g., "25.0047") + * @param filename - The filename to use for storage + * @param base64Content - File content as base64 data URL (for binary) or raw text + * @param mimeType - MIME type of the file + * @returns Upload result with FileReference on success + */ +export async function uploadAttachment( + doiIdentifier: string, + filename: string, + base64Content: string, + mimeType: string, +): Promise { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + try { + // Fallback to extension-based MIME type if not provided or empty + // Browsers often report empty MIME types for .psv, .mpc files + const effectiveMimeType = mimeType || getMimeTypeFromExtension(filename) + + let content: Blob | string + + // Check if it's a base64 data URL (binary content) + if (base64Content.startsWith('data:')) { + // Extract the base64 data from the data URL + const base64Data = base64Content.split(',')[1] + const binaryString = Buffer.from(base64Data, 'base64') + content = new Blob([binaryString], { type: effectiveMimeType }) + } else { + // It's raw text content + content = base64Content + } + + const result = await uploadToVOSpace( + doiIdentifier, + filename, + content, + effectiveMimeType, + accessToken, + ) + + return result + } catch (error) { + console.error('[uploadAttachment action] Error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Upload failed', + } + } +} + +// ============================================================================ +// Download Actions +// ============================================================================ + +export interface DownloadAttachmentResult { + success: boolean + content?: string + mimeType?: string + error?: string +} + +/** + * Server action to download an attachment from VOSpace + * + * @param doiIdentifier - The DOI identifier + * @param filename - The filename to download + * @param asText - If true, return content as text string + * @returns Download result with content on success + */ +export async function downloadAttachment( + doiIdentifier: string, + filename: string, + asText: boolean = false, +): Promise { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + try { + const result = await downloadFromVOSpace(doiIdentifier, filename, accessToken, asText) + + if (!result.success || !result.content) { + return { success: false, error: result.error || 'Download failed' } + } + + // Convert Blob to string for client consumption + let content: string + if (result.content instanceof Blob) { + const buffer = await result.content.arrayBuffer() + const base64 = Buffer.from(buffer).toString('base64') + content = `data:${result.mimeType};base64,${base64}` + } else { + content = result.content + } + + return { + success: true, + content, + mimeType: result.mimeType, + } + } catch (error) { + console.error('[downloadAttachment action] Error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Download failed', + } + } +} + +/** + * Server action to download an attachment as base64 data URL + * Useful for displaying images in the browser + */ +export async function downloadAttachmentAsBase64( + doiIdentifier: string, + filename: string, +): Promise<{ success: boolean; base64?: string; error?: string }> { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + try { + return await downloadBase64FromVOSpace(doiIdentifier, filename, accessToken) + } catch (error) { + console.error('[downloadAttachmentAsBase64 action] Error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Download failed', + } + } +} + +// ============================================================================ +// Delete Action +// ============================================================================ + +/** + * Server action to delete an attachment from VOSpace + * + * @param doiIdentifier - The DOI identifier + * @param filename - The filename to delete + * @returns Success status + */ +export async function deleteAttachment( + doiIdentifier: string, + filename: string, +): Promise<{ success: boolean; error?: string }> { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + try { + return await deleteFromVOSpace(doiIdentifier, filename, accessToken) + } catch (error) { + console.error('[deleteAttachment action] Error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Delete failed', + } + } +} diff --git a/rafts/frontend/src/actions/auth.ts b/rafts/frontend/src/actions/auth.ts new file mode 100644 index 0000000..08acdd8 --- /dev/null +++ b/rafts/frontend/src/actions/auth.ts @@ -0,0 +1,151 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { signIn, signOut } from '@/auth/cadc-auth/credentials' + +export interface AuthState { + success: boolean + error: null | string +} +export interface LoginFormValues { + username: string + password: string + turnstileToken?: string +} + +// Verify Turnstile token with Cloudflare +async function verifyTurnstile(token: string): Promise { + const secretKey = process.env.TURNSTILE_SECRET_KEY + if (!secretKey) { + console.warn('TURNSTILE_SECRET_KEY not configured, skipping verification') + return true + } + + try { + const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + secret: secretKey, + response: token, + }), + }) + + const data = await response.json() + return data.success === true + } catch (error) { + console.error('Turnstile verification error:', error) + return false + } +} + +export const authenticateUser = async (prevState: AuthState | null, formData: LoginFormValues) => { + try { + // Verify Turnstile token if provided + if (formData.turnstileToken) { + const isValid = await verifyTurnstile(formData.turnstileToken) + if (!isValid) { + return { + success: false, + error: 'Security verification failed. Please try again.', + } + } + } else if (process.env.TURNSTILE_SECRET_KEY) { + // Turnstile is configured but no token provided + return { + success: false, + error: 'Security verification required', + } + } + + await signIn('credentials', { + username: formData.username, + password: formData.password, + redirect: false, + }) + + return { success: true, error: null } + } catch (error) { + console.error('Authentication error:', error) + return { + success: false, + error: 'Invalid credentials or server error', + } + } +} + +export const handleSignOut = async () => { + try { + await signOut() + return { success: true } + } catch (error) { + console.error('Error during sign out:', error) + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' } + } +} diff --git a/rafts/frontend/src/actions/constants.ts b/rafts/frontend/src/actions/constants.ts new file mode 100644 index 0000000..0314d46 --- /dev/null +++ b/rafts/frontend/src/actions/constants.ts @@ -0,0 +1,76 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// DOI service URL - configurable via environment variable for local development +// Default: CANFAR production DOI service +// Local dev: https://haproxy.cadc.dao.nrc.ca/doi/instances +export const SUBMIT_DOI_URL = + process.env.NEXT_DOI_BASE_URL || 'https://ws-cadc.canfar.net/doi/instances' + +export const MESSAGE = 'message' +export const SUCCESS = 'success' +export const DATA = 'data' diff --git a/rafts/frontend/src/actions/createDOIForDraft.ts b/rafts/frontend/src/actions/createDOIForDraft.ts new file mode 100644 index 0000000..e95bf4f --- /dev/null +++ b/rafts/frontend/src/actions/createDOIForDraft.ts @@ -0,0 +1,220 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +/** + * Create DOI for Draft + * + * Creates a minimal DOI entry in DataCite to get a DOI identifier. + * This allows attachment uploads before the form is fully completed. + * The DOI is created in "draft" state and can be updated later. + */ + +import { auth } from '@/auth/cadc-auth/credentials' +import { SUBMIT_DOI_URL, SUCCESS, MESSAGE } from '@/actions/constants' +import { createDoiFormData } from '@/actions/utils/doiFormData' +import { IResponseData } from '@/actions/types' +import { ensureContainerNodeExists } from '@/services/vospaceTransfer' +import { getCurrentPath } from '@/services/utils' + +const ATTACHMENTS_FOLDER = 'attachments' + +export interface CreateDOIResult { + doiIdentifier: string + doiUrl: string +} + +/** + * Build minimal DataCite metadata for draft DOI creation + */ +function buildMinimalDataCiteMetadata(title: string, creatorName: string): Record { + const publicationYear = new Date().getFullYear() + const nameParts = creatorName.split(' ') + const givenName = nameParts[0] || 'Unknown' + const familyName = nameParts.slice(1).join(' ') || 'Unknown' + + return { + resource: { + '@xmlns': 'http://datacite.org/schema/kernel-4', + identifier: { + '@identifierType': 'DOI', + $: '10.5072/draft', // Placeholder, will be assigned by service + }, + creators: { + $: [ + { + creator: { + creatorName: { + '@nameType': 'Personal', + $: `${familyName}, ${givenName}`, + }, + givenName: { $: givenName }, + familyName: { $: familyName }, + affiliation: { $: 'Not specified' }, + }, + }, + ], + }, + titles: { + $: [{ title: { $: title || 'Untitled RAFT Draft' } }], + }, + publisher: { $: 'NRC CADC' }, + publicationYear: { $: publicationYear }, + resourceType: { + '@resourceTypeGeneral': 'Dataset', + $: 'RAFT Announcement', + }, + }, + } +} + +/** + * Create a minimal DOI for draft purposes + * + * This creates a DOI entry so that attachments can be uploaded + * before the form is fully completed. The DOI metadata will be + * updated when the form is submitted. + * + * @param title - The RAFT title (can be provisional) + * @returns DOI identifier and URL on success + */ +export async function createDOIForDraft( + title: string = 'Untitled RAFT Draft', +): Promise> { + const session = await auth() + const accessToken = session?.accessToken + const user = session?.user + + if (!accessToken || !user) { + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + try { + // Build minimal metadata + const creatorName = user.name || user.id || 'Unknown' + const metadata = buildMinimalDataCiteMetadata(title, creatorName) + + // Create multipart form data (same format as submitDOI) + const multipartFormData = createDoiFormData({ metaData: metadata }) + + // Submit to DOI service + const response = await fetch(SUBMIT_DOI_URL, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + body: multipartFormData, + redirect: 'manual', + }) + + // 303 redirect means success - Location header contains the new DOI URL + if (response.status === 303) { + const location = response.headers.get('Location') + + if (location) { + const doiIdentifier = location.split('/').pop() + + if (doiIdentifier) { + // Create the data folder and attachments subfolder in VOSpace + try { + const basePath = getCurrentPath(doiIdentifier) + + await ensureContainerNodeExists(basePath, accessToken) + await ensureContainerNodeExists(`${basePath}/${ATTACHMENTS_FOLDER}`, accessToken) + } catch (folderError) { + console.warn('[createDOIForDraft] Failed to create VOSpace folders:', folderError) + // Continue anyway - folders will be created on first upload + } + + return { + [SUCCESS]: true, + data: { + doiIdentifier, + doiUrl: location, + }, + } + } + } + } + + // Handle error responses + const errorText = await response.text().catch(() => '') + console.error('[createDOIForDraft] Error:', response.status, errorText) + + return { + [SUCCESS]: false, + [MESSAGE]: `Failed to create DOI: ${response.status} ${errorText}`, + } + } catch (error) { + console.error('[createDOIForDraft] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'Unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/deleteRaft.ts b/rafts/frontend/src/actions/deleteRaft.ts new file mode 100644 index 0000000..c850e73 --- /dev/null +++ b/rafts/frontend/src/actions/deleteRaft.ts @@ -0,0 +1,139 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { SUBMIT_DOI_URL, SUCCESS, MESSAGE } from '@/actions/constants' +import { IResponseData } from '@/actions/types' + +/** + * Server action to delete a RAFT/DOI from the backend + * + * The DOI service expects: + * - DELETE /doi/instances/{doiSuffix} + * - Cookie-based authentication with CADC_SSO + * + * @param {string} doiSuffix - The DOI suffix to delete (e.g., "24.0001") + * @returns Response with success/error information + */ +export const deleteRaft = async (doiSuffix: string): Promise> => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + if (!doiSuffix) { + return { [SUCCESS]: false, [MESSAGE]: 'DOI suffix is required for deletion' } + } + + // Make the DELETE API call with cookie-based auth (DOI service expects CADC_SSO cookie) + const response = await fetch(`${SUBMIT_DOI_URL}/${doiSuffix}`, { + method: 'DELETE', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[deleteRaft] Error response:', response.status, errorText) + + // Parse error message from response if available + let errorMessage = `Request failed with status ${response.status}` + if (errorText) { + errorMessage = errorText + } + if (response.status === 401) { + errorMessage = 'Not authorized to delete this resource' + } + if (response.status === 403) { + errorMessage = 'Access denied - you may not have permission to delete this DOI' + } + if (response.status === 404) { + errorMessage = 'DOI not found' + } + + return { + [SUCCESS]: false, + [MESSAGE]: errorMessage, + } + } + + return { [SUCCESS]: true, [MESSAGE]: 'RAFT deleted successfully' } + } catch (error) { + console.error('[deleteRaft] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getDOI.ts b/rafts/frontend/src/actions/getDOI.ts new file mode 100644 index 0000000..ab07e0f --- /dev/null +++ b/rafts/frontend/src/actions/getDOI.ts @@ -0,0 +1,144 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { SUBMIT_DOI_URL } from '@/actions/constants' +import { parseXmlToJson } from '@/utilities/xmlParser' +import { sortByIdentifierNumber } from '@/utilities/doiIdentifier' +import { DOIData } from '@/types/doi' +import { downloadRaftFile } from '@/services/canfarStorage' + +export const getDOIData = async () => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + console.error('[getDOIData] No access token available') + return { success: false, error: 'Not authenticated' } + } + + // Make the API call with the access token as a cookie (DOI expects cookie auth) + const response = await fetch(`${SUBMIT_DOI_URL}`, { + method: 'GET', + headers: { + Accept: 'application/xml', + Cookie: `CADC_SSO=${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[getDOIData] Error response:', response.status, errorText) + if (response.status === 401) { + return { + success: false, + data: [], + error: `${response.status}`, + } + } + + return { + success: false, + data: [], + error: `Request failed with status ${response.status}`, + } + } + + const xmlString = await response.text() + const data: DOIData[] = await parseXmlToJson(xmlString) + + // Enrich with RAFT.json titles (the DOI status title is stale after updates) + const enrichedData = await Promise.all( + data.map(async (doi) => { + if (doi.dataDirectory && accessToken) { + try { + const raftResult = await downloadRaftFile(doi.dataDirectory, accessToken) + if (raftResult.success && raftResult.data?.generalInfo?.title) { + return { ...doi, title: raftResult.data.generalInfo.title } + } + } catch { + // Fall back to DOI status title + } + } + return doi + }), + ) + + return { success: true, data: sortByIdentifierNumber(enrichedData) } + } catch (error) { + console.error('[getDOIData] Exception:', error) + return { + success: false, + data: [], + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getDOIRAFT.ts b/rafts/frontend/src/actions/getDOIRAFT.ts new file mode 100644 index 0000000..604e86f --- /dev/null +++ b/rafts/frontend/src/actions/getDOIRAFT.ts @@ -0,0 +1,187 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { downloadRaftFile } from '@/services/canfarStorage' +import { TRaftContext } from '@/context/types' +import { IResponseData } from '@/actions/types' +import { SUBMIT_DOI_URL } from '@/actions/constants' +import { parseXmlToJson } from '@/utilities/xmlParser' +import { DOIData } from '@/types/doi' +import { dataCiteToRaft, parseDataCiteXml } from '@/utilities/dataCiteToRaft' + +export const getDOIRaft = async (dataIdentifier: string): Promise> => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, message: 'Not authenticated' } + } + + // Fetch DOI data to get the correct dataDirectory for this identifier + const doiResponse = await fetch(`${SUBMIT_DOI_URL}`, { + method: 'GET', + headers: { + Accept: 'application/xml', + Cookie: `CADC_SSO=${accessToken}`, + }, + }) + + if (!doiResponse.ok) { + console.error('[getDOIRaft] Failed to fetch DOI list:', doiResponse.status) + return { success: false, message: `Failed to fetch DOI list: ${doiResponse.status}` } + } + + const xmlString = await doiResponse.text() + const doiDataList: DOIData[] = await parseXmlToJson(xmlString) + + // Find the DOI entry matching the dataIdentifier (identifier ends with the dataIdentifier) + const matchingDoi = doiDataList.find((doi) => doi.identifier.endsWith(`/${dataIdentifier}`)) + + if (!matchingDoi) { + console.error('[getDOIRaft] No matching DOI found for:', dataIdentifier) + return { success: false, message: `No matching DOI found for: ${dataIdentifier}` } + } + + // Try to download RAFT.json first + const response = await downloadRaftFile(matchingDoi.dataDirectory, accessToken) + + if (response.success && response.data) { + // Override status from DOI list (RAFT.json may have stale status) + const raftData = response.data + if (raftData.generalInfo && matchingDoi.status) { + raftData.generalInfo.status = matchingDoi.status as typeof raftData.generalInfo.status + } + // Create a new object with all fields to ensure proper serialization + const finalData: TRaftContext = { + ...raftData, + id: dataIdentifier, + dataDirectory: matchingDoi.dataDirectory, + reviewer: matchingDoi.reviewer, + } + return { success: true, data: finalData } + } + + // FALLBACK: If RAFT.json doesn't exist, construct from DataCite XML + + try { + // Fetch the full DataCite XML for this specific DOI + const dataCiteResponse = await fetch(`${SUBMIT_DOI_URL}/${dataIdentifier}`, { + method: 'GET', + headers: { + Accept: 'application/xml', + Cookie: `CADC_SSO=${accessToken}`, + }, + }) + + if (!dataCiteResponse.ok) { + console.error('[getDOIRaft] Failed to fetch DataCite XML:', dataCiteResponse.status) + return { + success: false, + message: `RAFT.json not found and failed to fetch DataCite XML: ${dataCiteResponse.status}`, + } + } + + const dataCiteXml = await dataCiteResponse.text() + + // Parse DataCite XML and convert to RAFT form structure + const dataCiteResource = await parseDataCiteXml(dataCiteXml) + + const raftData = dataCiteToRaft(dataCiteResource, { + title: matchingDoi.title, + status: matchingDoi.status, + }) + + // Set the id, dataDirectory, and reviewer fields + raftData.id = dataIdentifier + raftData.dataDirectory = matchingDoi.dataDirectory + raftData.reviewer = matchingDoi.reviewer + + return { + success: true, + data: raftData, + message: 'Constructed from DataCite XML (RAFT.json not found)', + } + } catch (fallbackError) { + console.error('[getDOIRaft] Fallback failed:', fallbackError) + return { + success: false, + message: response?.message || 'RAFT.json not found and fallback to XML failed', + } + } + } catch (error) { + console.error('DOI data retrieval error:', error) + return { + success: false, + message: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getDOIsForReview.ts b/rafts/frontend/src/actions/getDOIsForReview.ts new file mode 100644 index 0000000..52616da --- /dev/null +++ b/rafts/frontend/src/actions/getDOIsForReview.ts @@ -0,0 +1,227 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { SUBMIT_DOI_URL } from '@/actions/constants' +import { parseXmlToJson } from '@/utilities/xmlParser' +import { DOIData, RaftData } from '@/types/doi' +import { downloadRaftFile } from '@/services/canfarStorage' +import { TRaftContext } from '@/context/types' +import { + OPTION_REVIEW, + OPTION_UNDER_REVIEW, + OPTION_APPROVED, + OPTION_REJECTED, +} from '@/shared/constants' + +import { BACKEND_STATUS } from '@/shared/backendStatus' + +// Map frontend status constants to backend status values +const STATUS_MAPPING: Record = { + [OPTION_REVIEW]: BACKEND_STATUS.REVIEW_READY, // review_ready -> review ready (waiting for reviewer) + [OPTION_UNDER_REVIEW]: BACKEND_STATUS.IN_REVIEW, // under_review -> in review (reviewer claimed) + [OPTION_APPROVED]: BACKEND_STATUS.APPROVED, // approved -> approved + [OPTION_REJECTED]: BACKEND_STATUS.REJECTED, // rejected -> rejected +} + +export interface ReviewRaftsResponse { + data: RaftData[] + counts: Record +} + +export const getDOIsForReview = async ( + filterStatus?: string, +): Promise<{ success: boolean; data?: ReviewRaftsResponse; error?: string }> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + console.error('[getDOIsForReview] No access token available') + return { success: false, error: 'Not authenticated' } + } + + // Fetch all DOIs + const response = await fetch(`${SUBMIT_DOI_URL}`, { + method: 'GET', + headers: { + Accept: 'application/xml', + Cookie: `CADC_SSO=${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[getDOIsForReview] Error response:', response.status, errorText) + if (response.status === 401 || response.status === 403) { + return { success: false, error: '401' } + } + return { + success: false, + error: `Request failed with status ${response.status}`, + } + } + + const xmlString = await response.text() + const doiDataList: DOIData[] = await parseXmlToJson(xmlString) + + // Calculate counts for all statuses + const counts: Record = { + [OPTION_REVIEW]: 0, + [OPTION_UNDER_REVIEW]: 0, + [OPTION_APPROVED]: 0, + [OPTION_REJECTED]: 0, + } + + // Count DOIs by status + doiDataList.forEach((doi) => { + const backendStatus = doi.status?.toLowerCase() + if (backendStatus === BACKEND_STATUS.REVIEW_READY) { + counts[OPTION_REVIEW]++ // review ready (waiting for reviewer) + } else if (backendStatus === BACKEND_STATUS.IN_REVIEW) { + counts[OPTION_UNDER_REVIEW]++ // in review (reviewer claimed) + } else if (backendStatus === BACKEND_STATUS.APPROVED) { + counts[OPTION_APPROVED]++ + } else if (backendStatus === BACKEND_STATUS.REJECTED) { + counts[OPTION_REJECTED]++ + } + }) + + // Filter DOIs by requested status + const backendStatus = filterStatus ? STATUS_MAPPING[filterStatus] : null + const filteredDois = backendStatus + ? doiDataList.filter((doi) => doi.status?.toLowerCase() === backendStatus) + : doiDataList.filter((doi) => doi.status?.toLowerCase() === BACKEND_STATUS.REVIEW_READY) + + // Fetch full RAFT data for each filtered DOI + const rafts: RaftData[] = [] + + for (const doi of filteredDois) { + try { + // Extract identifier suffix from the full identifier (e.g., "RAFTS-7rtut-gkryn.test") + // Full identifier format: "doi:10.80791/RAFTS-7rtut-gkryn.test" or similar + const identifierParts = doi.identifier.split('/') + const raftSuffix = identifierParts[identifierParts.length - 1] // Just the last part (RAFTS-xxx) + + // Download RAFT.json + const raftResponse = await downloadRaftFile(doi.dataDirectory, accessToken) + + if (raftResponse.success && raftResponse.data) { + const raftData = raftResponse.data as TRaftContext + + // Override status from DOI list + if (raftData.generalInfo && doi.status) { + raftData.generalInfo.status = doi.status as typeof raftData.generalInfo.status + } + + // Convert to RaftData format - use raftSuffix for URL-safe ID + // Preserve metadata from RAFT.json if available, fall back to defaults + rafts.push({ + _id: raftSuffix, + id: raftSuffix, + ...raftData, + relatedRafts: [], + generateForumPost: false, + createdBy: + ((raftData as Record).createdBy as string) || + raftData.authorInfo?.correspondingAuthor?.email || + '', + createdAt: ((raftData as Record).createdAt as string) || '', + updatedAt: ((raftData as Record).updatedAt as string) || '', + doi: doi.identifier, + dataDirectory: doi.dataDirectory, + submittedAt: (raftData as Record).submittedAt as string | undefined, + version: (raftData as Record).version as number | undefined, + statusHistory: (raftData as Record).statusHistory as + | RaftData['statusHistory'] + | undefined, + } as RaftData) + } else { + console.warn('[getDOIsForReview] Could not fetch RAFT.json for:', doi.identifier) + } + } catch (err) { + console.error('[getDOIsForReview] Error fetching RAFT for:', doi.identifier, err) + } + } + + return { + success: true, + data: { + data: rafts, + counts, + }, + } + } catch (error) { + console.error('[getDOIsForReview] Exception:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getInternalRaftById.ts b/rafts/frontend/src/actions/getInternalRaftById.ts new file mode 100644 index 0000000..8df4b41 --- /dev/null +++ b/rafts/frontend/src/actions/getInternalRaftById.ts @@ -0,0 +1,128 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { RaftData } from '@/types/doi' +import { useMockData } from '@/config/environment' +import { getMockRaftById } from '@/tests/mock-data-loader' + +export const getInternalRaftById = async (id: string) => { + try { + // Use mock data if enabled + if (useMockData) { + const mockRaft = getMockRaftById(id) + + if (mockRaft) { + return { success: true, data: mockRaft } + } else { + return { success: false, error: 'RAFT not found' } + } + } + + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + // Make the API call with the access token as a Bearer token + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/rafts/internal/${id}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error(`Failed to fetch RAFT with ID ${id}:`, errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + const data: RaftData = responseData.data + + return { success: true, data } + } catch (error) { + console.error(`Error fetching RAFT with ID ${id}:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getPublishedRaftById.ts b/rafts/frontend/src/actions/getPublishedRaftById.ts new file mode 100644 index 0000000..681b260 --- /dev/null +++ b/rafts/frontend/src/actions/getPublishedRaftById.ts @@ -0,0 +1,107 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { RaftData } from '@/types/doi' + +export const getPublishedRaftById = async (id: string) => { + try { + // Get the session with the access token + + // Make the API call with the access token as a Bearer token + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/rafts/published/${id}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error(`Failed to fetch RAFT with ID ${id}:`, errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + const data: RaftData = responseData.data + + return { success: true, data } + } catch (error) { + console.error(`Error fetching RAFT with ID ${id}:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getRaftById.ts b/rafts/frontend/src/actions/getRaftById.ts new file mode 100644 index 0000000..60d27e8 --- /dev/null +++ b/rafts/frontend/src/actions/getRaftById.ts @@ -0,0 +1,128 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { RaftData } from '@/types/doi' +import { useMockData } from '@/config/environment' +import { getMockRaftById } from '@/tests/mock-data-loader' + +export const getRaftById = async (id: string) => { + try { + // Use mock data if enabled + if (useMockData) { + const mockRaft = getMockRaftById(id) + + if (mockRaft) { + return { success: true, data: mockRaft } + } else { + return { success: false, error: 'RAFT not found' } + } + } + + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + // Make the API call with the access token as a Bearer token + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/rafts/user-owned/${id}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error(`Failed to fetch RAFT with ID ${id}:`, errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + const data: RaftData = responseData.data + + return { success: true, data } + } catch (error) { + console.error(`Error fetching RAFT with ID ${id}:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getRaftReview.ts b/rafts/frontend/src/actions/getRaftReview.ts new file mode 100644 index 0000000..e951abc --- /dev/null +++ b/rafts/frontend/src/actions/getRaftReview.ts @@ -0,0 +1,193 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { RaftReview } from '@/types/reviews' +import { useMockData } from '@/config/environment' +import { getMockRaftById } from '@/tests/mock-data-loader' + +export const getRaftReview = async (raftId: string) => { + try { + // Use mock data if enabled + if (useMockData) { + const mockRaft = getMockRaftById(raftId) + + if (!mockRaft) { + return { success: false, error: 'RAFT not found' } + } + + // Create a mock review based on the RAFT status + const mockReview: RaftReview = { + _id: `review-${raftId}`, + raftId: raftId, + currentVersion: 1, + versions: [ + { + versionNumber: 1, + raftData: mockRaft, + createdAt: mockRaft.createdAt, + createdBy: { + _id: 'mock-user-1', + firstName: 'Mock', + lastName: 'User', + }, + commitMessage: 'Initial submission', + _id: `version-${raftId}-1`, + }, + ], + statusHistory: + mockRaft.generalInfo.status !== 'review_ready' + ? [ + { + fromStatus: 'review_ready', + toStatus: mockRaft.generalInfo.status, + changedBy: { + _id: 'mock-reviewer-1', + firstName: 'Mock', + lastName: 'Reviewer', + }, + changedAt: mockRaft.updatedAt, + reason: `Status changed to ${mockRaft.generalInfo.status}`, + _id: `status-change-${raftId}-1`, + }, + ] + : [], + comments: + mockRaft.generalInfo.status === 'rejected' + ? [ + { + _id: `comment-${raftId}-1`, + content: 'This submission needs more data to support the findings.', + createdBy: { + _id: 'mock-reviewer-1', + firstName: 'Mock', + lastName: 'Reviewer', + }, + createdAt: mockRaft.updatedAt, + isResolved: false, + }, + ] + : [], + assignedReviewers: [ + { + _id: 'mock-reviewer-1', + firstName: 'Mock', + lastName: 'Reviewer', + }, + ], + isActive: + mockRaft.generalInfo.status !== 'approved' && mockRaft.generalInfo.status !== 'rejected', + createdAt: mockRaft.createdAt, + updatedAt: mockRaft.updatedAt, + } + + return { success: true, data: mockReview } + } + + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + // Make the API call with the access token as a Bearer token + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/reviews/by-raft/${raftId}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error(`Failed to fetch reviews for RAFT ${raftId}:`, errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + const data: RaftReview = responseData.data + + return { success: true, data } + } catch (error) { + console.error(`Error fetching reviews for RAFT ${raftId}:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getRafts.ts b/rafts/frontend/src/actions/getRafts.ts new file mode 100644 index 0000000..1deb331 --- /dev/null +++ b/rafts/frontend/src/actions/getRafts.ts @@ -0,0 +1,152 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' +import { RaftData } from '@/types/doi' +import { loadMockRaftData, getMockRaftCounts } from '@/tests/mock-data-loader' +import { useMockData } from '@/config/environment' + +export interface RaftApiResponse { + message: string + data: RaftData[] + meta: { + total: number + page: number + limit: number + totalPages: number + } +} + +export interface GetRaftsOptions { + page?: number + limit?: number + status?: string + search?: string +} + +export const getRafts = async (options: GetRaftsOptions = {}) => { + try { + // Use mock data if enabled + if (useMockData) { + const mockData = options.status ? loadMockRaftData(options.status) : loadMockRaftData() + const allCounts = getMockRaftCounts() + const total = options.status ? allCounts[options.status] || 0 : mockData.length + + const response: RaftApiResponse = { + message: 'Mock data loaded', + data: mockData, + meta: { + total: total, + page: options.page || 1, + limit: options.limit || 10, + totalPages: Math.ceil(total / (options.limit || 10)), + }, + } + return { success: true, data: response } + } + + // Get the session with the access token + + // Build query parameters + const queryParams = new URLSearchParams() + if (options.page) queryParams.append('page', options.page.toString()) + if (options.limit) queryParams.append('limit', options.limit.toString()) + if (options.status) queryParams.append('status', options.status) + if (options.search) queryParams.append('search', options.search) + + const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '' + + // Make the API call with the access token as a Bearer token + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/rafts${queryString}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error('Failed to fetch RAFTs:', errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const data: RaftApiResponse = await response.json() + return { success: true, data: data } + } catch (error) { + console.error('Error fetching RAFTs:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getReviewReadyRafts.ts b/rafts/frontend/src/actions/getReviewReadyRafts.ts new file mode 100644 index 0000000..85d90ec --- /dev/null +++ b/rafts/frontend/src/actions/getReviewReadyRafts.ts @@ -0,0 +1,158 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { RaftData } from '@/types/doi' +import { loadMockRaftData } from '@/tests/mock-data-loader' +import { useMockData } from '@/config/environment' + +export interface RaftApiResponse { + message: string + data: RaftData[] + meta: { + total: number + page: number + limit: number + totalPages: number + } +} + +export interface GetRaftsOptions { + page?: number + limit?: number + status?: string + search?: string +} + +export const getReviewReadyRafts = async (options: GetRaftsOptions = {}) => { + try { + // Use mock data if enabled + if (useMockData) { + const mockData = loadMockRaftData('review_ready') + const response: RaftApiResponse = { + message: 'Mock data loaded', + data: mockData, + meta: { + total: mockData.length, + page: options.page || 1, + limit: options.limit || 10, + totalPages: 1, + }, + } + return { success: true, data: response } + } + + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + // Build query parameters + const queryParams = new URLSearchParams() + if (options.page) queryParams.append('page', options.page.toString()) + if (options.limit) queryParams.append('limit', options.limit.toString()) + if (options.status) queryParams.append('status', options.status) + if (options.search) queryParams.append('search', options.search) + + const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '' + + // Make the API call with the access token as a Bearer token + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/rafts/review_ready${queryString}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error('Failed to fetch RAFTs:', errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const data: RaftApiResponse = await response.json() + return { success: true, data } + } catch (error) { + console.error('Error fetching RAFTs:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/getUserRafts.ts b/rafts/frontend/src/actions/getUserRafts.ts new file mode 100644 index 0000000..b699084 --- /dev/null +++ b/rafts/frontend/src/actions/getUserRafts.ts @@ -0,0 +1,151 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { RaftData } from '@/types/doi' + +export interface UserRaftsResponse { + success: boolean + data?: RaftData[] + error?: string + meta?: { + total: number + page: number + limit: number + totalPages: number + } +} + +/** + * Fetches all RAFTs associated with a specific user + * + * @param userId - Optional user ID to fetch RAFTs for (defaults to current authenticated user) + * @param options - Optional pagination and filtering options + * @returns Response containing RAFTs data or error information + */ +export const getUserRafts = async ( + userId?: string, + options: { page?: number; limit?: number; status?: string } = {}, +): Promise => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + // Determine user ID - either provided or current user + const targetUserId = userId || session.user?.id || 'current' + + // Build query parameters for pagination/filtering + const queryParams = new URLSearchParams() + if (options.page) queryParams.append('page', options.page.toString()) + if (options.limit) queryParams.append('limit', options.limit.toString()) + if (options.status) queryParams.append('status', options.status) + + const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '' + + // Make the API call with the access token as a Bearer token + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/rafts/user/${targetUserId}${queryString}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error(`Failed to fetch user RAFTs:`, errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + + return { + success: true, + data: responseData.data, + meta: responseData.meta, + } + } catch (error) { + console.error(`Error fetching user RAFTs:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/publishRaftDoi.ts b/rafts/frontend/src/actions/publishRaftDoi.ts new file mode 100644 index 0000000..8b8a57c --- /dev/null +++ b/rafts/frontend/src/actions/publishRaftDoi.ts @@ -0,0 +1,157 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { SUBMIT_DOI_URL, SUCCESS, MESSAGE } from '@/actions/constants' +import { IResponseData } from '@/actions/types' +import { updateRaftMetadata } from '@/services/canfarStorage' +import { RaftStatusChange } from '@/types/doi' + +/** + * Mints/Publishes a DOI for a RAFT submission via the DOI backend. + * Calls the dedicated /mint endpoint to trigger the minting workflow. + * + * Endpoint: POST /rafts/instances/{raftId}/mint + * + * @param raftId - The DOI identifier suffix (e.g., "RAFTS-7rtut-gkryn.test") + * @returns Response object with success status and message + */ +export const publishRAFTDOI = async ( + raftId: string, + options?: { + dataDirectory?: string + previousStatus?: string + }, +): Promise> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { + [SUCCESS]: false, + [MESSAGE]: 'Not authenticated', + } + } + + const url = `${SUBMIT_DOI_URL}/${raftId}/mint` + + const response = await fetch(url, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + }) + + const responseText = await response.text() + + if (!response.ok) { + return { + [SUCCESS]: false, + [MESSAGE]: `Failed to mint DOI: ${response.status} ${responseText}`, + } + } + + // Update RAFT.json metadata with status history after successful mint + if (options?.dataDirectory) { + try { + const statusChange: RaftStatusChange = { + fromStatus: options.previousStatus || 'approved', + toStatus: 'minted', + changedBy: session?.user?.name || '', + changedAt: new Date().toISOString(), + } + + await updateRaftMetadata( + options.dataDirectory, + { + updatedAt: new Date().toISOString(), + updatedBy: session?.user?.name || '', + statusHistory: [statusChange], + }, + accessToken, + ) + } catch (metaError) { + console.warn('[publishRAFTDOI] Metadata update failed (non-critical):', metaError) + } + } + + return { + [SUCCESS]: true, + [MESSAGE]: 'RAFT status changed to Published.', + } + } catch (error) { + console.error('[publishRAFTDOI] Error:', error) + + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/submitDOI.ts b/rafts/frontend/src/actions/submitDOI.ts new file mode 100644 index 0000000..68dcb85 --- /dev/null +++ b/rafts/frontend/src/actions/submitDOI.ts @@ -0,0 +1,161 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import convertToDataCite from '@/utilities/jsonToDataCite' +import { SUBMIT_DOI_URL, SUCCESS, MESSAGE } from '@/actions/constants' +import { createDoiFormData } from '@/actions/utils/doiFormData' +import { extractDOI } from '@/utilities/doiIdentifier' +import { uploadFile } from '@/services/canfarStorage' +import { TRaftContext } from '@/context/types' +import { IResponseData } from '@/actions/types' + +export const submitDOI = async (formData: TRaftContext): Promise> => { + const convertedJSON = convertToDataCite(formData) + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + console.error('[submitDOI] No access token available') + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + // DOI backend expects multipart form data with JSON blobs + const multipartFormData = createDoiFormData({ metaData: convertedJSON }) + + // Note: status defaults to DRAFT on creation (set by backend). + // PostAction handles both creation (POST without suffix) and update (POST with suffix). + + // POST without suffix = create new DOI. Backend returns 303 redirect on success. + const response = await fetch(SUBMIT_DOI_URL, { + method: 'POST', + headers: { + // Don't set Content-Type for FormData - browser will set it with boundary + Cookie: `CADC_SSO=${accessToken}`, + }, + body: multipartFormData, + redirect: 'manual', // Don't follow redirects - 303 means success + }) + + // 303 redirect means success - the Location header contains the new DOI URL + let identifier: string | undefined + + if (response.status === 303) { + const location = response.headers.get('Location') + identifier = location?.split('/').pop() + } else if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[submitDOI] Error response:', response.status, errorText) + return { + [SUCCESS]: false, + [MESSAGE]: `Request failed with status ${response.status}: ${errorText}`, + } + } else { + const data = await response.text() + identifier = data ? extractDOI(data)?.split('/')?.[1] : undefined + } + + if (!identifier) { + console.error('[submitDOI] Could not extract DOI identifier from response') + return { + [SUCCESS]: false, + [MESSAGE]: 'DOI created but could not determine identifier', + } + } + + // DOI created successfully - now upload RAFT.json with metadata (both must succeed) + const raftDataWithMeta: TRaftContext = { + ...formData, + createdBy: session?.user?.name || '', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + updatedBy: session?.user?.name || '', + version: 1, + statusHistory: [], + } + const uploadResult = await uploadFile(identifier, raftDataWithMeta, accessToken) + if (uploadResult.error) { + console.error('[submitDOI] RAFT.json upload failed:', uploadResult.error.message) + return { + [SUCCESS]: false, + [MESSAGE]: `DOI created (${identifier}) but RAFT data save failed: ${uploadResult.error.message}`, + } + } + + return { [SUCCESS]: true, data: identifier } + } catch (error) { + console.error('[submitDOI] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/submitForReview.ts b/rafts/frontend/src/actions/submitForReview.ts new file mode 100644 index 0000000..5396f0c --- /dev/null +++ b/rafts/frontend/src/actions/submitForReview.ts @@ -0,0 +1,280 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { SUBMIT_DOI_URL, MESSAGE, SUCCESS } from '@/actions/constants' +import { createDoiFormData } from '@/actions/utils/doiFormData' +import { IResponseData } from '@/actions/types' +import { BACKEND_STATUS } from '@/shared/backendStatus' +import { updateRaftMetadata, downloadRaftFile } from '@/services/canfarStorage' +import { RaftStatusChange } from '@/types/doi' + +/** + * Submits a RAFT for review by changing its status from 'in progress' to 'review ready' + * + * Per backend API: To update status, POST a plain JSON object like { status: "review ready" } + * to /rafts/ (which is /doi/instances/) + * + * The workflow is: + * - Author submits: in progress → review ready + * - Reviewer claims: review ready → in review (and assigns reviewer) + * + * @param doiId - The DOI identifier (e.g., 'RAFTS-0001') + * @returns Response object with success status + */ +export const submitForReview = async ( + doiId: string, + dataDirectory?: string, +): Promise> => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + console.error('[submitForReview] No access token available') + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + const url = `${SUBMIT_DOI_URL}/${doiId}` + + // Backend expects multipart form data with JSON blob labeled 'doiNodeData' + const formData = createDoiFormData({ nodeData: { status: BACKEND_STATUS.REVIEW_READY } }) + + const response = await fetch(url, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + body: formData, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[submitForReview] Error response:', response.status, errorText) + return { + [SUCCESS]: false, + [MESSAGE]: `Failed to submit for review: ${response.status} ${errorText}`, + } + } + + // Update RAFT.json with submittedAt, statusHistory, and version + if (dataDirectory) { + try { + // Fetch current RAFT.json to get actual status and history + const currentRaft = await downloadRaftFile(dataDirectory, accessToken) + const existing = ( + currentRaft.success && currentRaft.data ? currentRaft.data : {} + ) as Record + const existingHistory = (existing.statusHistory as RaftStatusChange[]) || [] + const lastEntry = existingHistory[existingHistory.length - 1] + + // Skip metadata update if already in "review ready" state (prevents duplicate entries) + if (lastEntry && lastEntry.toStatus === BACKEND_STATUS.REVIEW_READY) { + console.info('[submitForReview] Already review ready, skipping history update') + } else { + const metaUpdate: Record = { + submittedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + updatedBy: session?.user?.name || '', + } + + // Use actual current status as fromStatus instead of hardcoding + const actualFromStatus = lastEntry?.toStatus || BACKEND_STATUS.IN_PROGRESS + + const statusChange: RaftStatusChange = { + fromStatus: actualFromStatus, + toStatus: BACKEND_STATUS.REVIEW_READY, + changedBy: session?.user?.name || '', + changedAt: new Date().toISOString(), + } + + metaUpdate.statusHistory = [statusChange] + + // Increment version if this is a resubmission (has prior history) + if (existingHistory.length > 0) { + metaUpdate.version = ((existing.version as number) || 1) + 1 + } + + await updateRaftMetadata(dataDirectory, metaUpdate, accessToken) + } + } catch (metaError) { + console.warn('[submitForReview] Metadata update failed (non-critical):', metaError) + } + } + + return { + [SUCCESS]: true, + data: 'RAFT submitted for review successfully', + } + } catch (error) { + console.error('[submitForReview] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} + +/** + * Reverts a RAFT from review back to draft status + * + * Per backend API: POST plain JSON { status: "in progress" } to /rafts/ + * + * @param doiId - The DOI identifier (e.g., 'RAFTS-0001') + * @returns Response object with success status + */ +export const revertToDraft = async ( + doiId: string, + dataDirectory?: string, + previousStatus?: string, +): Promise> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + console.error('[revertToDraft] No access token available') + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + const url = `${SUBMIT_DOI_URL}/${doiId}` + + // Backend expects multipart form data with JSON blob labeled 'doiNodeData' + const formData = createDoiFormData({ nodeData: { status: BACKEND_STATUS.IN_PROGRESS } }) + + const response = await fetch(url, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + body: formData, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[revertToDraft] Error response:', response.status, errorText) + return { + [SUCCESS]: false, + [MESSAGE]: `Failed to revert to draft: ${response.status} ${errorText}`, + } + } + + // Update RAFT.json metadata with status history + if (dataDirectory) { + try { + // Fetch current RAFT.json to get actual status + const currentRaft = await downloadRaftFile(dataDirectory, accessToken) + const existing = ( + currentRaft.success && currentRaft.data ? currentRaft.data : {} + ) as Record + const existingHistory = (existing.statusHistory as RaftStatusChange[]) || [] + const lastEntry = existingHistory[existingHistory.length - 1] + + // Skip if already in "in progress" state + if (lastEntry && lastEntry.toStatus === BACKEND_STATUS.IN_PROGRESS) { + console.info('[revertToDraft] Already in progress, skipping history update') + } else { + // Use actual current status as fromStatus + const actualFromStatus = lastEntry?.toStatus || previousStatus || 'unknown' + + const statusChange: RaftStatusChange = { + fromStatus: actualFromStatus, + toStatus: BACKEND_STATUS.IN_PROGRESS, + changedBy: session?.user?.name || '', + changedAt: new Date().toISOString(), + } + + await updateRaftMetadata( + dataDirectory, + { + updatedAt: new Date().toISOString(), + updatedBy: session?.user?.name || '', + statusHistory: [statusChange], + }, + accessToken, + ) + } + } catch (metaError) { + console.warn('[revertToDraft] Metadata update failed (non-critical):', metaError) + } + } + + return { + [SUCCESS]: true, + data: 'RAFT reverted to draft successfully', + } + } catch (error) { + console.error('[revertToDraft] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/submitRaft.ts b/rafts/frontend/src/actions/submitRaft.ts new file mode 100644 index 0000000..187dcf6 --- /dev/null +++ b/rafts/frontend/src/actions/submitRaft.ts @@ -0,0 +1,130 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { TRaftSubmission } from '@/shared/model' + +export interface SubmitRaftOptions { + generateForumPost?: boolean + relatedRafts?: string[] +} + +/** + * Server action to submit a new RAFT to the backend API + * + * @param {SubmitRaftOptions} options - Additional options for RAFT submission + * @param {TRaftSubmission} raftData - The RAFT data to submit + * @returns {Promise<{success: boolean, data?: any, error?: string}>} + */ +export const submitRaft = async (raftData: TRaftSubmission, options: SubmitRaftOptions = {}) => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + // Prepare the payload for submission + const payload = { + ...raftData, + generateForumPost: options.generateForumPost ?? true, // Default to true + relatedRafts: options.relatedRafts ?? [], + } + // Make the API call with the access token as a Bearer token + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/rafts/create`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'RAFT-System/1.0', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(payload), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error('Failed to submit RAFT:', errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + return { success: true, data: responseData.data } + } catch (error) { + console.error('Error submitting RAFT:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/submitReviewComment.ts b/rafts/frontend/src/actions/submitReviewComment.ts new file mode 100644 index 0000000..b41fbe7 --- /dev/null +++ b/rafts/frontend/src/actions/submitReviewComment.ts @@ -0,0 +1,147 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { RaftReview } from '@/types/reviews' + +interface AddCommentParams { + content: string + location?: string +} + +/** + * Adds a comment to a RAFT review + * + * @param reviewId - The ID of the review to add a comment to + * @param params - Comment parameters (content and optional location) + * @returns Response with success status and updated review data + */ +export const submitReviewComment = async ( + reviewId: string, + params: AddCommentParams, +): Promise<{ + success: boolean + data?: RaftReview + error?: string +}> => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + // Validate required fields + if (!params.content || params.content.trim() === '') { + return { success: false, error: 'Comment content is required' } + } + + // Prepare the request payload + const payload = { + content: params.content, + ...(params.location && { location: params.location }), + } + + // Make the API call with the access token as a Bearer token + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/reviews/${reviewId}/comments`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(payload), + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error(`Failed to add comment to review ${reviewId}:`, errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + return { + success: true, + data: responseData.data, + } + } catch (error) { + console.error(`Error adding comment to review ${reviewId}:`, error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/types.ts b/rafts/frontend/src/actions/types.ts new file mode 100644 index 0000000..1929249 --- /dev/null +++ b/rafts/frontend/src/actions/types.ts @@ -0,0 +1,77 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { DATA, MESSAGE, SUCCESS } from '@/actions/constants' + +export interface IResponse { + [SUCCESS]: boolean + [MESSAGE]?: string +} + +export interface IResponseData extends IResponse { + [DATA]?: T +} diff --git a/rafts/frontend/src/actions/updateDOI.ts b/rafts/frontend/src/actions/updateDOI.ts new file mode 100644 index 0000000..43282ad --- /dev/null +++ b/rafts/frontend/src/actions/updateDOI.ts @@ -0,0 +1,220 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import convertToDataCite from '@/utilities/jsonToDataCite' +import { SUBMIT_DOI_URL, MESSAGE, SUCCESS } from '@/actions/constants' +import { createDoiFormData } from '@/actions/utils/doiFormData' +import { uploadFile, downloadRaftFile } from '@/services/canfarStorage' +import { TRaftContext } from '@/context/types' +import { IResponseData } from '@/actions/types' +import { OPTION_REVIEW, OPTION_DRAFT } from '@/shared/constants' +import { BACKEND_STATUS } from '@/shared/backendStatus' +import { RaftStatusChange } from '@/types/doi' + +// Map frontend status to backend status +// When author submits for review, status becomes "review ready" (not "in review") +// "in review" is set when a publisher claims the RAFT for review +const getBackendStatus = (frontendStatus?: string): string => { + switch (frontendStatus) { + case OPTION_REVIEW: + return BACKEND_STATUS.REVIEW_READY + case OPTION_DRAFT: + return BACKEND_STATUS.IN_PROGRESS + default: + return BACKEND_STATUS.IN_PROGRESS + } +} + +export const updateDOI = async ( + formData: TRaftContext, + id: string, +): Promise> => { + const convertedJSON = convertToDataCite(formData) + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + console.error('[updateDOI] No access token available') + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + // DOI backend expects multipart form data with JSON blobs + const statusNodeData = formData.generalInfo?.status + ? { status: getBackendStatus(formData.generalInfo.status) } + : undefined + const multipartFormData = createDoiFormData({ + metaData: convertedJSON, + nodeData: statusNodeData, + }) + + const url = `${SUBMIT_DOI_URL}/${id}` + + // Make the API call with the access token as a cookie (DOI expects cookie auth) + // DOI backend returns 303 redirect on success - don't auto-follow + const response = await fetch(url, { + method: 'POST', + headers: { + // Don't set Content-Type for FormData - browser will set it with boundary + Cookie: `CADC_SSO=${accessToken}`, + }, + body: multipartFormData, + redirect: 'manual', // Don't follow redirects - 303 means success + }) + + // 303 redirect means success - the backend processed the update + if (response.status === 303) { + // success, no further action needed + } else if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[updateDOI] Error response:', response.status, errorText) + return { + [SUCCESS]: false, + [MESSAGE]: `Request failed with status ${response.status}: ${errorText}`, + } + } + + // DOI XML updated successfully - now upload RAFT.json (both must succeed) + const now = new Date().toISOString() + const userName = session?.user?.name || '' + const isSubmittingForReview = formData.generalInfo?.status === OPTION_REVIEW + + // Download fresh RAFT.json to get actual statusHistory and version + // (the form context data may be stale if status changes happened while editing) + let freshHistory: RaftStatusChange[] = [] + let freshVersion: number = (formData.version as number) || 1 + if (formData.dataDirectory) { + try { + const freshRaft = await downloadRaftFile(formData.dataDirectory, accessToken) + if (freshRaft.success && freshRaft.data) { + const freshData = freshRaft.data as Record + freshHistory = (freshData.statusHistory as RaftStatusChange[]) || [] + freshVersion = (freshData.version as number) || 1 + } + } catch { + // Fall back to context data if download fails + freshHistory = (formData.statusHistory as RaftStatusChange[]) || [] + } + } else { + freshHistory = (formData.statusHistory as RaftStatusChange[]) || [] + } + + const updatedFormData: TRaftContext = { + ...formData, + updatedAt: now, + updatedBy: userName, + statusHistory: freshHistory, + version: freshVersion, + } + + // When submitting for review via the form, add statusHistory + version bump + if (isSubmittingForReview) { + const lastEntry = freshHistory[freshHistory.length - 1] + const actualFromStatus = lastEntry?.toStatus || BACKEND_STATUS.IN_PROGRESS + + const statusChange: RaftStatusChange = { + fromStatus: actualFromStatus, + toStatus: BACKEND_STATUS.REVIEW_READY, + changedBy: userName, + changedAt: now, + } + + // Only add history entry if not already in review_ready state + if (!lastEntry || lastEntry.toStatus !== BACKEND_STATUS.REVIEW_READY) { + updatedFormData.statusHistory = [...freshHistory, statusChange] + } + + updatedFormData.submittedAt = now + + // Bump version on resubmission (has prior history = was submitted before) + if (freshHistory.length > 0) { + updatedFormData.version = freshVersion + 1 + } + } + + const uploadResult = await uploadFile(id, updatedFormData, accessToken) + if (uploadResult.error) { + console.error('[updateDOI] RAFT.json upload failed:', uploadResult.error.message) + return { + [SUCCESS]: false, + [MESSAGE]: `DOI metadata updated but RAFT data save failed: ${uploadResult.error.message}`, + } + } + + return { [SUCCESS]: true, data: id } + } catch (error) { + console.error('[updateDOI] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/updateDOIStatus.ts b/rafts/frontend/src/actions/updateDOIStatus.ts new file mode 100644 index 0000000..2efe047 --- /dev/null +++ b/rafts/frontend/src/actions/updateDOIStatus.ts @@ -0,0 +1,184 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { SUBMIT_DOI_URL, SUCCESS, MESSAGE } from '@/actions/constants' +import { createDoiFormData } from '@/actions/utils/doiFormData' +import { IResponseData } from '@/actions/types' +import { BackendStatusType, getStatusDisplayName } from '@/shared/backendStatus' +import { downloadRaftFile, updateRaftMetadata } from '@/services/canfarStorage' +import { RaftStatusChange } from '@/types/doi' +import { BACKEND_STATUS } from '@/shared/backendStatus' + +/** + * Updates the status of a RAFT/DOI via the DOI backend service. + * Optionally updates RAFT.json metadata (statusHistory, version) when dataDirectory is provided. + * + * @param doiId - The DOI identifier suffix (e.g., "RAFTS-7rtut-gkryn.test") + * @param newStatus - The new backend status value + * @param options - Optional: dataDirectory for metadata update, previousStatus for history + * @returns Response with success/error information + */ +export const updateDOIStatus = async ( + doiId: string, + newStatus: BackendStatusType, + options?: { + dataDirectory?: string + previousStatus?: string + }, +): Promise> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + const url = `${SUBMIT_DOI_URL}/${doiId}` + + // Backend expects multipart form data with JSON blob labeled 'doiNodeData' + const formData = createDoiFormData({ nodeData: { status: newStatus } }) + + const response = await fetch(url, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + body: formData, + }) + + const responseText = await response.text() + + if (!response.ok) { + return { + [SUCCESS]: false, + [MESSAGE]: `Failed to update status: ${response.status} ${responseText}`, + } + } + + // After successful backend status change, update RAFT.json metadata if dataDirectory provided + if (options?.dataDirectory) { + try { + // Fetch current RAFT.json to get actual status for accurate fromStatus + const currentRaft = await downloadRaftFile(options.dataDirectory, accessToken) + const existing = ( + currentRaft.success && currentRaft.data ? currentRaft.data : {} + ) as Record + const existingHistory = (existing.statusHistory as RaftStatusChange[]) || [] + const lastEntry = existingHistory[existingHistory.length - 1] + + // Skip if already in the target state (prevents duplicate entries) + if (lastEntry && lastEntry.toStatus === newStatus) { + console.info(`[updateDOIStatus] Already "${newStatus}", skipping history update`) + } else { + // Use actual current status from history, fall back to passed previousStatus + const actualFromStatus = lastEntry?.toStatus || options.previousStatus || 'unknown' + + const statusChange: RaftStatusChange = { + fromStatus: actualFromStatus, + toStatus: newStatus, + changedBy: session?.user?.name || '', + changedAt: new Date().toISOString(), + } + + const metaUpdate: Record = { + updatedAt: new Date().toISOString(), + updatedBy: session?.user?.name || '', + } + + metaUpdate.statusHistory = [statusChange] + + if (newStatus === BACKEND_STATUS.REVIEW_READY) { + metaUpdate.submittedAt = new Date().toISOString() + if (existingHistory.length > 0) { + metaUpdate.version = ((existing.version as number) || 1) + 1 + } + } + + await updateRaftMetadata(options.dataDirectory, metaUpdate, accessToken) + } + } catch (metaError) { + console.warn('[updateDOIStatus] Metadata update failed (non-critical):', metaError) + } + } + + return { + [SUCCESS]: true, + [MESSAGE]: `RAFT status changed to ${getStatusDisplayName(newStatus)}.`, + data: responseText, + } + } catch (error) { + console.error('[updateDOIStatus] Error:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/updateDOIXml.ts b/rafts/frontend/src/actions/updateDOIXml.ts new file mode 100644 index 0000000..680c05e --- /dev/null +++ b/rafts/frontend/src/actions/updateDOIXml.ts @@ -0,0 +1,185 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { SUBMIT_DOI_URL, MESSAGE, SUCCESS } from '@/actions/constants' +import { createDoiFormData } from '@/actions/utils/doiFormData' +import { IResponseData } from '@/actions/types' + +/** + * Fetches the XML for a specific DOI instance + */ +export const getDOIXml = async (doiSuffix: string): Promise> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + console.error('[getDOIXml] No access token available') + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + const url = `${SUBMIT_DOI_URL}/${doiSuffix}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/xml', + Cookie: `CADC_SSO=${accessToken}`, + }, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[getDOIXml] Error response:', response.status, errorText) + return { + [SUCCESS]: false, + [MESSAGE]: `Request failed with status ${response.status}: ${errorText}`, + } + } + + const xmlString = await response.text() + + return { [SUCCESS]: true, data: xmlString } + } catch (error) { + console.error('[getDOIXml] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} + +/** + * Updates a DOI instance via POST with multipart form data + * Backend expects: doiMetaData (DataCite JSON) and/or doiNodeData (node properties JSON) + * + * Updatable fields in doiMetaData: creators, titles, publicationYear, language + * Updatable fields in doiNodeData: journalRef, status, reviewer + */ +export const updateDOIXml = async ( + doiSuffix: string, + doiMetaData?: object, + doiNodeData?: { journalRef?: string; status?: string; reviewer?: string }, +): Promise> => { + try { + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + console.error('[updateDOIXml] No access token available') + return { [SUCCESS]: false, [MESSAGE]: 'Not authenticated' } + } + + const url = `${SUBMIT_DOI_URL}/${doiSuffix}` + + // Backend expects multipart form data with JSON blobs + const formData = createDoiFormData({ + metaData: doiMetaData || undefined, + nodeData: doiNodeData || undefined, + }) + + const response = await fetch(url, { + method: 'POST', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + body: formData, + redirect: 'manual', // Backend returns 303 on success + }) + + // 303 redirect means success + if (response.status === 303) { + const location = response.headers.get('Location') + return { [SUCCESS]: true, data: location || 'Updated successfully' } + } + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + console.error('[updateDOIXml] Error response:', response.status, errorText) + return { + [SUCCESS]: false, + [MESSAGE]: `Request failed with status ${response.status}: ${errorText}`, + } + } + + const responseText = await response.text() + + return { [SUCCESS]: true, data: responseText } + } catch (error) { + console.error('[updateDOIXml] Exception:', error) + return { + [SUCCESS]: false, + [MESSAGE]: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} + +// Note: modifyDataDirectoryInXml has been moved to @/utilities/xmlParser +// Import it from there: import { modifyDataDirectoryInXml } from '@/utilities/xmlParser' diff --git a/rafts/frontend/src/actions/updateRaft.ts b/rafts/frontend/src/actions/updateRaft.ts new file mode 100644 index 0000000..c82beef --- /dev/null +++ b/rafts/frontend/src/actions/updateRaft.ts @@ -0,0 +1,140 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { RaftData } from '@/types/doi' + +export interface UpdateRaftOptions { + generateForumPost?: boolean + relatedRafts?: string[] +} + +/** + * Server action to update an existing RAFT in the backend API + * + * @param {string} raftId - The ID of the RAFT to update + * @param {TRaftSubmission} raftData - The updated RAFT data + * @param {UpdateRaftOptions} options - Additional options for RAFT update + * @returns {Promise<{success: boolean, data?: any, error?: string}>} + */ +export const updateRaft = async ( + raftId: string, + raftData: RaftData, + options: UpdateRaftOptions = {}, +) => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + if (!raftId) { + return { success: false, error: 'RAFT ID is required for updates' } + } + + // Prepare the payload for submission + const payload = { + ...raftData, + generateForumPost: options.generateForumPost ?? false, // Default to false for updates + relatedRafts: options.relatedRafts ?? [], + } + + // Make the API call with the access token as a Bearer token + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/rafts/${raftId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'RAFT-System/1.0', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(payload), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error(`Failed to update RAFT ${raftId}:`, errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + return { success: true, data: responseData.data } + } catch (error) { + console.error('Error updating RAFT:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/updateRaftStatus.ts b/rafts/frontend/src/actions/updateRaftStatus.ts new file mode 100644 index 0000000..89d31c5 --- /dev/null +++ b/rafts/frontend/src/actions/updateRaftStatus.ts @@ -0,0 +1,165 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' +import { useMockData } from '@/config/environment' +import { updateMockRaftStatus } from '@/tests/mock-data-loader' + +interface UpdateRaftStatusResponse { + success: boolean + message?: string + error?: string +} + +/** + * Updates the status of a RAFT submission + * + * @param raftId - The ID of the RAFT to update + * @param newStatus - The new status to set for the RAFT + * @param comment - Optional comment about the status change + * @returns Response object with success status and message + */ +export const updateRaftStatus = async ( + raftId: string, + newStatus: string, + comment?: string, +): Promise => { + try { + // Use mock data if enabled + if (useMockData) { + const result = updateMockRaftStatus(raftId, newStatus) + + if (result.success) { + return { + success: true, + message: `Status updated to ${newStatus} (mock)`, + } + } else { + return { + success: false, + error: result.error || 'Failed to update status', + } + } + } + + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return { + success: false, + error: 'Not authenticated', + } + } + + // Prepare the request payload + const payload = { + status: newStatus, + ...(comment && { comment }), + } + + // Make the API call to update the RAFT status + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/rafts/${raftId}/status`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(payload), + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error(`Failed to update RAFT status:`, errorData) + + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const data = await response.json() + + return { + success: true, + message: data.message || 'Status updated successfully', + } + } catch (error) { + console.error('Error updating RAFT status:', error) + + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/user/changeUserRole.ts b/rafts/frontend/src/actions/user/changeUserRole.ts new file mode 100644 index 0000000..b8c776c --- /dev/null +++ b/rafts/frontend/src/actions/user/changeUserRole.ts @@ -0,0 +1,146 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' + +interface ChangeRoleResponse { + success: boolean + error?: string + message?: string +} + +/** + * Server action to change a user's role + * Requires admin role + * + * @param {string} userId - ID of the user to update + * @param {string} newRole - New role to assign + * @returns {Promise} + */ +export const changeUserRole = async ( + userId: string, + newRole: string, +): Promise => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + const userRole = session?.user?.role + // Check if user is authenticated + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + // Verify admin role + if (userRole !== 'admin') { + return { success: false, error: 'Unauthorized. Admin role required.' } + } + + // Validate role + const validRoles = ['admin', 'reviewer', 'contributor'] + if (!validRoles.includes(newRole)) { + return { success: false, error: 'Invalid role specified' } + } + + // Make the API call to update the user role + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/${userId}/role`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'RAFT-System/1.0', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ role: newRole }), + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error('Failed to change user role:', errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + return { + success: true, + message: responseData.message || `User role updated to ${newRole}`, + } + } catch (error) { + console.error('Error changing user role:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/user/getUsers.ts b/rafts/frontend/src/actions/user/getUsers.ts new file mode 100644 index 0000000..add6608 --- /dev/null +++ b/rafts/frontend/src/actions/user/getUsers.ts @@ -0,0 +1,172 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' + +export interface User { + _id: string + firstName: string + lastName: string + email: string + affiliation?: string + role: string + isEmailVerified: boolean + isActive: boolean + createdAt: string + updatedAt: string +} + +export interface GetUsersResponse { + success: boolean + data?: User[] + error?: string + meta?: { + total: number + page: number + limit: number + totalPages: number + } +} + +export interface GetUsersOptions { + page?: number + limit?: number + search?: string + role?: string +} + +/** + * Server action to fetch all users from the backend API + * Requires admin role + * + * @param {GetUsersOptions} options - Options for pagination, filtering, etc. + * @returns {Promise} + */ +export const getUsers = async (options: GetUsersOptions = {}): Promise => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + const userRole = session?.user?.role + + // Check if user is authenticated + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + + // Verify admin role + if (userRole !== 'admin') { + return { success: false, error: 'Unauthorized. Admin role required.' } + } + + // Build query parameters + const queryParams = new URLSearchParams() + if (options.page) queryParams.append('page', options.page.toString()) + if (options.limit) queryParams.append('limit', options.limit.toString()) + if (options.search) queryParams.append('search', options.search) + if (options.role) queryParams.append('role', options.role) + + const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '' + + // Make the API call with the access token as a Bearer token + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users${queryString}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'RAFT-System/1.0', + Authorization: `Bearer ${accessToken}`, + }, + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error('Failed to fetch users:', errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + return { + success: true, + data: responseData.data, + meta: responseData.meta, + } + } catch (error) { + console.error('Error fetching users:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/user/registerUser.ts b/rafts/frontend/src/actions/user/registerUser.ts new file mode 100644 index 0000000..512c2df --- /dev/null +++ b/rafts/frontend/src/actions/user/registerUser.ts @@ -0,0 +1,112 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +interface RegisterFormValues { + firstName: string + lastName: string + email: string + password: string + affiliation?: string +} + +/** + * Server action for user registration + */ +export const registerUser = async (formData: RegisterFormValues) => { + try { + // Make API call to backend registration endpoint + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }) + + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || 'Registration failed', + } + } + + return { + success: true, + message: 'Registration successful. Please check your email to verify your account.', + } + } catch (error) { + console.error('Registration error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/user/requestPasswordReset.ts b/rafts/frontend/src/actions/user/requestPasswordReset.ts new file mode 100644 index 0000000..80d4584 --- /dev/null +++ b/rafts/frontend/src/actions/user/requestPasswordReset.ts @@ -0,0 +1,111 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +interface RequestPasswordResetValues { + email: string +} + +/** + * Server action for requesting a password reset + */ +export const requestPasswordReset = async (formData: RequestPasswordResetValues) => { + try { + // Make API call to backend password reset endpoint + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/request-password-reset`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData), + }, + ) + + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || 'Password reset request failed', + } + } + + return { + success: true, + message: 'If your email is registered, you will receive a password reset link', + } + } catch (error) { + console.error('Password reset request error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/user/resetPassword.ts b/rafts/frontend/src/actions/user/resetPassword.ts new file mode 100644 index 0000000..c707bae --- /dev/null +++ b/rafts/frontend/src/actions/user/resetPassword.ts @@ -0,0 +1,113 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// src/actions/user/resetPassword.ts +'use server' + +interface ResetPasswordData { + token: string + newPassword: string +} + +/** + * Resets a user's password using the reset token + */ +export const resetPassword = async ({ token, newPassword }: ResetPasswordData) => { + try { + // Call the API endpoint to reset the password + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/reset-password`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token, newPassword }), + }, + ) + + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || 'Password reset failed', + } + } + + return { + success: true, + message: 'Password has been reset successfully. You can now sign in.', + } + } catch (error) { + console.error('Password reset error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/user/toggleUserStatus.ts b/rafts/frontend/src/actions/user/toggleUserStatus.ts new file mode 100644 index 0000000..80db16c --- /dev/null +++ b/rafts/frontend/src/actions/user/toggleUserStatus.ts @@ -0,0 +1,142 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { auth } from '@/auth/cadc-auth/credentials' + +interface ToggleStatusResponse { + success: boolean + error?: string + message?: string +} + +/** + * Server action to toggle a user's active status (lock/unlock) + * Requires admin role + * + * @param {string} userId - ID of the user to update + * @param {boolean} isActive - Whether to activate (true) or deactivate (false) the user + * @returns {Promise} + */ +export const toggleUserStatus = async ( + userId: string, + isActive: boolean, +): Promise => { + try { + // Get the session with the access token + const session = await auth() + const accessToken = session?.accessToken + const userRoles = session?.user?.role + + // Check if user is authenticated + if (!accessToken) { + return { success: false, error: 'Not authenticated' } + } + const isAdmin = userRoles && userRoles === 'admin' + // Verify admin role + if (!isAdmin) { + return { success: false, error: 'Unauthorized. Admin role required.' } + } + + // Make the API call to toggle the user status + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/${userId}/status`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'RAFT-System/1.0', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ isActive }), + }, + ) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + console.error(`Failed to ${isActive ? 'activate' : 'deactivate'} user:`, errorData) + return { + success: false, + error: errorData.message || `Request failed with status ${response.status}`, + } + } + + const responseData = await response.json() + return { + success: true, + message: + responseData.message || `User ${isActive ? 'activated' : 'deactivated'} successfully`, + } + } catch (error) { + console.error('Error toggling user status:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/user/verifyEmail.ts b/rafts/frontend/src/actions/user/verifyEmail.ts new file mode 100644 index 0000000..00990c2 --- /dev/null +++ b/rafts/frontend/src/actions/user/verifyEmail.ts @@ -0,0 +1,106 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +/** + * Verifies a user's email address by token + */ +export const verifyEmail = async (token: string) => { + try { + // Call the API endpoint to verify the email + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/verify-email/${token}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + + const data = await response.json() + + if (!response.ok) { + return { + success: false, + error: data.message || 'Email verification failed', + } + } + + return { + success: true, + message: 'Email verified successfully. You can now sign in.', + } + } catch (error) { + console.error('Email verification error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'An unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/actions/utils/doiFormData.ts b/rafts/frontend/src/actions/utils/doiFormData.ts new file mode 100644 index 0000000..15306c8 --- /dev/null +++ b/rafts/frontend/src/actions/utils/doiFormData.ts @@ -0,0 +1,81 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ +export function createDoiFormData(options?: { metaData?: object; nodeData?: object }): FormData { + const formData = new FormData() + + if (options?.metaData) { + const blob = new Blob([JSON.stringify(options.metaData)], { type: 'application/json' }) + formData.append('doiMetaData', blob) + } + + if (options?.nodeData) { + const blob = new Blob([JSON.stringify(options.nodeData)], { type: 'application/json' }) + formData.append('doiNodeData', blob) + } + + return formData +} diff --git a/rafts/frontend/src/app/[locale]/[...not_found]/page.tsx b/rafts/frontend/src/app/[locale]/[...not_found]/page.tsx new file mode 100644 index 0000000..ee317ce --- /dev/null +++ b/rafts/frontend/src/app/[locale]/[...not_found]/page.tsx @@ -0,0 +1,76 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { notFound } from 'next/navigation' + +/** + * Catch-all route to handle any unmatched paths within the [locale] segment. + * This triggers the custom not-found.tsx page instead of the default Next.js 404. + */ +export default function CatchAllPage() { + notFound() +} diff --git a/rafts/frontend/src/app/[locale]/admin/page.tsx b/rafts/frontend/src/app/[locale]/admin/page.tsx new file mode 100644 index 0000000..447284c --- /dev/null +++ b/rafts/frontend/src/app/[locale]/admin/page.tsx @@ -0,0 +1,72 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import ManageUsers from '@/components/User/management/ManageUsers' + +const ManageUsersPage = () => + +export default ManageUsersPage diff --git a/rafts/frontend/src/app/[locale]/form/create/page.tsx b/rafts/frontend/src/app/[locale]/form/create/page.tsx new file mode 100644 index 0000000..b2176a9 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/form/create/page.tsx @@ -0,0 +1,95 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { RaftFormProvider } from '@/context/RaftFormContext' +import FormLayoutWithContext from '@/components/Form/FormLayoutWithContext' +import { auth } from '@/auth/cadc-auth/credentials' +import { redirect } from 'next/navigation' +import { PROP_GENERAL_INFO, PROP_POST_OPT_OUT, PROP_STATUS, PROP_TITLE } from '@/shared' + +const CreateRAFT = async () => { + const session = await auth() + if (!session) { + redirect('/login') + } + + return ( + + + + ) +} + +export default CreateRAFT diff --git a/rafts/frontend/src/app/[locale]/form/edit/[id]/loading.tsx b/rafts/frontend/src/app/[locale]/form/edit/[id]/loading.tsx new file mode 100644 index 0000000..531a908 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/form/edit/[id]/loading.tsx @@ -0,0 +1,143 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Skeleton, Box } from '@mui/material' + +export default function Loading() { + return ( +
+ {/* Breadcrumbs skeleton */} + + + {/* Title and buttons row */} +
+ + + + + +
+ + {/* Step navigation skeleton */} + + + {[...Array(6)].map((_, i) => ( + + + {i < 5 && } + + ))} + + + {[...Array(6)].map((_, i) => ( + + ))} + + + + {/* Form content skeleton */} + + {/* Section title */} + + + {/* Form fields */} + + + + + + + + + + + + + + + + + + + + + + + {/* Action buttons */} + + + + + + + + +
+ ) +} diff --git a/rafts/frontend/src/app/[locale]/form/edit/[id]/page.tsx b/rafts/frontend/src/app/[locale]/form/edit/[id]/page.tsx new file mode 100644 index 0000000..29cd23b --- /dev/null +++ b/rafts/frontend/src/app/[locale]/form/edit/[id]/page.tsx @@ -0,0 +1,112 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Metadata } from 'next' + +import { notFound } from 'next/navigation' +import FormLayoutWithContext from '@/components/Form/FormLayoutWithContext' +import { RaftFormProvider } from '@/context/RaftFormContext' +import { getDOIRaft } from '@/actions/getDOIRAFT' + +export async function generateMetadata(props: { + params: Promise<{ id: string }> +}): Promise { + // Fetch RAFT data for metadata + const params = await props.params + const { success, data } = await getDOIRaft(params?.id) + + if (!success || !data) { + return { + title: 'RAFT Not Found', + } + } + + return { + title: `RAFT - ${data?.generalInfo?.title || 'View RAFT'}`, + description: + data.observationInfo?.abstract?.substring(0, 160) || + 'Research Announcement For The Solar System', + } +} + +export default async function RaftPage(props: { params: Promise<{ id: string }> }) { + // Fetch the RAFT data + const params = await props.params + + const { success, data } = await getDOIRaft(params.id) + + // Handle not found + if (!success || !data) { + notFound() + } + + return ( + + + + ) +} diff --git a/rafts/frontend/src/app/[locale]/layout.tsx b/rafts/frontend/src/app/[locale]/layout.tsx new file mode 100644 index 0000000..c8ce699 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/layout.tsx @@ -0,0 +1,88 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { LayoutProps } from '@/types/common' +import { NextIntlClientProvider } from 'next-intl' +import { getMessages } from 'next-intl/server' +import { notFound } from 'next/navigation' +import { routing } from '@/i18n/routing' +import AppLayout from '@/components/Layout/AppLayout' + +const LangLayout = async ({ children, params }: LayoutProps) => { + const { locale } = await params + if (!routing.locales.includes(locale as 'en' | 'fr')) { + notFound() + } + const messages = await getMessages() + return ( + + {children} + + ) +} + +export default LangLayout diff --git a/rafts/frontend/src/app/[locale]/login-required/page.tsx b/rafts/frontend/src/app/[locale]/login-required/page.tsx new file mode 100644 index 0000000..d3bb4ea --- /dev/null +++ b/rafts/frontend/src/app/[locale]/login-required/page.tsx @@ -0,0 +1,155 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useSearchParams } from 'next/navigation' +import { useRouter } from '@/i18n/routing' +import { Box, Container, Paper, Typography, Button, Divider } from '@mui/material' +import { LogIn, Home, Eye, FileText } from 'lucide-react' +import { useTranslations } from 'next-intl' + +export default function LoginRequiredPage() { + const searchParams = useSearchParams() + const router = useRouter() + const t = useTranslations('auth') + const returnUrl = searchParams.get('returnUrl') || '/' + + const handleLogin = () => { + router.push(`/login?returnUrl=${encodeURIComponent(returnUrl)}`) + } + + const handleHome = () => { + router.push('/') + } + + const handlePublicRafts = () => { + router.push('/public-view/rafts') + } + + return ( + + + + + + + + {t('login_required_title')} + + + + {t('login_required_message')} + + + + + + + {t('or_browse')} + + + + + {t('explore_without_login')} + + + + + + + + + + + + {t('requested_page')}: {returnUrl} + + + + + ) +} diff --git a/rafts/frontend/src/app/[locale]/login/page.tsx b/rafts/frontend/src/app/[locale]/login/page.tsx new file mode 100644 index 0000000..b20fc0b --- /dev/null +++ b/rafts/frontend/src/app/[locale]/login/page.tsx @@ -0,0 +1,102 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// app/[locale]/login/page.tsx +import { redirect } from 'next/navigation' +import LoginForm from '@/components/User/LoginForm' +import LoginFormLayout from '@/components/Layout/LoginFormLayout' +import { auth } from '@/auth/cadc-auth/credentials' +import { authenticateUser } from '@/actions/auth' + +// Define types that match what Next.js is expecting +interface PageProps { + params: Promise<{ locale: string }> + searchParams: Promise<{ [key: string]: string | string[] | undefined }> +} + +const LoginPage = async ({ searchParams }: PageProps) => { + // Await the promises to get the actual values + const resolvedSearchParams = await searchParams + + const session = await auth() + const defaultReturnUrl = '/' + + if (session) { + redirect((resolvedSearchParams.returnUrl as string) || defaultReturnUrl) + } + + return ( + + + + ) +} + +export default LoginPage diff --git a/rafts/frontend/src/app/[locale]/not-found.tsx b/rafts/frontend/src/app/[locale]/not-found.tsx new file mode 100644 index 0000000..a449e57 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/not-found.tsx @@ -0,0 +1,150 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useTranslations } from 'next-intl' +import { Box, Typography, Button, Container, Paper } from '@mui/material' +import { Home, SearchOff } from '@mui/icons-material' +import { useRouter } from '@/i18n/routing' + +export default function NotFound() { + const t = useTranslations('not_found') + const router = useRouter() + + return ( + + + + + + 404 + + + + {t('title')} + + + + {t('description')} + + + + + + + + + ) +} diff --git a/rafts/frontend/src/app/[locale]/page.tsx b/rafts/frontend/src/app/[locale]/page.tsx new file mode 100644 index 0000000..759f3a7 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/page.tsx @@ -0,0 +1,77 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import LandingChoice from '@/components/LandingPage/LandingChoice' +import { auth } from '@/auth/cadc-auth/credentials' + +const HomePage = async () => { + const session = await auth() + + return +} + +export default HomePage diff --git a/rafts/frontend/src/app/[locale]/profile/page.tsx b/rafts/frontend/src/app/[locale]/profile/page.tsx new file mode 100644 index 0000000..8bd9d6b --- /dev/null +++ b/rafts/frontend/src/app/[locale]/profile/page.tsx @@ -0,0 +1,97 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import LoginFormLayout from '@/components/Layout/LoginFormLayout' +import { auth } from '@/auth/cadc-auth/credentials' +import UserProfile from '@/components/User/Profile' + +const LoginPage = async () => { + // Await the promises to get the actual values + + const session = await auth() + + // Log user roles and session info + + if (!session || !session.user) { + return null + } + return ( + + + + ) +} + +export default LoginPage diff --git a/rafts/frontend/src/app/[locale]/public-view/doi/page.tsx b/rafts/frontend/src/app/[locale]/public-view/doi/page.tsx new file mode 100644 index 0000000..990b47e --- /dev/null +++ b/rafts/frontend/src/app/[locale]/public-view/doi/page.tsx @@ -0,0 +1,133 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' +import { useState, useEffect } from 'react' +import { getDOIData } from '@/actions/getDOI' +import RaftTable from '@/components/DOIRaftTable/RaftTable' +import { DOIData } from '@/types/doi' +import { getRafts } from '@/actions/getRafts' + +export default function View() { + const [doiData, setDoiData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const fetchData = async () => { + setIsLoading(true) + const { success, data, error } = await getDOIData() + if (success && data) { + setDoiData(data) + } else { + console.error('Error fetching DOI data:', error) + setError('Failed to load RAFT data. Please try again later.') + } + setIsLoading(false) + } + + fetchData() + }, []) + + useEffect(() => { + const fetchRafts = async () => { + setIsLoading(true) + const { success, data, error } = await getRafts() + if (success && data) { + // Data fetched successfully + } else { + console.error('Error fetching DOI data:', error) + setError('Failed to load RAFT data. Please try again later.') + } + setIsLoading(false) + } + + fetchRafts() + }, []) + + return ( +
+
+

Your submissions (RAFTs)

+
+
+ {isLoading ? ( +
+ Loading RAFT data... +
+ ) : error ? ( +
{error}
+ ) : ( + + )} +
+
+
CADC RAFT Publication System
+
+
+ ) +} diff --git a/rafts/frontend/src/app/[locale]/public-view/rafts/[id]/page.tsx b/rafts/frontend/src/app/[locale]/public-view/rafts/[id]/page.tsx new file mode 100644 index 0000000..29ba89f --- /dev/null +++ b/rafts/frontend/src/app/[locale]/public-view/rafts/[id]/page.tsx @@ -0,0 +1,106 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Metadata } from 'next' +import RaftDetail from '@/components/RaftDetail/PublishedRaftDetail' +import { getRaftById } from '@/actions/getRaftById' +import { notFound } from 'next/navigation' +import { getPublishedRaftById } from '@/actions/getPublishedRaftById' + +export async function generateMetadata(props: { + params: Promise<{ id: string }> +}): Promise { + // Fetch RAFT data for metadata + const params = await props.params + const { success, data } = await getRaftById(params?.id) + + if (!success || !data) { + return { + title: 'RAFT Not Found', + } + } + + return { + title: `RAFT - ${data?.generalInfo?.title || 'View RAFT'}`, + description: + data.observationInfo?.abstract?.substring(0, 160) || + 'Research Announcement For The Solar System', + } +} + +export default async function RaftPage(props: { params: Promise<{ id: string }> }) { + // Fetch the RAFT data + const params = await props.params + const { success, data } = await getPublishedRaftById(params?.id) + + // Handle not found + if (!success || !data) { + notFound() + } + + return +} diff --git a/rafts/frontend/src/app/[locale]/public-view/rafts/page.tsx b/rafts/frontend/src/app/[locale]/public-view/rafts/page.tsx new file mode 100644 index 0000000..bc189cf --- /dev/null +++ b/rafts/frontend/src/app/[locale]/public-view/rafts/page.tsx @@ -0,0 +1,117 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +//rafts/page.tsx +'use client' +import { useState, useEffect } from 'react' +import RaftTable from '@/components/RaftTable/PublishedRaftTable' +import { RaftData } from '@/types/doi' +import { getRafts } from '@/actions/getRafts' + +export default function View() { + const [raftData, setRaftData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + const fetchData = async () => { + setIsLoading(true) + const { success, data, error } = await getRafts() + if (success && data) { + setRaftData(data.data) + } else { + console.error('Error fetching DOI data:', error) + setError('Failed to load RAFT data. Please try again later.') + } + setIsLoading(false) + } + + fetchData() + }, []) + + return ( +
+
+

All Published (RAFTs)

+
+
+ {isLoading ? ( +
+ Loading RAFTs... +
+ ) : error ? ( +
{error}
+ ) : ( + + )} +
+
+
CADC RAFT Publication System
+
+
+ ) +} diff --git a/rafts/frontend/src/app/[locale]/registration/page.tsx b/rafts/frontend/src/app/[locale]/registration/page.tsx new file mode 100644 index 0000000..8b5f6d8 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/registration/page.tsx @@ -0,0 +1,79 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import RegistrationForm from '@/components/User/RegistrationForm' +import LoginFormLayout from '@/components/Layout/LoginFormLayout' + +const RegisterPage = () => { + return ( + + + + ) +} + +export default RegisterPage diff --git a/rafts/frontend/src/app/[locale]/request-password-reset/page.tsx b/rafts/frontend/src/app/[locale]/request-password-reset/page.tsx new file mode 100644 index 0000000..6acaaf2 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/request-password-reset/page.tsx @@ -0,0 +1,77 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// src/app/[locale]/reset-password/request/page.tsx +import RequestPasswordResetForm from '@/components/User/RequestPasswordReset' + +export default function RequestPasswordResetPage() { + return ( +
+ +
+ ) +} diff --git a/rafts/frontend/src/app/[locale]/reset-password/[token]/page.tsx b/rafts/frontend/src/app/[locale]/reset-password/[token]/page.tsx new file mode 100644 index 0000000..f3ea7dc --- /dev/null +++ b/rafts/frontend/src/app/[locale]/reset-password/[token]/page.tsx @@ -0,0 +1,75 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import ResetPassword from '@/components/User/ResetPasswordPage' + +const ResetPasswordPage = async (props: { params: Promise<{ token: string }> }) => { + const params = await props.params + return +} + +export default ResetPasswordPage diff --git a/rafts/frontend/src/app/[locale]/review/rafts/[id]/loading.tsx b/rafts/frontend/src/app/[locale]/review/rafts/[id]/loading.tsx new file mode 100644 index 0000000..149bcef --- /dev/null +++ b/rafts/frontend/src/app/[locale]/review/rafts/[id]/loading.tsx @@ -0,0 +1,138 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Container, Paper, Skeleton, Box, Grid } from '@mui/material' + +export default function Loading() { + return ( + + + {/* Main content area */} + + {/* Breadcrumbs skeleton */} + + + {/* Back button skeleton */} + + + + {/* Header section */} + + + + + + + + + + {/* Tabs */} + + + + + + + + + {/* Content */} + + + + + + + + + {/* Side panel skeleton */} + + + + + {/* Status section */} + + + + + + {/* Action buttons */} + + + + + + {/* Comments section */} + + + + + + + + + ) +} diff --git a/rafts/frontend/src/app/[locale]/review/rafts/[id]/page.tsx b/rafts/frontend/src/app/[locale]/review/rafts/[id]/page.tsx new file mode 100644 index 0000000..c3dde0b --- /dev/null +++ b/rafts/frontend/src/app/[locale]/review/rafts/[id]/page.tsx @@ -0,0 +1,102 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Metadata } from 'next' +import RaftDetail from '@/components/RaftDetail/ReviewRaftDetail' +import { notFound } from 'next/navigation' +import { getDOIRaft } from '@/actions/getDOIRAFT' + +export async function generateMetadata(props: { + params: Promise<{ id: string }> +}): Promise { + const params = await props.params + const { success, data } = await getDOIRaft(params.id) + + if (!success || !data) { + return { + title: 'RAFT Not Found', + } + } + + return { + title: `Review RAFT - ${data?.generalInfo?.title || 'Review RAFT'}`, + description: + data.observationInfo?.abstract?.substring(0, 160) || + 'Research Announcement For The Solar System', + } +} + +export default async function RaftPage(props: { params: Promise<{ id: string }> }) { + const params = await props.params + const { success, data } = await getDOIRaft(params.id) + + if (!success || !data) { + notFound() + } + + return +} diff --git a/rafts/frontend/src/app/[locale]/review/rafts/page.tsx b/rafts/frontend/src/app/[locale]/review/rafts/page.tsx new file mode 100644 index 0000000..0f44758 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/review/rafts/page.tsx @@ -0,0 +1,162 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' +import { useState, useEffect } from 'react' +import RaftTable from '@/components/RaftTable/ReviewRaftTable' +import { RaftData } from '@/types/doi' +import { getDOIsForReview } from '@/actions/getDOIsForReview' +import { OPTION_REVIEW } from '@/shared/constants' +import { Typography, Paper, Alert } from '@mui/material' +import StatusFilter from '@/components/RaftDetail/components/StatusFilter' + +export default function ReviewRafts() { + const [raftData, setRaftData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [isAuthError, setIsAuthError] = useState(false) + const [currentStatus, setCurrentStatus] = useState(OPTION_REVIEW) + const [counts, setCounts] = useState>({}) + + const fetchData = async (status: string) => { + setIsLoading(true) + setError(null) + + const { success, data, error } = await getDOIsForReview(status) + + if (success && data) { + setRaftData(data.data || []) + // Update counts from the response + setCounts(data.counts || {}) + setIsAuthError(false) + } else { + console.error('Error fetching RAFT data:', error) + if (error === '401' || error === 'Not authenticated') { + setIsAuthError(true) + setError('Your session has expired. Please sign in again.') + } else { + setError('Failed to load RAFTS data. Please try again later.') + } + setRaftData([]) + } + + setIsLoading(false) + } + + // Fetch data when status changes + useEffect(() => { + fetchData(currentStatus) + }, [currentStatus]) + + const handleStatusChange = (status: string) => { + setCurrentStatus(status) + } + + return ( +
+
+ + Review RAFT Submissions + + + Manage and review RAFT submissions based on their current status. + + + +
+
+ {error ? ( + + {error} + + ) : !isLoading && raftData.length === 0 ? ( + + + No submissions found with this status + + + ) : ( + fetchData(currentStatus)} + /> + )} +
+ +
+
CADC RAFT Publication System
+
+
+ ) +} diff --git a/rafts/frontend/src/app/[locale]/verify-email/[token]/page.tsx b/rafts/frontend/src/app/[locale]/verify-email/[token]/page.tsx new file mode 100644 index 0000000..399f9a3 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/verify-email/[token]/page.tsx @@ -0,0 +1,75 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import VerifyEmail from '@/components/User/VerifyEmailPage' + +const VerifyEmailPage = async (props: { params: Promise<{ token: string }> }) => { + const params = await props.params + return +} + +export default VerifyEmailPage diff --git a/rafts/frontend/src/app/[locale]/view/rafts/[id]/loading.tsx b/rafts/frontend/src/app/[locale]/view/rafts/[id]/loading.tsx new file mode 100644 index 0000000..271f1a3 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/view/rafts/[id]/loading.tsx @@ -0,0 +1,139 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Container, Paper, Skeleton, Box } from '@mui/material' + +export default function Loading() { + return ( + + {/* Breadcrumbs skeleton */} + + + {/* Back button skeleton */} + + + {/* Main content */} + + {/* Header section skeleton */} + + {/* Status badge */} + + + {/* Title */} + + + {/* Metadata row */} + + + + + + + {/* Action buttons */} + + + + + + + + {/* Tabs skeleton */} + + + + + + + + + {/* Tab content skeleton */} + + {/* Section title */} + + + {/* Content blocks */} + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/rafts/frontend/src/app/[locale]/view/rafts/[id]/page.tsx b/rafts/frontend/src/app/[locale]/view/rafts/[id]/page.tsx new file mode 100644 index 0000000..3b3278b --- /dev/null +++ b/rafts/frontend/src/app/[locale]/view/rafts/[id]/page.tsx @@ -0,0 +1,102 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Metadata } from 'next' +import RaftDetail from '@/components/RaftDetail/RaftDetail' +import { notFound } from 'next/navigation' +import { getDOIRaft } from '@/actions/getDOIRAFT' + +export async function generateMetadata(props: { + params: Promise<{ id: string }> +}): Promise { + const params = await props.params + const { success, data } = await getDOIRaft(params.id) + + if (!success || !data) { + return { + title: 'RAFT Not Found', + } + } + + return { + title: `RAFT - ${data?.generalInfo?.title || 'View RAFT'}`, + description: + data.observationInfo?.abstract?.substring(0, 160) || + 'Research Announcement For The Solar System', + } +} + +export default async function RaftPage(props: { params: Promise<{ id: string }> }) { + const params = await props.params + const { success, data } = await getDOIRaft(params.id) + + if (!success || !data) { + notFound() + } + + return +} diff --git a/rafts/frontend/src/app/[locale]/view/rafts/loading.tsx b/rafts/frontend/src/app/[locale]/view/rafts/loading.tsx new file mode 100644 index 0000000..ddb84e2 --- /dev/null +++ b/rafts/frontend/src/app/[locale]/view/rafts/loading.tsx @@ -0,0 +1,97 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Skeleton, Box } from '@mui/material' + +export default function Loading() { + return ( +
+
+
+ + +
+
+
+ {/* Table header skeleton */} + + + + + {/* Table rows skeleton */} + + {[...Array(5)].map((_, i) => ( + + ))} + +
+
+ +
+
+ ) +} diff --git a/rafts/frontend/src/app/[locale]/view/rafts/page.tsx b/rafts/frontend/src/app/[locale]/view/rafts/page.tsx new file mode 100644 index 0000000..886847d --- /dev/null +++ b/rafts/frontend/src/app/[locale]/view/rafts/page.tsx @@ -0,0 +1,132 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' +import { useState, useEffect, useCallback } from 'react' +import { getDOIData } from '@/actions/getDOI' +import RaftTable from '@/components/DOIRaftTable/RaftTable' +import { DOIData } from '@/types/doi' +import { signOut } from 'next-auth/react' +import { AUTH_FAILED } from '@/auth/cadc-auth/constants' +import { Link } from '@/i18n/routing' +import { Button } from '@mui/material' +import { Add } from '@mui/icons-material' + +export default function View() { + const [doiData, setDoiData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + const fetchData = useCallback(async () => { + setIsLoading(true) + const { success, data, error } = await getDOIData() + if (success && data) { + setDoiData(data) + } else { + if (error === AUTH_FAILED) { + await signOut() + } + console.error('Error fetching DOI data:', error) + setError('Failed to load RAFT data. Please try again later.') + } + setIsLoading(false) + }, []) + + useEffect(() => { + fetchData() + }, [fetchData]) + + // Callback to refresh data after status changes + const handleRefresh = useCallback(() => { + fetchData() + }, [fetchData]) + + return ( +
+
+
+

Your submissions (RAFTs)

+ + + +
+
+
+ {error ? ( +
{error}
+ ) : ( + + )} +
+
+
CADC RAFT Publication System
+
+
+ ) +} diff --git a/rafts/frontend/src/app/api/attachments/[doiId]/[filename]/route.ts b/rafts/frontend/src/app/api/attachments/[doiId]/[filename]/route.ts new file mode 100644 index 0000000..b1ec442 --- /dev/null +++ b/rafts/frontend/src/app/api/attachments/[doiId]/[filename]/route.ts @@ -0,0 +1,132 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +/** + * API Route: Attachment Proxy + * + * Proxies attachment downloads from VOSpace to avoid CORS issues. + * This allows images and other files to be displayed in the browser. + */ + +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/auth/cadc-auth/credentials' +import { downloadAttachment } from '@/services/attachmentService' + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ doiId: string; filename: string }> }, +) { + const { doiId, filename } = await params + + // Get session for authentication + const session = await auth() + const accessToken = session?.accessToken + + if (!accessToken) { + return NextResponse.json({ error: 'Not authenticated' }, { status: 401 }) + } + + try { + // Decode the filename (it may be URL encoded) + const decodedFilename = decodeURIComponent(filename) + + // Download from VOSpace + const result = await downloadAttachment(doiId, decodedFilename, accessToken, false) + + if (!result.success || !result.content) { + console.error('[API attachments] Download failed:', result.error) + return NextResponse.json({ error: result.error || 'Download failed' }, { status: 404 }) + } + + // Convert content to ArrayBuffer for response + let arrayBuffer: ArrayBuffer + + if (result.content instanceof Blob) { + arrayBuffer = await result.content.arrayBuffer() + } else if (typeof result.content === 'string') { + // Text content - encode as UTF-8 + const encoder = new TextEncoder() + const encoded = encoder.encode(result.content) + // Create a proper ArrayBuffer copy to satisfy TypeScript + arrayBuffer = new Uint8Array(encoded).buffer as ArrayBuffer + } else { + return NextResponse.json({ error: 'Invalid content type' }, { status: 500 }) + } + + // Return the file with appropriate headers + return new NextResponse(arrayBuffer, { + headers: { + 'Content-Type': result.mimeType || 'application/octet-stream', + 'Content-Disposition': `inline; filename="${decodedFilename}"`, + 'Cache-Control': 'private, max-age=3600', // Cache for 1 hour + }, + }) + } catch (error) { + console.error('[API attachments] Error:', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/rafts/frontend/src/app/api/auth/[...nextauth]/route-wrapper.ts b/rafts/frontend/src/app/api/auth/[...nextauth]/route-wrapper.ts new file mode 100644 index 0000000..71e88cd --- /dev/null +++ b/rafts/frontend/src/app/api/auth/[...nextauth]/route-wrapper.ts @@ -0,0 +1,102 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { NextRequest } from 'next/server' +import { GET as AuthGET, POST as AuthPOST } from '@/auth/cadc-auth/credentials' + +const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' + +// Wrapper to handle base path issues with NextAuth v5 +async function handleRequest(req: NextRequest, handler: (req: NextRequest) => Promise) { + // Create a modified request with the base path stripped from the URL + const url = new URL(req.url) + + // If the pathname includes the base path, strip it for NextAuth + if (basePath && url.pathname.startsWith(basePath)) { + url.pathname = url.pathname.slice(basePath.length) + } + + // Create a new request with the modified URL + const modifiedReq = new NextRequest(url.toString(), { + method: req.method, + headers: req.headers, + body: req.body, + }) + + // Call the original handler with the modified request + const response = await handler(modifiedReq) + + return response +} + +export async function GET(req: NextRequest) { + return handleRequest(req, AuthGET) +} + +export async function POST(req: NextRequest) { + return handleRequest(req, AuthPOST) +} diff --git a/rafts/frontend/src/app/api/auth/[...nextauth]/route.ts b/rafts/frontend/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..af53c8e --- /dev/null +++ b/rafts/frontend/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,94 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { NextRequest } from 'next/server' +import { GET as AuthGET, POST as AuthPOST } from '@/auth/cadc-auth/credentials' + +const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' + +// Fix the URL path for NextAuth v5 which doesn't handle base paths well +function fixUrl(req: NextRequest): NextRequest { + if (!basePath) return req + + const url = new URL(req.url) + // Remove the base path from the pathname for NextAuth processing + if (url.pathname.startsWith(`${basePath}/api/auth`)) { + url.pathname = url.pathname.replace(basePath, '') + } + + return new NextRequest(url, req) +} + +export async function GET(req: NextRequest) { + const fixedReq = fixUrl(req) + return AuthGET(fixedReq) +} + +export async function POST(req: NextRequest) { + const fixedReq = fixUrl(req) + return AuthPOST(fixedReq) +} diff --git a/rafts/frontend/src/app/api/doi/[id]/route.ts b/rafts/frontend/src/app/api/doi/[id]/route.ts new file mode 100644 index 0000000..da80527 --- /dev/null +++ b/rafts/frontend/src/app/api/doi/[id]/route.ts @@ -0,0 +1,114 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getDOIXml, updateDOIXml } from '@/actions/updateDOIXml' + +// GET /api/doi/[id] - Fetch DOI XML +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params + + const result = await getDOIXml(id) + + if (!result.success) { + return NextResponse.json({ error: result.message }, { status: 400 }) + } + + return new NextResponse(result.data, { + headers: { 'Content-Type': 'application/xml' }, + }) +} + +// POST /api/doi/[id] - Update DOI via POST +// Body: { doiMetaData?: object, doiNodeData?: { status?, journalRef?, reviewer? } } +// +// Updatable fields: +// - doiMetaData: creators, titles, publicationYear, language +// - doiNodeData: status, journalRef, reviewer +// +// NOTE: dataDirectory is NOT modifiable - it's computed from backend config +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const { id } = await params + + const body = await request.json() + const { doiMetaData, doiNodeData } = body + + if (!doiMetaData && !doiNodeData) { + return NextResponse.json( + { error: 'At least one of doiMetaData or doiNodeData is required' }, + { status: 400 }, + ) + } + + const updateResult = await updateDOIXml(id, doiMetaData, doiNodeData) + + if (!updateResult.success) { + return NextResponse.json({ error: updateResult.message }, { status: 400 }) + } + + return NextResponse.json({ success: true, data: updateResult.data }) +} diff --git a/rafts/frontend/src/app/api/health/route.ts b/rafts/frontend/src/app/api/health/route.ts new file mode 100644 index 0000000..8b0c4d0 --- /dev/null +++ b/rafts/frontend/src/app/api/health/route.ts @@ -0,0 +1,76 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { NextResponse } from 'next/server' + +/** + * Health check endpoint for the frontend service + * Used by Docker health check to determine if the service is healthy + */ +export async function GET() { + return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() }) +} diff --git a/rafts/frontend/src/app/api/set-cookie/route.ts b/rafts/frontend/src/app/api/set-cookie/route.ts new file mode 100644 index 0000000..b67f3a4 --- /dev/null +++ b/rafts/frontend/src/app/api/set-cookie/route.ts @@ -0,0 +1,127 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// src/app/api/setup-cookies/route.ts +import { NextResponse } from 'next/server' +import { CADC_COOKIE_DOMAIN_URL, CANFAR_COOKIE_DOMAIN_URL } from '@/auth/cadc-auth/constants' + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url) + const token = searchParams.get('token') + + if (!token) { + return NextResponse.json({ error: 'No token provided' }, { status: 400 }) + } + + // Forward the requests and capture responses + const canfarRes = await fetch(`${CANFAR_COOKIE_DOMAIN_URL}${token}`, { + credentials: 'include', + redirect: 'manual', + }) + const cadcRes = await fetch(`${CADC_COOKIE_DOMAIN_URL}${token}`, { + credentials: 'include', + redirect: 'manual', + }) + + // Extract cookie headers + const canfarCookies = canfarRes.headers.getSetCookie() + const cadcCookies = cadcRes.headers.getSetCookie() + + // Create response with combined cookies + const response = NextResponse.json({ success: true }) + + const extractCookieValue = (cookieHeader: string) => { + const matches = cookieHeader.match(/CADC_SSO="([^"]+)"/) + return matches ? matches[1] : null + } + + /*const extractCookieAttribute = (cookieHeader: string, attribute: string): string | undefined => { + const regex = new RegExp(`${attribute}=([^;]+)`, 'i') + const matches = cookieHeader.match(regex) + return matches ? matches[1] : undefined + }*/ + + // Forward all cookies to the client + for (const cookieHeader of [...canfarCookies, ...cadcCookies]) { + const cookieValue = extractCookieValue(cookieHeader) + if (cookieValue) { + // Create a new cookie with the same value but set the domain to your app's domain + + response.cookies.set({ + name: 'CADC_SSO', + value: cookieValue, + httpOnly: true, + secure: true, + sameSite: 'none', + path: '/', + maxAge: 169344, + }) + } + } + + return response +} diff --git a/rafts/frontend/src/app/favicon.ico b/rafts/frontend/src/app/favicon.ico new file mode 100644 index 0000000..836f7b3 Binary files /dev/null and b/rafts/frontend/src/app/favicon.ico differ diff --git a/rafts/frontend/src/app/globals.css b/rafts/frontend/src/app/globals.css new file mode 100644 index 0000000..849b078 --- /dev/null +++ b/rafts/frontend/src/app/globals.css @@ -0,0 +1,184 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + /* Base light theme (Defaults) */ + --background: #ffffff; /* White */ + --foreground: #171717; /* Near Black */ + + /* Input fields - light theme */ + --input-background: #f5f5f5; /* Light Gray */ + --input-border: #d1d5db; /* Gray 300 */ + --input-text: #171717; /* Near Black */ + --input-focus-border: #2563eb; /* Blue 600 */ + --input-focus-ring: rgba(37, 99, 235, 0.2); + + /* Form section headers - light theme */ + --header-text: #1f2937; /* Gray 800 */ + + /* Labels - light theme */ + --label-text: #374151; /* Gray 700 */ + + /* Helper text - light theme */ + --helper-text: #6b7280; /* Gray 500 */ + + /* Form fieldset - light theme */ + --fieldset-background: #ffffff; /* White */ + --fieldset-border: #e5e7eb; /* Gray 200 */ + --legend-text: #1f2937; /* Gray 800 */ + + /* Buttons - light theme */ + --button-background: #2563eb; /* Blue 600 */ + --button-hover: #1d4ed8; /* Blue 700 */ + --button-text: #ffffff; /* White */ + + /*SolarLogo*/ + --orbit-color: #000; +} +.solar-system { + width: 80px; + height: 80px; +} +/* --- Dark Theme --- */ +.dark { + /* --- Base Dark Theme --- */ + /* Off-black background preferred over pure #000 for reduced eye strain */ + --background: #1a1a1a; + /* High contrast off-white text */ + --foreground: #f0f0f0; /* Slightly off-white, very high contrast */ + + /* --- Input Fields - Dark Theme --- */ + /* Clearly distinct background from main page background */ + --input-background: #2c2c2c; /* Dark gray, distinct from #1a1a1a */ + /* Border visible enough to define the field */ + --input-border: #555555; /* Mid-dark gray, clearly visible */ + /* Text matching main foreground for consistency and high contrast */ + --input-text: #f0f0f0; + /* Bright, clear focus border */ + --input-focus-border: #60a5fa; /* Bright Blue (Blue 400) */ + /* Visible focus ring */ + --input-focus-ring: rgba(96, 165, 250, 0.4); + + /* --- Form Section Headers - Dark Theme --- */ + /* Bright color for prominence, good contrast */ + --header-text: #93c5fd; /* Light Sky Blue (Sky 300) - Good contrast */ + + /* --- Labels - Dark Theme --- */ + /* High contrast label text */ + --label-text: #e5e7eb; /* Light Gray (Gray 200) - Very readable */ + + /* --- Helper Text - Dark Theme --- */ + /* Visible but distinct from labels/main text */ + --helper-text: #a0a0a0; /* Lighter Gray than before for better visibility (passes WCAG AA) */ + + /* --- Form Fieldset - Dark Theme --- */ + /* Slightly distinct background if needed, matching input bg for consistency */ + --fieldset-background: #2c2c2c; + /* Consistent border style */ + --fieldset-border: #555555; + /* Legend matches header style for consistency */ + --legend-text: #93c5fd; + + /* --- Buttons - Dark Theme --- */ + /* Bright button for clear action indication */ + --button-background: #3b82f6; /* Bright Blue (Blue 500) */ + /* Clear hover state */ + --button-hover: #2563eb; /* Darker Blue (Blue 600) */ + /* Pure white text for maximum contrast on blue button */ + --button-text: #ffffff; + /*SolarLogo*/ + --orbit-color: #fff; +} + +/* --- Base Body Styles --- */ +/* Using !important to ensure CSS variables take precedence over MUI CssBaseline */ +body { + color: var(--foreground) !important; + background: var(--background) !important; + /* Consider a slightly larger base font size if applicable */ + /* font-size: 16px; or font-size: 1rem; */ + /* Using a sans-serif font is generally good for readability */ + font-family: Arial, Helvetica, sans-serif; + /* Improve text rendering */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* --- Form Component Styles (Using Variables) --- */ +.form-input { + background-color: var(--input-background); + color: var(--input-text); + border: 1px solid var(--input-border); + border-radius: 0.375rem; /* 6px */ + padding: 0.75rem 1rem; /* 12px 16px */ + font-size: 1.125rem; /* 18px */ + width: 100%; + transition: + border-color 0.15s, + box-shadow 0.15s; +} + +.form-input:focus { + border-color: var(--input-focus-border); + box-shadow: 0 0 0 3px var(--input-focus-ring); + outline: none; +} + +.form-label { + display: block; + color: var(--label-text); + font-size: 1.125rem; /* 18px */ + font-weight: 500; /* Medium weight */ + margin-bottom: 0.5rem; /* 8px */ +} + +.form-helper-text { + color: var(--helper-text); + font-size: 0.875rem; /* 14px */ + margin-top: 0.375rem; /* 6px */ +} + +.form-heading { + color: var(--header-text); + font-size: 1.5rem; /* 24px */ + font-weight: 600; /* Semibold */ + margin-bottom: 1.5rem; /* 24px */ +} + +.form-fieldset { + border: 1px solid var(--fieldset-border); + border-radius: 0.5rem; /* 8px */ + padding: 1.5rem; /* 24px */ + margin-bottom: 1.5rem; /* 24px */ + background-color: var(--fieldset-background); +} + +.form-fieldset legend { + color: var(--legend-text); + font-weight: 600; /* Semibold */ + font-size: 1.25rem; /* 20px */ + padding: 0 0.75rem; /* 0 12px */ +} + +.form-button { + background-color: var(--button-background); + color: var(--button-text); + border: none; + border-radius: 0.375rem; /* 6px */ + padding: 0.75rem 1.5rem; /* 12px 24px */ + font-size: 1.125rem; /* 18px */ + font-weight: 500; /* Medium */ + cursor: pointer; + transition: background-color 0.15s; +} + +.form-button:hover { + background-color: var(--button-hover); +} + +.form-button:focus { + outline: none; + /* Using same focus ring as inputs for consistency */ + box-shadow: 0 0 0 3px var(--input-focus-ring); +} diff --git a/rafts/frontend/src/app/layout.tsx b/rafts/frontend/src/app/layout.tsx new file mode 100644 index 0000000..2ee9533 --- /dev/null +++ b/rafts/frontend/src/app/layout.tsx @@ -0,0 +1,121 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { ReactElement } from 'react' +import { Metadata } from 'next' +import { RootLayoutProps } from '@/types/common' +import { useLocale } from 'next-intl' +import { ThemeProvider as NextThemesProvider } from 'next-themes' + +//auth +import { AuthProvider } from '@/components/Providers/AuthProvider' + +// Material UI +import '@fontsource/roboto/300.css' +import '@fontsource/roboto/400.css' +import '@fontsource/roboto/500.css' +import '@fontsource/roboto/700.css' +import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter' +import ThemeProvider from '@/styles/ThemeProvider' + +// Tailwind Global Styles (Load After MUI) +import './globals.css' +import ErrorBoundary from '@/components/ErrorBoundary/ErrorBoundary' // Move this import below MUI components + +export const metadata: Metadata = { + title: 'RAFTS', + description: 'Research Announcements For The Solar System', + icons: { + icon: [{ url: '/favicon.ico', type: 'image/x-icon' }], + }, +} + +const RootLayout = ({ children }: RootLayoutProps): ReactElement => { + const locale = useLocale() + return ( + + + + + + + {children} + + + {' '} + + + + ) +} + +export default RootLayout diff --git a/rafts/frontend/src/app/page.tsx b/rafts/frontend/src/app/page.tsx new file mode 100644 index 0000000..a1b61b3 --- /dev/null +++ b/rafts/frontend/src/app/page.tsx @@ -0,0 +1,70 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export default function RootPage() { + return null +} diff --git a/rafts/frontend/src/assets/systeme-solaire-og.jpg b/rafts/frontend/src/assets/systeme-solaire-og.jpg new file mode 100644 index 0000000..a84a130 Binary files /dev/null and b/rafts/frontend/src/assets/systeme-solaire-og.jpg differ diff --git a/rafts/frontend/src/auth/cadc-auth/authenticateUser.ts b/rafts/frontend/src/auth/cadc-auth/authenticateUser.ts new file mode 100644 index 0000000..8796ff3 --- /dev/null +++ b/rafts/frontend/src/auth/cadc-auth/authenticateUser.ts @@ -0,0 +1,101 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { CANFAR_LOGIN_URL } from './constants' + +export const authenticateUser = async ( + username: string, + password: string, +): Promise => { + try { + const loginOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': 'RAFT-System/1.0', + }, + body: new URLSearchParams({ + username, + password, + }), + credentials: 'include' as RequestCredentials, + } + const loginResponse = await fetch(CANFAR_LOGIN_URL, loginOptions) + + if (!loginResponse.ok) { + console.error('Login failed:', loginResponse.status, loginResponse.statusText) + return null + } + + // Extract the token from the response + const token = await loginResponse.text() + return token ? token.trim() : null + } catch (error) { + console.error('Authentication error:', error) + return null + } +} diff --git a/rafts/frontend/src/auth/cadc-auth/checkIsAuthenticated.ts b/rafts/frontend/src/auth/cadc-auth/checkIsAuthenticated.ts new file mode 100644 index 0000000..4bd08bf --- /dev/null +++ b/rafts/frontend/src/auth/cadc-auth/checkIsAuthenticated.ts @@ -0,0 +1,123 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use server' + +import { CANFAR_USER_URL } from '@/auth/cadc-auth/constants' + +export async function checkIsAuthenticated(req: Request): Promise<{ + isAuthenticated: boolean + username?: string +}> { + try { + // Extract cookies from the request + const cookieHeader = req.headers.get('cookie') || '' + const cookies = parseCookies(cookieHeader) + + // Look for CADC_SSO cookie + const cadcCookie = cookies['CADC_SSO'] + + if (!cadcCookie) { + return { isAuthenticated: false } + } + + // Validate the cookie by making a request to CADC whoami service + const response = await fetch(CANFAR_USER_URL, { + headers: { + Cookie: `CADC_SSO="${cadcCookie}"`, + }, + }) + + if (!response.ok) { + return { isAuthenticated: false } + } + // Parse the user info + const userInfo = await response.json() + + return { + isAuthenticated: true, + username: userInfo.username, + } + } catch (error) { + console.error('Error checking CADC auth:', error) + return { isAuthenticated: false } + } +} + +// Helper function to parse cookies +function parseCookies(cookieHeader: string): Record { + const cookies: Record = {} + + if (!cookieHeader) return cookies + + cookieHeader.split(';').forEach((cookie) => { + const [name, value] = cookie.trim().split('=') + cookies[name] = value + }) + + return cookies +} diff --git a/rafts/frontend/src/auth/cadc-auth/constants.ts b/rafts/frontend/src/auth/cadc-auth/constants.ts new file mode 100644 index 0000000..42215cf --- /dev/null +++ b/rafts/frontend/src/auth/cadc-auth/constants.ts @@ -0,0 +1,84 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export const AUTH_FAILED = '401' +export const CANFAR_LOGIN_URL = + process.env.NEXT_PUBLIC_CANFAR_LOGIN_URL || 'https://ws-cadc.canfar.net/ac/login' +export const CANFAR_USERS_URL = + process.env.NEXT_PUBLIC_CANFAR_USERS_URL || 'https://ws-cadc.canfar.net/ac/users' +export const CANFAR_USER_URL = + process.env.NEXT_CANFAR_AC_WHOAMI_URL || 'https://ws-cadc.canfar.net/ac/whoami' +export const CANFAR_USER_GROUPS_URL = + process.env.NEXT_CANFAR_AC_GROUPS_URL || 'https://ws-cadc.canfar.net/ac/groups' +export const CANFAR_RAFT_REVIEWER_GROUP = + process.env.NEXT_CANFAR_RAFT_GROUP_NAME || 'RAFTS-reviewers' +export const CANFAR_COOKIE_DOMAIN_URL = + process.env.NEXT_CANFAR_COOKIE_URL || 'https://www.canfar.net/access/sso?cookieValue=' +export const CADC_COOKIE_DOMAIN_URL = + process.env.NEXT_CADC_COOKIE_URL || + 'https://www.cadc-ccda.hia-iha.nrc-cnrc.gc.ca/access/sso?cookieValue=' +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:4000' diff --git a/rafts/frontend/src/auth/cadc-auth/credentials.ts b/rafts/frontend/src/auth/cadc-auth/credentials.ts new file mode 100644 index 0000000..560f524 --- /dev/null +++ b/rafts/frontend/src/auth/cadc-auth/credentials.ts @@ -0,0 +1,157 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import NextAuth, { User } from 'next-auth' +import CredentialsProvider from 'next-auth/providers/credentials' + +import { fetchUserInfo } from './fetchUserInfo' +import { fetchUserGroups } from './fetchUserGroups' +import { authenticateUser } from '@/auth/cadc-auth/authenticateUser' + +export const { + handlers: { GET, POST }, + auth, + signIn, + signOut, +} = NextAuth({ + providers: [ + CredentialsProvider({ + name: 'CADC Login', + credentials: { + username: { label: 'Username', type: 'text' }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials): Promise { + try { + if (!credentials?.username || !credentials?.password) { + return null + } + + // Step 1: Authenticate and get token + const token = await authenticateUser( + credentials.username as string, + credentials.password as string, + ) + + if (!token) return null + /*// Step 2: Setup cross-domain cookies if needed + await setupCrossDomainCookies(token, `${CANFAR_COOKIE_DOMAIN_URL}${token}`) + await setupCrossDomainCookies(token, `${CADC_COOKIE_DOMAIN_URL}${token}`) + */ + // Step 3: Fetch user information + const user: User | null = await fetchUserInfo(token) + + // Step 4: Fetch user groups/roles + const { role: userRole, groups: userGroups } = await fetchUserGroups(token) + // Step 5: Combine all information into a complete user object + return { + id: credentials.username as string, + ...user, + accessToken: token, + role: userRole, + groups: userGroups, + } + } catch { + // Authentication failed + } + return null + }, + }), + ], + callbacks: { + async jwt({ token, user }) { + if (user) { + token.accessToken = user.accessToken + token.userId = user.id + token.role = user.role + token.groups = user.groups + token.affiliation = user.affiliation + token.name = user.firstName + ' ' + user.lastName + } + return token + }, + async session({ session, token }) { + // Add information to the session that will be available client-side + session.accessToken = token.accessToken + + if (session.user) { + session.user.id = token?.userId ? token.userId : '' + session.user.role = token?.role ? (token.role as string) : undefined + session.user.groups = token?.groups ? (token.groups as string[]) : [] + session.user.affiliation = token.affiliation + session.user.name = token.name + } + + return session + }, + }, + session: { + strategy: 'jwt', + maxAge: 7 * 24 * 60 * 60, // 7 days + }, + trustHost: true, +}) diff --git a/rafts/frontend/src/auth/cadc-auth/fetchUserGroups.ts b/rafts/frontend/src/auth/cadc-auth/fetchUserGroups.ts new file mode 100644 index 0000000..f1789a2 --- /dev/null +++ b/rafts/frontend/src/auth/cadc-auth/fetchUserGroups.ts @@ -0,0 +1,102 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { CANFAR_RAFT_REVIEWER_GROUP, CANFAR_USER_GROUPS_URL } from './constants' +import { TRoles } from '@/shared/model' +import { ROLE_CONTRIBUTOR, ROLE_REVIEWER } from '@/shared/constants' + +interface UserRoleInfo { + role: TRoles + groups: string[] +} + +export const fetchUserGroups = async (token?: string): Promise => { + try { + // Check membership by requesting the specific reviewer group directly + // GET /ac/groups/{groupName} returns 200 if the user is a member, 403/404 otherwise + const groupCheckUrl = `${CANFAR_USER_GROUPS_URL}/${CANFAR_RAFT_REVIEWER_GROUP}` + + const response = await fetch(groupCheckUrl, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + credentials: 'include', + }) + + const isRaftReviewer = response.ok + const assignedRole = isRaftReviewer ? ROLE_REVIEWER : ROLE_CONTRIBUTOR + + return { + role: assignedRole, + groups: isRaftReviewer ? [CANFAR_RAFT_REVIEWER_GROUP] : [], + } + } catch (error) { + console.error('[fetchUserGroups] Error checking group membership:', error) + return { role: ROLE_CONTRIBUTOR, groups: [] } + } +} diff --git a/rafts/frontend/src/auth/cadc-auth/fetchUserInfo.ts b/rafts/frontend/src/auth/cadc-auth/fetchUserInfo.ts new file mode 100644 index 0000000..517efb3 --- /dev/null +++ b/rafts/frontend/src/auth/cadc-auth/fetchUserInfo.ts @@ -0,0 +1,94 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { CANFAR_USER_URL } from './constants' +import { parseUserInfo } from '@/auth/cadc-auth/utils/parseUserInfo' +import { TPerson } from '@/shared/model' + +export const fetchUserInfo = async (token?: string): Promise => { + try { + const userResponse = await fetch(`${CANFAR_USER_URL}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + credentials: 'include', + }) + + if (!userResponse.ok) { + console.warn('Could not fetch user data:', userResponse.status, userResponse.statusText) + return null + } + + const userData = await userResponse.json() + return parseUserInfo(userData) + } catch (error) { + console.warn('Error fetching user data:', error) + return null + } +} diff --git a/rafts/frontend/src/auth/cadc-auth/setupCrossDomainCookies.ts b/rafts/frontend/src/auth/cadc-auth/setupCrossDomainCookies.ts new file mode 100644 index 0000000..6c65714 --- /dev/null +++ b/rafts/frontend/src/auth/cadc-auth/setupCrossDomainCookies.ts @@ -0,0 +1,98 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export const setupCrossDomainCookies = async ( + token: string, + endpointUrl: string, +): Promise => { + try { + const secondOptions = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + credentials: 'include' as RequestCredentials, + } + + const secondResponse = await fetch(endpointUrl, secondOptions) + + if (!secondResponse.ok) { + console.warn( + 'Cross-domain cookie setup failed:', + secondResponse.status, + secondResponse.statusText, + ) + return false + } + + return true + } catch (error) { + console.warn('Error setting up cross-domain cookies:', error) + return false + } +} diff --git a/rafts/frontend/src/auth/cadc-auth/utils/parseUserGroups.ts b/rafts/frontend/src/auth/cadc-auth/utils/parseUserGroups.ts new file mode 100644 index 0000000..2a83ed9 --- /dev/null +++ b/rafts/frontend/src/auth/cadc-auth/utils/parseUserGroups.ts @@ -0,0 +1,84 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export const parseUserGroups = (responseText: string): string[] => { + if (!responseText) { + return [] + } + + // Split the text by newlines to get individual lines + const lines = responseText.split('\n') + + // Filter out empty lines and any content type metadata + const groups = lines.filter( + (line) => + line.trim() !== '' && !line.startsWith('content/type') && !line.includes('content/type'), + ) + + // Remove any leading/trailing whitespace from each group name + return groups.map((group) => group.trim()) +} diff --git a/rafts/frontend/src/auth/cadc-auth/utils/parseUserInfo.ts b/rafts/frontend/src/auth/cadc-auth/utils/parseUserInfo.ts new file mode 100644 index 0000000..4acd601 --- /dev/null +++ b/rafts/frontend/src/auth/cadc-auth/utils/parseUserInfo.ts @@ -0,0 +1,179 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { + PROP_AUTHOR_FIRST_NAME, + PROP_AUTHOR_LAST_NAME, + PROP_AUTHOR_AFFILIATION, + PROP_AUTHOR_EMAIL, + PROP_AUTHOR_ORCID, +} from '@/shared/constants' +import { TPerson } from '@/shared/model' + +// Define types for the CADC user response structure +interface CADCIdentity { + '@type': string + $: string | number +} + +interface CADCIdentityWrapper { + identity: CADCIdentity +} + +interface CADCPersonalDetails { + firstName?: { $: string } + lastName?: { $: string } + email?: { $: string } + institute?: { $: string } +} + +interface CADCPosixDetails { + username?: { $: string } + uid?: { $: number } + gid?: { $: number } + homeDirectory?: { $: string } +} + +interface CADCInternalID { + uri?: { $: string } +} + +interface CADCUser { + internalID?: CADCInternalID + identities?: { + $: CADCIdentityWrapper[] + } + personalDetails?: CADCPersonalDetails + posixDetails?: CADCPosixDetails +} + +interface CADCUserResponse { + user?: CADCUser +} + +/** + * Creates a default empty TPerson object + * @returns An empty TPerson object with default values + */ +const createEmptyPerson = (): TPerson => ({ + [PROP_AUTHOR_FIRST_NAME]: '', + [PROP_AUTHOR_LAST_NAME]: '', + [PROP_AUTHOR_AFFILIATION]: '', + [PROP_AUTHOR_EMAIL]: '', + [PROP_AUTHOR_ORCID]: '', +}) + +/** + * Safely extracts a property from a nested object with $ notation + * @param obj - The object to extract from + * @param defaultValue - Default value if property doesn't exist + * @returns The extracted value or default + */ +const extractNestedProperty = (obj: { $?: T } | undefined, defaultValue: T): T => { + return obj && obj.$ !== undefined ? obj.$ : defaultValue +} + +/** + * Parses the CADC user info response into a valid Person object + * @param responseData - The JSON response from CADC users endpoint + * @returns A valid Person object conforming to the TPerson schema + */ +export const parseUserInfo = (responseData: CADCUserResponse | null): TPerson => { + // Default empty object if no response + if (!responseData || !responseData.user) { + return createEmptyPerson() + } + + try { + const user = responseData.user + + // Extract personal details + const personalDetails = user.personalDetails || {} + + // Extract identities to look for ORCID + const identities = user.identities?.$ || [] + + // Find ORCID identity if present + const orcidIdentity = identities.find( + (item: CADCIdentityWrapper) => item.identity && item.identity['@type'] === 'ORCID', + )?.identity + + // Build the person object + return { + [PROP_AUTHOR_FIRST_NAME]: extractNestedProperty(personalDetails.firstName, ''), + [PROP_AUTHOR_LAST_NAME]: extractNestedProperty(personalDetails.lastName, ''), + [PROP_AUTHOR_AFFILIATION]: extractNestedProperty(personalDetails.institute, ''), + [PROP_AUTHOR_EMAIL]: extractNestedProperty(personalDetails.email, ''), + [PROP_AUTHOR_ORCID]: orcidIdentity ? String(orcidIdentity.$) : '', + } + } catch (error) { + console.error('Error parsing user info:', error) + + // Return empty object in case of parsing error + return createEmptyPerson() + } +} diff --git a/rafts/frontend/src/auth/config/authorization.ts b/rafts/frontend/src/auth/config/authorization.ts new file mode 100644 index 0000000..f88a48b --- /dev/null +++ b/rafts/frontend/src/auth/config/authorization.ts @@ -0,0 +1,280 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export type UserRole = 'contributor' | 'reviewer' | 'admin' | undefined + +interface RouteConfig { + path: string + roles: UserRole[] + title: string + description?: string + icon?: string // Optional icon identifier + children?: Record // For nested routes + isPublic?: boolean // For routes that don't require authentication +} + +export const routes: Record = { + // Public routes + login: { + path: '/login', + roles: [], + title: 'Login', + isPublic: true, + }, + + // Dashboard + home: { + path: '/', + roles: ['contributor', 'reviewer', 'admin'], + title: 'Dashboard', + description: 'Overview of RAFT activities', + }, + + // RAFT Creation + createRaft: { + path: '/form/create', + roles: ['contributor', 'reviewer', 'admin'], + title: 'Create RAFT', + description: 'Submit a new research announcement', + }, + + // View RAFTs (all users) + viewRafts: { + path: '/view', + roles: ['contributor', 'reviewer', 'admin'], + title: 'View RAFTs', + description: 'Browse published announcements', + }, + + // Review System (reviewers and admins only) + review: { + path: '/review', + roles: ['reviewer', 'admin'], + title: 'Review RAFTs', + description: 'Review and moderate submitted RAFTs', + children: { + pending: { + path: '/review/pending', + roles: ['reviewer', 'admin'], + title: 'Pending Review', + description: 'RAFTs awaiting initial review', + }, + inProgress: { + path: '/review/in-progress', + roles: ['reviewer', 'admin'], + title: 'In Progress', + description: 'RAFTs currently being reviewed', + }, + approved: { + path: '/review/approved', + roles: ['reviewer', 'admin'], + title: 'Approved', + description: 'RAFTs that have been approved', + }, + rejected: { + path: '/review/rejected', + roles: ['reviewer', 'admin'], + title: 'Rejected', + description: 'RAFTs that have been rejected', + }, + raftDetail: { + path: '/review/rafts/:id', + roles: ['reviewer', 'admin'], + title: 'RAFT Details', + description: 'Detailed view of a RAFT submission', + }, + }, + }, + + // Admin Panel (admin only) + admin: { + path: '/admin', + roles: ['admin'], + title: 'Admin Panel', + description: 'System administration', + children: { + users: { + path: '/admin/users', + roles: ['admin'], + title: 'User Management', + description: 'Manage system users', + }, + settings: { + path: '/admin/settings', + roles: ['admin'], + title: 'System Settings', + description: 'Configure system settings', + }, + statistics: { + path: '/admin/statistics', + roles: ['admin'], + title: 'Statistics', + description: 'System usage statistics', + }, + }, + }, + + // User Profile (all authenticated users) + profile: { + path: '/profile', + roles: ['contributor', 'reviewer', 'admin'], + title: 'My Profile', + description: 'View and update your profile', + }, + + // Fallback Routes + unauthorized: { + path: '/unauthorized', + roles: [], + title: 'Unauthorized', + isPublic: true, + }, + notFound: { + path: '/not-found', + roles: [], + title: 'Not Found', + isPublic: true, + }, +} + +// Helper functions for route access + +/** + * Check if a user with the given role can access a specific route + */ +export const canAccessRoute = (path: string, role?: UserRole): boolean => { + if (!role) { + // Only allow access to public routes when no role is provided + return getRouteByPath(path)?.isPublic || false + } + + const route = getRouteByPath(path) + + // If route doesn't exist in config, deny access + if (!route) return false + + // If it's a public route, allow access + if (route.isPublic) return true + + // Check if the user's role is in the list of allowed roles + return route.roles.includes(role) +} + +/** + * Find a route configuration by path + */ +export const getRouteByPath = (path: string): RouteConfig | undefined => { + // Handle dynamic routes by replacing route parameters + const normalizedPath = path.replace(/\/[^/]+$/, '/:id') + + // First check top-level routes + const topLevelRoute = Object.values(routes).find( + (route) => route.path === path || route.path === normalizedPath, + ) + if (topLevelRoute) return topLevelRoute + + // Then check children routes + for (const parentRoute of Object.values(routes)) { + if (!parentRoute.children) continue + + const childRoute = Object.values(parentRoute.children).find( + (route) => route.path === path || route.path === normalizedPath, + ) + + if (childRoute) return childRoute + } + + return undefined +} + +/** + * Get all routes accessible to a specific role + */ +export const getAccessibleRoutes = (role?: UserRole): RouteConfig[] => { + if (!role) { + return Object.values(routes).filter((route) => route.isPublic) + } + + return Object.values(routes).filter((route) => route.isPublic || route.roles.includes(role)) +} + +/** + * Get all menu items that should be displayed for a specific role + * (excludes utility routes like unauthorized, notFound, etc.) + */ +export const getNavigationRoutes = (role?: UserRole): RouteConfig[] => { + const accessibleRoutes = getAccessibleRoutes(role) + + // Filter out utility routes and routes that might not be suitable for the main navigation + return accessibleRoutes.filter( + (route) => + !route.isPublic && + route.path !== '/unauthorized' && + route.path !== '/not-found' && + !route.path.includes('/:'), // Exclude dynamic routes from main navigation + ) +} diff --git a/rafts/frontend/src/auth/constants.ts b/rafts/frontend/src/auth/constants.ts new file mode 100644 index 0000000..68e3a85 --- /dev/null +++ b/rafts/frontend/src/auth/constants.ts @@ -0,0 +1,75 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export const CANFAR_LOGIN_URL = 'https://ws-cadc.canfar.net/ac/login' +export const RAFT_LOGIN_URL = 'http://localhost:4000/api/users/login' +export const SUCCESS = 'success' +export const ERROR = 'error' +export const defaultAuth = { + [SUCCESS]: false, + [ERROR]: null, +} diff --git a/rafts/frontend/src/auth/credentials.ts b/rafts/frontend/src/auth/credentials.ts new file mode 100644 index 0000000..4d11640 --- /dev/null +++ b/rafts/frontend/src/auth/credentials.ts @@ -0,0 +1,211 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import NextAuth from 'next-auth' +import CredentialsProvider from 'next-auth/providers/credentials' +import { RAFT_LOGIN_URL } from '@/auth/constants' +import { User } from 'next-auth' +import { DefaultSession } from 'next-auth' +import 'next-auth/jwt' + +declare module 'next-auth' { + interface Session { + accessToken?: string + role?: string + affiliation?: string + user?: { + id?: string + role?: string + groups?: string[] + affiliation?: string + } & DefaultSession['user'] + } + + interface User { + id?: string + name?: string | null + email?: string | null + accessToken?: string + role?: string + groups?: string[] + affiliation?: string + } +} + +declare module 'next-auth/jwt' { + interface JWT { + accessToken?: string + userId?: string + role?: string + groups?: string[] + affiliation?: string + } +} + +export const { + handlers: { GET, POST }, + auth, + signIn, + signOut, +} = NextAuth({ + providers: [ + CredentialsProvider({ + name: 'RAFT Login', + credentials: { + email: { label: 'Email', type: 'email' }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials): Promise { + try { + // Extract credentials + const email = credentials?.email ? String(credentials.email) : '' + const password = credentials?.password ? String(credentials.password) : '' + + // Build request options + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'RAFT-System/1.0', + }, + body: JSON.stringify({ + email, + password, + }), + } + + const response = await fetch(RAFT_LOGIN_URL, options) + + if (!response.ok) { + console.error('Login failed:', response.status, response.statusText) + return null + } + + // Parse the response data + const responseData = await response.json() + + // Extract user information and token from the nested structure + if (responseData?.data?.token && responseData?.data?.user) { + const userData = responseData.data.user + + // Create the user object with all required fields + const user: User = { + id: userData._id, + name: `${userData.firstName} ${userData.lastName}`, + email: userData.email, + accessToken: responseData.data.token, + role: userData.role, + affiliation: userData.affiliation, + } + + return user + } + + console.error('Invalid response structure') + return null + } catch (error) { + console.error('Auth error:', error) + return null + } + }, + }), + ], + callbacks: { + async jwt({ token, user }) { + // When user signs in, add their info to the JWT + if (user) { + token.accessToken = user.accessToken + token.userId = user.id + token.role = user.role + token.affiliation = user.affiliation + } + + return token + }, + async session({ session, token }) { + // Add token info to the session available client-side + session.accessToken = token.accessToken + + if (session.user) { + session.user.id = token.userId! + session.user.role = token.role + session.user.affiliation = token.affiliation + } + + return session + }, + }, + debug: process.env.NEXTAUTH_DEBUG === 'true' || process.env.NODE_ENV === 'development', + pages: { + signIn: '/login', + }, + session: { + strategy: 'jwt', + maxAge: 7 * 24 * 60 * 60, // 7 days + }, + trustHost: true, +}) diff --git a/rafts/frontend/src/auth/types.ts b/rafts/frontend/src/auth/types.ts new file mode 100644 index 0000000..d612a42 --- /dev/null +++ b/rafts/frontend/src/auth/types.ts @@ -0,0 +1,73 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { ERROR, SUCCESS } from '@/auth/constants' + +export type AuthState = { + [SUCCESS]: boolean + [ERROR]: string | null +} diff --git a/rafts/frontend/src/components/DOIRaftTable/ActionMenu.tsx b/rafts/frontend/src/components/DOIRaftTable/ActionMenu.tsx new file mode 100644 index 0000000..e358626 --- /dev/null +++ b/rafts/frontend/src/components/DOIRaftTable/ActionMenu.tsx @@ -0,0 +1,184 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React, { useState } from 'react' +import { Box, IconButton, Tooltip, CircularProgress } from '@mui/material' +import { Eye, Edit, SendHorizontal } from 'lucide-react' +import type { DOIData } from '@/types/doi' +import { useRouter } from '@/i18n/routing' +import { submitForReview } from '@/actions/submitForReview' +import { updateDOIStatus } from '@/actions/updateDOIStatus' +import { BACKEND_STATUS } from '@/shared/backendStatus' + +interface ActionMenuProps { + rowData: DOIData + onStatusChange?: (message: string, severity: 'success' | 'error') => void +} + +export default function ActionMenu({ rowData, onStatusChange }: ActionMenuProps) { + const [isSubmitting, setIsSubmitting] = useState(false) + const router = useRouter() + + const handleView = () => { + const raftId = rowData.identifier?.split?.('/')?.[1] as string + router.push(`/view/rafts/${raftId}`) + } + + const handleEdit = async () => { + const raftId = rowData.identifier?.split?.('/')?.[1] as string + + // If the RAFT is rejected, change status to "in progress" (draft) before editing + if (rowData.status === BACKEND_STATUS.REJECTED) { + setIsSubmitting(true) + try { + const result = await updateDOIStatus(raftId, BACKEND_STATUS.IN_PROGRESS, { + dataDirectory: rowData.dataDirectory, + previousStatus: rowData.status, + }) + if (!result.success) { + console.error('[ActionMenu] Failed to update status:', result.message) + onStatusChange?.(result.message || 'Failed to revert to draft', 'error') + setIsSubmitting(false) + return + } + onStatusChange?.('RAFT status changed to Draft.', 'success') + } catch (error) { + console.error('[ActionMenu] Error updating status:', error) + onStatusChange?.('An error occurred', 'error') + setIsSubmitting(false) + return + } + setIsSubmitting(false) + } + + router.push(`/form/edit/${raftId}`) + } + + const handleSubmitForReview = async () => { + const raftId = rowData.identifier?.split?.('/')?.[1] as string + if (!raftId) { + console.error('[ActionMenu] No DOI ID available') + onStatusChange?.('No DOI ID available', 'error') + return + } + + setIsSubmitting(true) + try { + const result = await submitForReview(raftId, rowData.dataDirectory) + if (result.success) { + onStatusChange?.('RAFT status changed to Review Ready.', 'success') + } else { + console.error('[ActionMenu] Failed to submit for review:', result.message) + onStatusChange?.(result.message || 'Failed to submit for review', 'error') + } + } catch (error) { + console.error('[ActionMenu] Error submitting for review:', error) + onStatusChange?.('An error occurred while submitting for review', 'error') + } finally { + setIsSubmitting(false) + } + } + + // Check if status allows actions (backend uses 'in progress' for draft) + const isDraft = rowData.status === BACKEND_STATUS.IN_PROGRESS + const isRejected = rowData.status === BACKEND_STATUS.REJECTED + const isEditable = isDraft || isRejected // Authors can edit drafts and rejected RAFTs + const canSubmitForReview = isDraft + + return ( + + + + + + + + + + + + + + + + {canSubmitForReview && ( + + + {isSubmitting ? : } + + + )} + + ) +} diff --git a/rafts/frontend/src/components/DOIRaftTable/RaftTable.tsx b/rafts/frontend/src/components/DOIRaftTable/RaftTable.tsx new file mode 100644 index 0000000..cfb55b6 --- /dev/null +++ b/rafts/frontend/src/components/DOIRaftTable/RaftTable.tsx @@ -0,0 +1,315 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + flexRender, + SortingState, +} from '@tanstack/react-table' +import type { DOIData } from '@/types/doi' +import { columns } from './columns' +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TablePagination, + TextField, + Box, + FormControl, + InputAdornment, + Snackbar, + Alert, + Skeleton, + useTheme, +} from '@mui/material' +import { Search } from 'lucide-react' +import { useRouter } from 'next/navigation' + +interface RaftTableProps { + data: DOIData[] + /** Callback to refresh data after status changes */ + onRefresh?: () => void + /** Show loading skeleton */ + isLoading?: boolean +} + +// Extend TanStack Table's TableMeta to include our custom properties +declare module '@tanstack/react-table' { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface TableMeta { + onStatusChange?: (message: string, severity: 'success' | 'error') => void + } +} + +export default function RaftTable({ data, onRefresh, isLoading = false }: RaftTableProps) { + const router = useRouter() + const theme = useTheme() + const [sorting, setSorting] = useState([]) + const [globalFilter, setGlobalFilter] = useState('') + const [snackbar, setSnackbar] = useState<{ + open: boolean + message: string + severity: 'success' | 'error' + }>({ open: false, message: '', severity: 'success' }) + + const handleStatusChange = (message: string, severity: 'success' | 'error') => { + setSnackbar({ open: true, message, severity }) + // Refresh data after status change + if (onRefresh) { + // Small delay to allow backend to process + setTimeout(() => { + onRefresh() + }, 500) + } else { + // Fallback: try router.refresh for server components + router.refresh() + } + } + + const handleSnackbarClose = () => { + setSnackbar((prev) => ({ ...prev, open: false })) + } + + const table = useReactTable({ + data, + columns, + state: { + sorting, + globalFilter, + }, + onSortingChange: setSorting, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { + pagination: { + pageSize: 10, + }, + }, + meta: { + onStatusChange: handleStatusChange, + }, + }) + + // Skeleton loading state + if (isLoading) { + return ( + + + + + + + + + {columns.map((_, index) => ( + + + + ))} + + + + {Array.from({ length: 5 }).map((_, index) => ( + + {columns.map((_, cellIndex) => ( + + + + ))} + + ))} + +
+
+ + + +
+ ) + } + + return ( + + + + setGlobalFilter(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + }} + size="small" + /> + + + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} + + ))} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results found + + + )} + +
+
+ + { + table.setPageIndex(page) + }} + onRowsPerPageChange={(e) => { + const size = e.target.value ? parseInt(e.target.value, 10) : 10 + table.setPageSize(size) + }} + /> + + + + {snackbar.message} + + +
+ ) +} diff --git a/rafts/frontend/src/components/DOIRaftTable/columns.tsx b/rafts/frontend/src/components/DOIRaftTable/columns.tsx new file mode 100644 index 0000000..7a43654 --- /dev/null +++ b/rafts/frontend/src/components/DOIRaftTable/columns.tsx @@ -0,0 +1,138 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { ColumnDef } from '@tanstack/react-table' +import type { DOIData } from '@/types/doi' +import ActionMenu from './ActionMenu' +import StatusBadge from '@/components/common/StatusBadge' +import { CITATION_PARTIAL_URL, STORAGE_PARTIAL_URL } from '@/utilities/constants' + +// Define table columns +export const columns: ColumnDef[] = [ + { + accessorKey: 'identifier', + header: 'Landing page', + cell: ({ row }) => { + const doi = (row.getValue('identifier') as string)?.split?.('/')?.[1] as string + return ( + + {doi} + + ) + }, + }, + { + accessorKey: 'title', + header: 'Title', + cell: ({ row }) => { + const title = row.getValue('title') as string + return
{title}
+ }, + }, + { + accessorKey: 'status', + header: 'Status', + cell: ({ row }) => { + const status = row.getValue('status') as string + return + }, + }, + { + accessorKey: 'dataDirectory', + header: 'Data Directory', + cell: ({ row }) => { + const path = row.getValue('dataDirectory') as string | null + return path ? ( + + {path} + + ) : ( + - + ) + }, + }, + { + id: 'actions', + header: 'Actions', + cell: ({ row, table }) => { + const onStatusChange = table.options.meta?.onStatusChange + return + }, + }, +] diff --git a/rafts/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx b/rafts/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 0000000..cc2efd8 --- /dev/null +++ b/rafts/frontend/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,257 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Component, ReactNode, useEffect, ErrorInfo } from 'react' +import { Button, Typography, Paper, Box, Alert } from '@mui/material' +import { RefreshCw } from 'lucide-react' + +interface ErrorBoundaryProps { + children: ReactNode + fallback?: ReactNode + onError?: (error: Error, errorInfo: ErrorInfo) => void + disableCapturing?: boolean +} + +interface ErrorBoundaryState { + hasError: boolean + error: Error | null +} + +/** + * Error boundary component for catching and handling client-side errors + * + * @param children - The components to be wrapped by the error boundary + * @param fallback - Optional custom fallback UI to show when an error occurs + * @param onError - Optional callback for error reporting + * @param disableCapturing - Disable global event capturing (useful in dev mode with Next.js overlay) + */ +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { hasError: false, error: null } + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + // Update state so the next render will show the fallback UI + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + // You can log the error to an error reporting service + console.error('Error caught by boundary:', error, errorInfo) + + if (this.props.onError) { + this.props.onError(error, errorInfo) + } + } + + resetError = () => { + this.setState({ hasError: false, error: null }) + } + + render() { + const { hasError, error } = this.state + const { children, fallback, disableCapturing = false } = this.props + + // If we have an error, render the fallback or default error UI + if (hasError) { + if (fallback) { + return <>{fallback} + } + + return ( + + + + Something went wrong + + + + {error?.message || 'An unexpected error occurred'} + + + + The application encountered an error. You can try refreshing the page or resetting the + component. + {process.env.NODE_ENV === 'development' && ( + + Note: You may need to create a new raft the old one does not meet new + requirements.{' '} + + )} + + + + + + + + + ) + } + + // If disableCapturing is false, we still want to capture unhandled errors + if (!disableCapturing) { + return {children} + } + + // No error, render children + return <>{children} + } +} + +// Component to capture global errors not caught by React's error boundary +interface ErrorEventCapturerProps { + children: ReactNode + setError: (state: ErrorBoundaryState) => void +} + +function ErrorEventCapturer({ children, setError }: ErrorEventCapturerProps) { + useEffect(() => { + // Define error handler for uncaught exceptions + const errorHandler = (event: ErrorEvent) => { + // Prevent handling the same error twice + if (event.error && event.error._handled) return + + // Mark error as handled to prevent duplication + if (event.error) { + event.error._handled = true + } + + console.error('Global error caught:', event) + setError({ + hasError: true, + error: event.error || new Error(event.message), + }) + } + + // Define promise rejection handler + const rejectionHandler = (event: PromiseRejectionEvent) => { + // Prevent handling the same rejection twice + if (event.reason && event.reason._handled) return + + // Mark error as handled to prevent duplication + if (event.reason) { + event.reason._handled = true + } + + console.error('Promise rejection caught:', event) + setError({ + hasError: true, + error: event.reason instanceof Error ? event.reason : new Error(String(event.reason)), + }) + } + + // Only add global listeners in production + // In development, let Next.js handle the errors for better debugging + if (process.env.NODE_ENV === 'production') { + window.addEventListener('error', errorHandler) + window.addEventListener('unhandledrejection', rejectionHandler) + + return () => { + window.removeEventListener('error', errorHandler) + window.removeEventListener('unhandledrejection', rejectionHandler) + } + } + + return undefined + }, [setError]) + + return <>{children} +} + +export default ErrorBoundary diff --git a/rafts/frontend/src/components/Form/AnnouncementForm.tsx b/rafts/frontend/src/components/Form/AnnouncementForm.tsx new file mode 100644 index 0000000..7ea149e --- /dev/null +++ b/rafts/frontend/src/components/Form/AnnouncementForm.tsx @@ -0,0 +1,461 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useForm, Controller } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { TObservation, observationSchema } from '@/shared/model' +import { useEffect, useMemo, useImperativeHandle, forwardRef } from 'react' + +// Hooks +import { useTranslations } from 'next-intl' +import { useSectionTutorial } from '@/hooks/useSectionTutorial' + +// Constants +import { + PROP_TOPIC, + PROP_OBJECT_NAME, + PROP_ABSTRACT, + PROP_FIGURE, + PROP_ACKNOWLEDGEMENTS, + TOPIC_OPTIONS, + PROP_PREVIOUS_RAFTS, +} from '@/shared/constants' + +// Components +import InputFormField from '@/components/Form/InputFormField' +import FileUploadImage from '@/components/Form/FileUpload/FileUploadImage' +import { FileReference, isFileReference, parseStoredAttachment } from '@/types/attachments' +import { + Button, + FormControl, + InputLabel, + Select, + MenuItem, + FormHelperText, + TextField, + Divider, + Typography, + Box, + Paper, + OutlinedInput, + Chip, + IconButton, + Tooltip, +} from '@mui/material' +import SaveIcon from '@mui/icons-material/Save' +import { HelpCircle } from 'lucide-react' +import dynamic from 'next/dynamic' +import { useTheme, Theme } from '@mui/material/styles' +import type { Step } from 'react-joyride' + +const SectionTutorial = dynamic(() => import('@/components/Tutorial/SectionTutorial'), { + ssr: false, +}) + +// Menu configuration for dropdown height/width +const ITEM_HEIGHT = 48 +const ITEM_PADDING_TOP = 8 +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250, + }, + }, +} + +// Style function for selected/unselected items +const getStyles = (name: string, selectedNames: readonly string[], theme: Theme) => { + return { + fontWeight: selectedNames.includes(name) + ? theme.typography.fontWeightMedium + : theme.typography.fontWeightRegular, + } +} + +export interface AnnouncementFormRef { + getCurrentValues: () => TObservation +} + +const AnnouncementForm = forwardRef< + AnnouncementFormRef, + { + onSubmitObservation: (values: TObservation) => void + formIsDirty: (value: boolean) => void + initialData?: TObservation | null + doiIdentifier?: string | null + } +>(({ onSubmitObservation, initialData = null, formIsDirty, doiIdentifier }, ref) => { + const theme = useTheme() + const t = useTranslations('submission_form') + const tTutorial = useTranslations('tutorial') + + // Tutorial setup + const { run, stepIndex, handleJoyrideCallback, startTutorial } = useSectionTutorial({ + sectionName: 'announcement', + autoStart: false, + }) + + // Tutorial steps + const tutorialSteps: Step[] = useMemo( + () => [ + { + target: '.announcement-section-header', + content: tTutorial('announcement_section_welcome'), + placement: 'bottom', + disableBeacon: true, + }, + { + target: '.topic-selector', + content: tTutorial('announcement_topic'), + placement: 'bottom', + }, + { + target: '.object-name-field', + content: tTutorial('announcement_object'), + placement: 'bottom', + }, + { + target: '.abstract-field', + content: tTutorial('announcement_abstract'), + placement: 'bottom', + }, + { + target: '.figure-upload', + content: tTutorial('announcement_figure'), + placement: 'top', + }, + { + target: '.save-announcement-button', + content: tTutorial('announcement_save'), + placement: 'top', + }, + ], + [tTutorial], + ) + + const { + register, + handleSubmit, + control, + reset, + setValue, + watch, + getValues, + formState: { errors, isDirty }, + } = useForm({ + resolver: zodResolver(observationSchema), + mode: 'onBlur', + defaultValues: initialData || { + [PROP_TOPIC]: undefined, + [PROP_OBJECT_NAME]: '', + [PROP_ABSTRACT]: '', + [PROP_FIGURE]: '', + [PROP_ACKNOWLEDGEMENTS]: '', + [PROP_PREVIOUS_RAFTS]: '', + }, + }) + + // Watch the figure field to pass to the FileUploadImage component + const figureValue = watch(PROP_FIGURE) + + // Reset form with initialData when it changes + useEffect(() => { + if (initialData) { + reset(initialData) + } + }, [initialData, reset]) + + useEffect(() => { + formIsDirty(isDirty) + }, [isDirty, formIsDirty]) + + // Expose getCurrentValues via ref for parent to get form values before submit + useImperativeHandle(ref, () => ({ + getCurrentValues: () => getValues(), + })) + + const onSubmit = (data: TObservation) => { + onSubmitObservation(data) + } + + // Handle image upload - accepts both base64 string and FileReference + const handleImageLoaded = (data: string | FileReference) => { + if (isFileReference(data)) { + // Store FileReference as JSON string in form field + setValue(PROP_FIGURE, JSON.stringify(data), { shouldValidate: true }) + } else { + setValue(PROP_FIGURE, data, { shouldValidate: true }) + } + } + + // Handle image removal + const handleImageClear = () => { + setValue(PROP_FIGURE, '', { shouldValidate: true }) + } + + return ( + <> + + + {/* Tutorial Help Button */} +
+ + + + + +
+ +
+

+ {t('observation_info')} +

+ + {/* Topic Selection */} + + {t('topic')} + { + // Ensure value is always an array + const value = Array.isArray(field.value) + ? field.value + : field.value + ? [field.value] + : [] + + return ( + + ) + }} + /> + {errors[PROP_TOPIC] && ( + {t(errors[PROP_TOPIC]?.message)} + )} + + + {/* Object Name */} + + + {/* Abstract */} + + ( + + )} + /> + + + {/* Figure Upload - Replacing the text input with image upload */} + + + + {t('figure_upload') || 'Figure Upload'} + + + + +
+ +
+ + {errors[PROP_FIGURE] && ( + + {t(errors[PROP_FIGURE]?.message) || 'Error with the figure upload'} + + )} + + {/* Acknowledgements (Optional) */} + + ( + + )} + /> + + + {/* Previously reported RAFTs */} + + ( + + )} + /> + + + {/* Submit Button */} + + +
+ + ) +}) + +AnnouncementForm.displayName = 'AnnouncementForm' + +export default AnnouncementForm diff --git a/rafts/frontend/src/components/Form/AuthorForm.tsx b/rafts/frontend/src/components/Form/AuthorForm.tsx new file mode 100644 index 0000000..4d4b511 --- /dev/null +++ b/rafts/frontend/src/components/Form/AuthorForm.tsx @@ -0,0 +1,540 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' +//Libs +import { useForm, useFieldArray } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { TAuthor, authorSchema } from '@/shared/model' +import { useEffect, useMemo, useState, useImperativeHandle, forwardRef, useCallback } from 'react' + +// Hooks +import { useTranslations } from 'next-intl' +import { useSectionTutorial } from '@/hooks/useSectionTutorial' + +// Constants +import { + PROP_CONTRIBUTING_AUTHORS, + PROP_COLLABORATIONS, + PROP_CORRESPONDING_AUTHOR, + PROP_AUTHOR_FIRST_NAME, + PROP_AUTHOR_LAST_NAME, + PROP_AUTHOR_AFFILIATION, + PROP_AUTHOR_EMAIL, + PROP_AUTHOR_ORCID, +} from '@/shared/constants' +import { EMPTY_AUTHOR } from '@/components/Form/constants' + +// Components +import InputFormField from '@/components/Form/InputFormField' +import Button from '@mui/material/Button' +import DeleteIcon from '@mui/icons-material/Delete' +import AddIcon from '@mui/icons-material/Add' +import SaveIcon from '@mui/icons-material/Save' +import dynamic from 'next/dynamic' +import { Paper, IconButton, Tooltip } from '@mui/material' +import { HelpCircle } from 'lucide-react' +import type { Step } from 'react-joyride' + +const SectionTutorial = dynamic(() => import('@/components/Tutorial/SectionTutorial'), { + ssr: false, +}) + +export interface AuthorFormRef { + getCurrentValues: () => TAuthor +} + +const AuthorForm = forwardRef< + AuthorFormRef, + { + onSubmitAuthor: (values: TAuthor) => void + formIsDirty: (value: boolean) => void + initialData?: TAuthor | null + } +>(({ onSubmitAuthor, initialData = null, formIsDirty }, ref) => { + const t = useTranslations('submission_form') + const tTutorial = useTranslations('tutorial') + + // Tutorial setup + const { run, stepIndex, handleJoyrideCallback, startTutorial } = useSectionTutorial({ + sectionName: 'author', + autoStart: false, + }) + + // Tutorial steps + const tutorialSteps: Step[] = useMemo( + () => [ + { + target: '.author-section-header', + content: tTutorial('author_section_welcome'), + placement: 'bottom', + disableBeacon: true, + }, + { + target: '.corresponding-author-section', + content: tTutorial('author_corresponding'), + placement: 'bottom', + }, + { + target: '.contributing-authors-section', + content: tTutorial('author_contributing'), + placement: 'bottom', + }, + { + target: '.collaborations-section', + content: tTutorial('author_collaborations'), + placement: 'bottom', + }, + { + target: '.add-author-button', + content: tTutorial('author_add_button'), + placement: 'top', + }, + { + target: '.save-author-button', + content: tTutorial('author_save'), + placement: 'top', + }, + ], + [tTutorial], + ) + + const { + register, + handleSubmit, + control, + reset, + setValue, + getValues, + formState: { errors, isDirty }, + } = useForm({ + resolver: zodResolver(authorSchema), + mode: 'onBlur', + defaultValues: initialData || { + [PROP_CORRESPONDING_AUTHOR]: EMPTY_AUTHOR, + [PROP_CONTRIBUTING_AUTHORS]: [], + [PROP_COLLABORATIONS]: [], + }, + }) + + const { fields, append, remove } = useFieldArray({ + control, + name: PROP_CONTRIBUTING_AUTHORS, + }) + + // Track collaboration count locally to avoid watch() re-renders on every keystroke + const [collaborationCount, setCollaborationCount] = useState( + () => (initialData?.[PROP_COLLABORATIONS] || []).length, + ) + + const appendCollaboration = useCallback(() => { + const current = getValues(PROP_COLLABORATIONS) || [] + setValue(PROP_COLLABORATIONS, [...current, '']) + setCollaborationCount((c) => c + 1) + }, [getValues, setValue]) + + const removeCollaboration = useCallback( + (index: number) => { + const current = getValues(PROP_COLLABORATIONS) || [] + setValue( + PROP_COLLABORATIONS, + current.filter((_: string, i: number) => i !== index), + ) + setCollaborationCount((c) => c - 1) + }, + [getValues, setValue], + ) + + // Reset form with initialData when it changes (e.g., navigating back to this step) + useEffect(() => { + if (initialData) { + reset(initialData) + setCollaborationCount((initialData[PROP_COLLABORATIONS] || []).length) + } + }, [initialData, reset]) + + useEffect(() => { + formIsDirty(isDirty) + }, [isDirty, formIsDirty]) + + // Expose getCurrentValues via ref for parent to get form values before submit + useImperativeHandle(ref, () => ({ + getCurrentValues: () => getValues(), + })) + + const onSubmit = (data: TAuthor) => { + onSubmitAuthor(data) + } + + return ( + <> + + + {/* Tutorial Help Button */} +
+ + + + + +
+ +
+

+ {t('author_info')} +

+

{t('author_info_helper')}

+ + {/* Corresponding Author */} +
+ {t('cor_author')} + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ + {/* Contributing Authors */} +
+ {t('con_authors')} + + {fields.map((field, index) => ( +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+ ))} + + +
+ + {/* Collaborations */} +
+ {t('collaborations')} + + {Array.from({ length: collaborationCount }, (_, index) => ( +
+ + + +
+ ))} + + +
+ + +
+
+ + ) +}) + +AuthorForm.displayName = 'AuthorForm' + +export default AuthorForm diff --git a/rafts/frontend/src/components/Form/FileUpload/ADESFileUpload.tsx b/rafts/frontend/src/components/Form/FileUpload/ADESFileUpload.tsx new file mode 100644 index 0000000..fbe7bf8 --- /dev/null +++ b/rafts/frontend/src/components/Form/FileUpload/ADESFileUpload.tsx @@ -0,0 +1,545 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import React, { useState, useRef, ChangeEvent, useEffect } from 'react' +import { + Button, + Box, + Typography, + CircularProgress, + Alert, + IconButton, + MenuItem, + Select, + FormControl, + InputLabel, + SelectChangeEvent, + useTheme, + LinearProgress, +} from '@mui/material' +import { FileText, X, Eye, EyeOff, Upload, Cloud } from 'lucide-react' +import TextPreview from './TextPreview' +import { useADESValidation } from '@/hooks/useADESValidation' +import type { ADESFileKind } from '@/actions/adesValidation.types' +import { useAttachmentUpload } from '@/hooks/useAttachmentUpload' +import { + FileReference, + AttachmentValue, + isFileReference, + ATTACHMENT_CONFIGS, +} from '@/types/attachments' + +const FILE_TYPE_LABELS: Record = { + xml: 'XML (.xml)', + psv: 'Pipe Separated Values (.psv)', + mpc: 'Minor Planet Center (.mpc)', +} + +const ACCEPTED: Record = { + xml: '.xml,text/xml,application/xml', + psv: '.psv,text/plain', + mpc: '.mpc,text/plain,application/octet-stream', +} + +export interface ADESFileUploadProps { + /** Callback when file is cleared */ + onClear: () => void + /** Callback when file is uploaded - receives text content or FileReference */ + onFileUpload?: (data: string | FileReference) => void + /** Initial text value - can be text string or FileReference */ + initialText?: AttachmentValue + /** Maximum file size in bytes */ + maxSize?: number + /** Label for the upload area */ + label?: string + /** Hint text shown in upload area */ + hint?: string + /** Whether to show text preview */ + showPreview?: boolean + /** DOI identifier - if provided, uploads to VOSpace instead of inline */ + doiIdentifier?: string + /** Custom filename for the uploaded file */ + customFilename?: string +} + +/** + * Component for handling ADES file uploads (XML/PSV/MPC) with validation + * + * Supports two modes: + * 1. Legacy mode (no doiIdentifier): Stores text content inline + * 2. VOSpace mode (with doiIdentifier): Uploads to VOSpace and returns FileReference + */ +const ADESFileUpload: React.FC = ({ + onClear, + onFileUpload, + initialText = '', + maxSize = 5 * 1024 * 1024, + label = 'Upload ADES File', + hint = 'Select XML, PSV, or MPC file', + showPreview = true, + doiIdentifier, + customFilename, +}) => { + const theme = useTheme() + const [fileKind, setFileKind] = useState('xml') + const [fileName, setFileName] = useState('') + const [fileSizeError, setFileSizeError] = useState('') + const [textPreview, setTextPreview] = useState('') + const [showTextPreview, setShowTextPreview] = useState(false) + const [hasFile, setHasFile] = useState(false) + const [currentFileRef, setCurrentFileRef] = useState(null) + const fileInputRef = useRef(null) + + // Use our custom validation hook + const { isValidating, validationResult, validationError, validateFile, resetValidation } = + useADESValidation() + + // Use attachment upload hook when doiIdentifier is provided + const { + isUploading, + progress, + error: uploadError, + uploadFile: uploadToVOSpace, + resolveAttachment, + canUpload, + deleteFile, + } = useAttachmentUpload({ + doiIdentifier, + config: ATTACHMENT_CONFIGS.astrometry, + }) + + // Determine if we're in VOSpace upload mode + const useVOSpaceUpload = Boolean(doiIdentifier) && canUpload + + // Resolve initial text on mount or when it changes + useEffect(() => { + const resolveInitialText = async () => { + if (!initialText) { + setHasFile(false) + setTextPreview('') + setCurrentFileRef(null) + return + } + + // If it's a FileReference, store it and resolve to displayable content + if (isFileReference(initialText)) { + setCurrentFileRef(initialText) + setFileName(initialText.filename) + setHasFile(true) + const resolved = await resolveAttachment(initialText) + if (resolved) { + setTextPreview(resolved.length > 500 ? resolved.substring(0, 500) + '...' : resolved) + setShowTextPreview(true) + } + } else if (typeof initialText === 'string' && initialText.length > 0) { + // It's inline text content + setHasFile(true) + setCurrentFileRef(null) + setTextPreview( + initialText.length > 500 ? initialText.substring(0, 500) + '...' : initialText, + ) + setShowTextPreview(true) + } + } + + resolveInitialText() + }, [initialText, resolveAttachment]) + + const handleFileKindChange = (event: SelectChangeEvent) => { + setFileKind(event.target.value as ADESFileKind) + resetValidation() + } + + const handleFileChange = (event: ChangeEvent) => { + const files = event.target.files + if (files && files.length > 0) { + readAndUpload(files[0]) + } + if (event.target) event.target.value = '' + } + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault() + event.stopPropagation() + if (event.dataTransfer.files && event.dataTransfer.files.length > 0) { + readAndUpload(event.dataTransfer.files[0]) + } + } + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault() + event.stopPropagation() + } + + const readAndUpload = (file: File) => { + resetValidation() + setFileSizeError('') + setFileName(file.name) + + if (file.size > maxSize) { + setFileSizeError(`File too large (max ${(maxSize / 1024 / 1024).toFixed(2)} MB)`) + return + } + + const reader = new FileReader() + reader.onload = (e: ProgressEvent) => { + const fileText = e.target?.result as string + if (showPreview) { + setTextPreview(fileText.length > 500 ? fileText.substring(0, 500) + '...' : fileText) + } + setShowTextPreview(true) + uploadAndValidateFile(file, fileText) + } + reader.onerror = () => { + resetValidation() + } + reader.readAsText(file) + } + + const uploadAndValidateFile = async (file: File, fileText: string) => { + try { + // Use our validation hook with direct result access + const { success, result } = await validateFile(file, fileKind) + + // Check validation success and result + if ( + success && + result && + Array.isArray(result.results) && + result.results.length > 0 && + result.results.every((r) => r.valid) + ) { + setHasFile(true) + + // If VOSpace upload is enabled, upload to VOSpace + if (useVOSpaceUpload) { + const filename = customFilename || `astrometry.${fileKind}` + const uploadResult = await uploadToVOSpace(file, filename) + + if (uploadResult.success && uploadResult.fileReference) { + setCurrentFileRef(uploadResult.fileReference) + if (onFileUpload) { + onFileUpload(uploadResult.fileReference) + } + } + } else { + // Legacy mode: pass text content + setCurrentFileRef(null) + if (onFileUpload) { + onFileUpload(fileText) + } + } + } + } catch (err) { + console.error('Error during validation:', err) + } + } + + const handleClear = async () => { + // If we have a FileReference and doiIdentifier, delete from VOSpace + if (currentFileRef && doiIdentifier) { + try { + await deleteFile(currentFileRef.filename) + } catch (err) { + console.error('[ADESFileUpload] Failed to delete from VOSpace:', err) + // Continue with UI clear even if delete fails + } + } + + resetValidation() + setFileName('') + setTextPreview('') + setShowTextPreview(false) + setHasFile(false) + setCurrentFileRef(null) + setFileSizeError('') + if (fileInputRef.current) fileInputRef.current.value = '' + onClear() + } + + const togglePreview = () => setShowTextPreview((prev) => !prev) + + // Combine loading states + const showLoading = isValidating || isUploading + const displayError = validationError || uploadError || fileSizeError + + return ( + + + {label} + {useVOSpaceUpload && ( + + + + )} + + + File Type + + + + {!hasFile ? ( + fileInputRef.current?.click()} + onDrop={showLoading || !doiIdentifier ? undefined : handleDrop} + onDragOver={handleDragOver} + > + {!doiIdentifier ? ( + + + + Save the form first to enable file uploads + + + ) : showLoading ? ( + + + {isValidating && ( + + Validating file... + + )} + {isUploading && ( + + + + Uploading to cloud storage... + + + )} + + ) : ( + <> + {useVOSpaceUpload ? ( + + ) : ( + + )} + + {hint} + + + + )} + + ) : ( + + + + + {currentFileRef && ( + + + Stored + + )} + + + {fileName} + + + {showPreview && textPreview && ( + + {showTextPreview ? : } + + )} + + + + + + {showPreview && textPreview && showTextPreview && } + + )} + + {displayError && ( + + {displayError} + + )} + + {validationResult && ( + + + Validation result for {validationResult.filename} + + {validationResult.xml_info && ( + + + Root element: {validationResult.xml_info.root_element} +
+ Version: {validationResult.xml_info.version} +
+ {validationResult.xml_info.attributes && ( + + Attributes:{' '} + {Object.entries(validationResult.xml_info.attributes) + .map(([k, v]) => `${k}="${v}"`) + .join(', ')} + + )} +
+ )} + {validationResult.results && ( + + {validationResult.results.map((res, i) => ( +
  • + + Type: {res.type} | Status:{' '} + + {res.valid ? 'Valid' : 'Invalid'} + +
    + Message: {res.message} +
    +
  • + ))} +
    + )} +
    + )} +
    + ) +} + +export default ADESFileUpload diff --git a/rafts/frontend/src/components/Form/FileUpload/FileUpload.tsx b/rafts/frontend/src/components/Form/FileUpload/FileUpload.tsx new file mode 100644 index 0000000..97d59b9 --- /dev/null +++ b/rafts/frontend/src/components/Form/FileUpload/FileUpload.tsx @@ -0,0 +1,356 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import React, { useState, useRef, ChangeEvent } from 'react' +import { Button, Box, Typography, CircularProgress, Alert, Paper, Chip } from '@mui/material' +import { Upload, FileText, X, Check, AlertTriangle } from 'lucide-react' +import { TRaftContext } from '@/context/types' + +interface FileUploadProps { + onFileLoaded: (data: TRaftContext) => void + onError?: (error: string) => void + accept?: string + maxSize?: number // in bytes + label?: string + hint?: string + showPreview?: boolean +} + +/** + * Component for handling JSON file uploads in forms + */ +const FileUpload: React.FC = ({ + onFileLoaded, + onError, + accept = '.json', + maxSize = 5 * 1024 * 1024, // 5MB default + label = 'Upload JSON File', + hint = 'Select or drag & drop a JSON file', + showPreview = true, +}) => { + const [isLoading, setIsLoading] = useState(false) + const [fileName, setFileName] = useState('') + const [fileSize, setFileSize] = useState(0) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + const [preview, setPreview] = useState(null) + + const fileInputRef = useRef(null) + + const handleFileChange = (event: ChangeEvent) => { + const files = event.target.files + + if (files && files.length > 0) { + processFile(files[0]) + } + + // Reset the input value to allow selecting the same file again + if (event.target) { + event.target.value = '' + } + } + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault() + event.stopPropagation() + + if (event.dataTransfer.files && event.dataTransfer.files.length > 0) { + processFile(event.dataTransfer.files[0]) + } + } + + const processFile = (file: File) => { + // Reset states + setError(null) + setSuccess(false) + setPreview(null) + + // Check file type + if (!file.name.endsWith('.json') && !file.type.includes('application/json')) { + const errorMsg = 'Please upload a JSON file' + setError(errorMsg) + if (onError) onError(errorMsg) + return + } + + // Check file size + if (file.size > maxSize) { + const errorMsg = `File is too large. Maximum size is ${maxSize / 1024 / 1024}MB` + setError(errorMsg) + if (onError) onError(errorMsg) + return + } + + setIsLoading(true) + setFileName(file.name) + setFileSize(file.size) + + const reader = new FileReader() + + reader.onload = (e) => { + try { + const content = e.target?.result as string + const parsedData = JSON.parse(content) + + // Generate preview + if (showPreview) { + setPreview( + JSON.stringify(parsedData, null, 2).substring(0, 500) + + (JSON.stringify(parsedData, null, 2).length > 500 ? '...' : ''), + ) + } + + onFileLoaded(parsedData) + setSuccess(true) + } catch { + const errorMsg = 'Invalid JSON format' + setError(errorMsg) + if (onError) onError(errorMsg) + } finally { + setIsLoading(false) + } + } + + reader.onerror = () => { + const errorMsg = 'Error reading file' + setError(errorMsg) + if (onError) onError(errorMsg) + setIsLoading(false) + } + + reader.readAsText(file) + } + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault() + event.stopPropagation() + } + + const handleClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click() + } + } + + const handleClear = () => { + setFileName('') + setFileSize(0) + setError(null) + setSuccess(false) + setPreview(null) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + + return ( + + + {label} + + + + + + {isLoading ? ( + + ) : ( + <> + {fileName ? ( + + + + + + {fileName} + + + + + + + + + {success && ( + + + + File loaded successfully + + + )} + + ) : ( + <> + + + {hint} + + + + )} + + )} + + + {error && ( + + {error} + + )} + + {preview && !error && ( + + + Preview (truncated) + +
    {preview}
    +
    + )} +
    + ) +} + +export default FileUpload diff --git a/rafts/frontend/src/components/Form/FileUpload/FileUploadImage.tsx b/rafts/frontend/src/components/Form/FileUpload/FileUploadImage.tsx new file mode 100644 index 0000000..dbbccc6 --- /dev/null +++ b/rafts/frontend/src/components/Form/FileUpload/FileUploadImage.tsx @@ -0,0 +1,536 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import React, { useState, useRef, ChangeEvent, useEffect } from 'react' +import { + Button, + Box, + Typography, + CircularProgress, + Alert, + IconButton, + LinearProgress, +} from '@mui/material' +import { Image as ImageIcon, X, Upload, Cloud } from 'lucide-react' +import ImageComponent from 'next/image' +import { useAttachmentUpload } from '@/hooks/useAttachmentUpload' +import { + FileReference, + AttachmentValue, + isFileReference, + isBase64DataUrl, + ATTACHMENT_CONFIGS, +} from '@/types/attachments' + +interface FileUploadImageProps { + /** Callback when image is loaded - receives base64 data URL or FileReference */ + onImageLoaded: (data: string | FileReference) => void + /** Callback when image is cleared */ + onClear: () => void + /** Initial image value - can be base64 string or FileReference */ + initialImage?: AttachmentValue + /** Accepted file types */ + accept?: string + /** Maximum file size in bytes */ + maxSize?: number + /** Label for the upload area */ + label?: string + /** Hint text shown in upload area */ + hint?: string + /** DOI identifier - if provided, uploads to VOSpace instead of base64 */ + doiIdentifier?: string + /** Custom filename for the uploaded file */ + customFilename?: string +} + +/** + * Component for handling image uploads (PNG/JPG) in forms + * + * Supports two modes: + * 1. Legacy mode (no doiIdentifier): Converts to base64 and stores inline + * 2. VOSpace mode (with doiIdentifier): Uploads to VOSpace and returns FileReference + */ +const FileUploadImage: React.FC = ({ + onImageLoaded, + onClear, + initialImage, + accept = 'image/png, image/jpeg, image/jpg', + maxSize = 5 * 1024 * 1024, // 5MB default + label = 'Upload Image', + hint = 'Select or drag & drop an image (PNG, JPG)', + doiIdentifier, + customFilename, +}) => { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [imagePreview, setImagePreview] = useState('') + const [currentFileRef, setCurrentFileRef] = useState(null) + const [isPreviewLoading, setIsPreviewLoading] = useState(false) + + const fileInputRef = useRef(null) + // Track the last resolved image to avoid re-fetching on unrelated re-renders + const lastResolvedRef = useRef(null) + + // Use attachment upload hook when doiIdentifier is provided + const { + isUploading, + progress, + error: uploadError, + uploadFile: uploadToVOSpace, + resolveAttachment, + canUpload, + deleteFile, + } = useAttachmentUpload({ + doiIdentifier, + config: ATTACHMENT_CONFIGS.figure, + }) + + // Determine if we're in VOSpace upload mode + const useVOSpaceUpload = Boolean(doiIdentifier) && canUpload + + // Resolve initial image on mount or when it changes + useEffect(() => { + const resolveInitialImage = async () => { + if (!initialImage) { + if (lastResolvedRef.current !== null) { + setImagePreview('') + setCurrentFileRef(null) + setIsPreviewLoading(false) + lastResolvedRef.current = null + } + return + } + + // Create a stable key for comparison + const imageKey = isFileReference(initialImage) + ? `ref:${initialImage.filename}` + : typeof initialImage === 'string' + ? `str:${initialImage.substring(0, 50)}` + : null + + // Skip if we've already resolved this exact image + if (imageKey && lastResolvedRef.current === imageKey) { + return + } + + // If it's a FileReference, store it and resolve to displayable content + if (isFileReference(initialImage)) { + setCurrentFileRef(initialImage) + setIsPreviewLoading(true) // Start loading spinner for API fetch + const resolved = await resolveAttachment(initialImage) + if (resolved) { + setImagePreview(resolved) + lastResolvedRef.current = imageKey + // Note: isPreviewLoading will be set to false by onLoad handler + } else { + setIsPreviewLoading(false) + } + } else if (typeof initialImage === 'string' && isBase64DataUrl(initialImage)) { + // It's a base64 string - no loading needed + setImagePreview(initialImage) + setCurrentFileRef(null) + setIsPreviewLoading(false) + lastResolvedRef.current = imageKey + } + } + + resolveInitialImage() + }, [initialImage, resolveAttachment]) + + const handleFileChange = (event: ChangeEvent) => { + const files = event.target.files + + if (files && files.length > 0) { + processFile(files[0]) + } + + // Reset the input value to allow selecting the same file again + if (event.target) { + event.target.value = '' + } + } + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault() + event.stopPropagation() + + if (event.dataTransfer.files && event.dataTransfer.files.length > 0) { + processFile(event.dataTransfer.files[0]) + } + } + + const processFile = async (file: File) => { + // Reset states + setError(null) + + // Check file type + if ( + !file.type.includes('image/png') && + !file.type.includes('image/jpeg') && + !file.type.includes('image/jpg') + ) { + setError('Please upload a PNG or JPG image') + return + } + + // Check file size + if (file.size > maxSize) { + setError(`File is too large. Maximum size is ${maxSize / 1024 / 1024}MB`) + return + } + + // If VOSpace upload is enabled, upload to VOSpace + if (useVOSpaceUpload) { + setIsLoading(true) + try { + // Build filename with proper extension + const extension = getExtensionFromType(file.type) + const baseFilename = customFilename || 'figure' + // Ensure filename has the correct extension + const filename = baseFilename.includes('.') ? baseFilename : `${baseFilename}${extension}` + const result = await uploadToVOSpace(file, filename) + + if (result.success && result.fileReference) { + // Show preview immediately + const reader = new FileReader() + reader.onload = (e) => { + setImagePreview(e.target?.result as string) + } + reader.readAsDataURL(file) + + setCurrentFileRef(result.fileReference) + onImageLoaded(result.fileReference) + } else { + setError(result.error || 'Upload failed') + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Upload failed') + } finally { + setIsLoading(false) + } + return + } + + // Legacy mode: convert to base64 + setIsLoading(true) + const reader = new FileReader() + + reader.onload = (e) => { + try { + const base64Data = e.target?.result as string + setImagePreview(base64Data) + setCurrentFileRef(null) + onImageLoaded(base64Data) + } catch (err) { + console.error(err) + setError('Error processing image') + } finally { + setIsLoading(false) + } + } + + reader.onerror = () => { + setError('Error reading file') + setIsLoading(false) + } + + reader.readAsDataURL(file) + } + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault() + event.stopPropagation() + } + + const handleClick = () => { + if (!imagePreview && fileInputRef.current) { + fileInputRef.current.click() + } + } + + const handleClear = async () => { + // If we have a FileReference and doiIdentifier, delete from VOSpace + if (currentFileRef && doiIdentifier) { + try { + await deleteFile(currentFileRef.filename) + } catch (err) { + console.error('[FileUploadImage] Failed to delete from VOSpace:', err) + // Continue with UI clear even if delete fails + } + } + + setError(null) + setImagePreview('') + setCurrentFileRef(null) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + onClear() + } + + // Combine loading states + const showLoading = isLoading || isUploading + const displayError = error || uploadError + + // Helper to get file extension from MIME type + function getExtensionFromType(mimeType: string): string { + const extensions: Record = { + 'image/png': '.png', + 'image/jpeg': '.jpg', + 'image/jpg': '.jpg', + } + return extensions[mimeType] || '.png' + } + + return ( + + + + {label} + + {useVOSpaceUpload && ( + + + + )} + + + + + {!imagePreview ? ( + + {!doiIdentifier ? ( + + + + Save the form first to enable file uploads + + + ) : showLoading ? ( + + + {isUploading && ( + + + + Uploading to cloud storage... + + + )} + + ) : ( + <> + + + {hint} + + + + )} + + ) : ( + + {/* Show spinner while loading image from API */} + {isPreviewLoading && ( + + + + )} + {/* Use regular img for API routes, Next.js Image for base64 */} + {imagePreview.startsWith('/api/') ? ( + // eslint-disable-next-line @next/next/no-img-element + Uploaded preview setIsPreviewLoading(false)} + onError={() => setIsPreviewLoading(false)} + /> + ) : ( + setIsPreviewLoading(false)} + /> + )} + {currentFileRef && ( + + + + Stored + + + )} + + + + + )} + + {displayError && ( + + {displayError} + + )} + + ) +} + +export default FileUploadImage diff --git a/rafts/frontend/src/components/Form/FileUpload/JsonImportComponent.tsx b/rafts/frontend/src/components/Form/FileUpload/JsonImportComponent.tsx new file mode 100644 index 0000000..6465706 --- /dev/null +++ b/rafts/frontend/src/components/Form/FileUpload/JsonImportComponent.tsx @@ -0,0 +1,489 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import React, { useState } from 'react' +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Box, + Typography, + Alert, + AlertTitle, + Divider, + IconButton, + useTheme, + useMediaQuery, + Tabs, + Tab, + Stepper, + Step, + StepLabel, + Paper, +} from '@mui/material' +import { FileJson, Download, Upload, X, AlertTriangle, Check } from 'lucide-react' +import { useTranslations } from 'next-intl' +import { useRaftForm } from '@/context/RaftFormContext' +import { TRaftContext } from '@/context/types' +import FileUpload from './FileUpload' +import { downloadJsonAsFile } from './utils' + +/** + * Component that provides JSON import/export functionality for the RAFT form + * Shows a button that opens a dialog for importing/exporting JSON data + */ +const JsonImportComponent: React.FC = () => { + const [dialogOpen, setDialogOpen] = useState(false) + const [activeTab, setActiveTab] = useState(0) + const [importStep, setImportStep] = useState(0) + const [tempFormData, setTempFormData] = useState(null) + const [importSuccess, setImportSuccess] = useState(false) + + const t = useTranslations('exim_form') + const theme = useTheme() + const fullScreen = useMediaQuery(theme.breakpoints.down('md')) + + // Access the RAFT form context to get current form data + const { setFormFromFile, raftData } = useRaftForm() + + // Open the main dialog + const handleOpenDialog = () => { + setDialogOpen(true) + // Reset states when opening dialog + setImportStep(0) + setImportSuccess(false) + setTempFormData(null) + } + + // Close the main dialog + const handleCloseDialog = () => { + setDialogOpen(false) + } + + // Handle tab change + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setActiveTab(newValue) + } + + // Handle when a file is loaded by the FileUpload component + const handleFileLoaded = (data: TRaftContext) => { + setTempFormData(data) + setImportStep(1) // Move to confirmation step + } + + // Handle confirmation of import + const confirmImport = () => { + if (tempFormData) { + setFormFromFile(tempFormData) + setImportStep(2) // Move to success step + setImportSuccess(true) + } + } + + // Reset the import process + const resetImport = () => { + setImportStep(0) + setTempFormData(null) + setImportSuccess(false) + } + + // Handle export of current form data + const handleExport = () => { + if (raftData) { + const filename = `raft-${new Date().toISOString().split('T')[0]}.json` + downloadJsonAsFile(raftData, filename) + } + } + + // Import process steps + const importSteps = [ + { label: t('select_file_step') || 'Select File', icon: }, + { label: t('confirm_step') || 'Confirm', icon: }, + { label: t('complete_step') || 'Complete', icon: }, + ] + + // Render import content based on current step + const renderImportContent = () => { + switch (importStep) { + case 0: // File selection + return ( + + + {t('select_file_to_import') || 'Select a RAFT JSON file to import'} + + + {t('import_json_description') || + 'The file should be a valid RAFT JSON export. Importing will replace your current form data.'} + + + + + + ) + + case 1: // Confirmation + return ( + + + {t('confirm_import') || 'Confirm Data Import'} + + {t('confirm_import_message') || + 'Importing will replace any existing data in your form. This action cannot be undone.'} + + + + + + {t('file_preview') || 'File Preview'}: + + + {JSON.stringify(tempFormData, null, 2)} + + + + + + + + + ) + + case 2: // Success + return ( + + + + + + + {t('import_success') || 'Import Successful!'} + + + + {t('import_success_message') || + 'Your RAFT data has been successfully imported and applied to the form.'} + + + + + ) + + default: + return null + } + } + + // Render export section + const renderExportContent = () => { + return ( + + + {t('export_raft_data') || 'Export Your RAFT Data'} + + + + {t('export_json_description') || + 'Download your current form data as a JSON file. You can use this file later to import and restore your form.'} + + + + {!raftData || Object.keys(raftData).length === 0 ? ( + <> + + + {t('no_data_to_export') || 'No form data available to export'} + + + {t('fill_form_first') || 'Please fill in some form data before exporting'} + + + ) : ( + <> + + + {t('click_to_download') || 'Click to download your form data as a JSON file'} + + + )} + + + ) + } + + return ( + <> + {/* Main button to open the dialog */} + + + {/* Main Dialog */} + + + + + + {t('json_import_export') || 'Import/Export RAFT Data'} + + + + + + + + + + {/* Only show tabs if we're not in the midst of an import flow or it's the first step */} + {(importStep === 0 || activeTab === 1) && ( + + + } + iconPosition="start" + label={t('import') || 'Import'} + id="import-tab" + aria-controls="import-panel" + onClick={() => { + if (importStep > 0) resetImport() + }} + /> + } + iconPosition="start" + label={t('export') || 'Export'} + id="export-tab" + aria-controls="export-panel" + /> + + + )} + + {/* Show stepper when in import flow and not on first step */} + {activeTab === 0 && importStep > 0 && ( + + + {importSteps.map((step, index) => ( + index}> + + {step.label} + + + ))} + + + )} + + + {activeTab === 0 && renderImportContent()} + {activeTab === 1 && renderExportContent()} + + + {/* Only show actions if not in import flow or at start */} + {(activeTab === 1 || (activeTab === 0 && importStep === 0)) && ( + <> + + + + + + )} + + + ) +} + +export default JsonImportComponent diff --git a/rafts/frontend/src/components/Form/FileUpload/TextFileUpload.tsx b/rafts/frontend/src/components/Form/FileUpload/TextFileUpload.tsx new file mode 100644 index 0000000..09b61c0 --- /dev/null +++ b/rafts/frontend/src/components/Form/FileUpload/TextFileUpload.tsx @@ -0,0 +1,521 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import React, { useState, useRef, ChangeEvent, useEffect } from 'react' +import { + Button, + Box, + Typography, + CircularProgress, + Alert, + IconButton, + LinearProgress, +} from '@mui/material' +import { FileText, X, Eye, EyeOff, Upload, Cloud } from 'lucide-react' +import TextPreview from './TextPreview' +import { useAttachmentUpload } from '@/hooks/useAttachmentUpload' +import { + FileReference, + AttachmentValue, + isFileReference, + isBase64DataUrl, + AttachmentConfig, +} from '@/types/attachments' + +interface TextFileUploadProps { + /** Callback when file is loaded - receives text content or FileReference */ + onFileLoaded: (data: string | FileReference) => void + /** Callback when file is cleared */ + onClear: () => void + /** Initial text value - can be text string or FileReference */ + initialText?: AttachmentValue + /** Accepted file types */ + accept?: string + /** Maximum file size in bytes */ + maxSize?: number + /** Label for the upload area */ + label?: string + /** Hint text shown in upload area */ + hint?: string + /** Whether to show text preview */ + showPreview?: boolean + /** DOI identifier - if provided, uploads to VOSpace instead of inline */ + doiIdentifier?: string + /** Custom filename for the uploaded file */ + customFilename?: string + /** Attachment configuration for validation */ + config?: AttachmentConfig +} + +/** + * Component for handling text file uploads in forms + * + * Supports two modes: + * 1. Legacy mode (no doiIdentifier): Stores text content inline + * 2. VOSpace mode (with doiIdentifier): Uploads to VOSpace and returns FileReference + */ +const TextFileUpload: React.FC = ({ + onFileLoaded, + onClear, + initialText = '', + accept = '.txt,text/plain', + maxSize = 5 * 1024 * 1024, // 5MB default + label = 'Upload Text File', + hint = 'Select or drag & drop a text file (.txt)', + showPreview = true, + doiIdentifier, + customFilename, + config, +}) => { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [fileName, setFileName] = useState('') + const [hasFile, setHasFile] = useState(false) + const [textPreview, setTextPreview] = useState('') + const [showTextPreview, setShowTextPreview] = useState(false) + const [currentFileRef, setCurrentFileRef] = useState(null) + + const fileInputRef = useRef(null) + + // Use attachment upload hook when doiIdentifier is provided + const { + isUploading, + progress, + error: uploadError, + uploadFile: uploadToVOSpace, + resolveAttachment, + canUpload, + deleteFile, + } = useAttachmentUpload({ + doiIdentifier, + config, + }) + + // Determine if we're in VOSpace upload mode + const useVOSpaceUpload = Boolean(doiIdentifier) && canUpload + + // Resolve initial text on mount or when it changes + useEffect(() => { + const resolveInitialText = async () => { + if (!initialText) { + setHasFile(false) + setTextPreview('') + setCurrentFileRef(null) + return + } + + // If it's a FileReference, store it and resolve to displayable content + if (isFileReference(initialText)) { + setCurrentFileRef(initialText) + setFileName(initialText.filename) + setHasFile(true) + const resolved = await resolveAttachment(initialText) + if (resolved) { + const previewText = resolved.length > 500 ? resolved.substring(0, 500) + '...' : resolved + setTextPreview(previewText) + setShowTextPreview(true) + } + } else if (typeof initialText === 'string' && !isBase64DataUrl(initialText)) { + // It's inline text content + setHasFile(true) + setCurrentFileRef(null) + const previewText = + initialText.length > 500 ? initialText.substring(0, 500) + '...' : initialText + setTextPreview(previewText) + setShowTextPreview(true) + } + } + + resolveInitialText() + }, [initialText, resolveAttachment]) + + const handleFileChange = (event: ChangeEvent) => { + const files = event.target.files + + if (files && files.length > 0) { + processFile(files[0]) + } + + // Reset the input value to allow selecting the same file again + if (event.target) { + event.target.value = '' + } + } + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault() + event.stopPropagation() + + if (event.dataTransfer.files && event.dataTransfer.files.length > 0) { + processFile(event.dataTransfer.files[0]) + } + } + + const processFile = async (file: File) => { + // Reset states + setError(null) + setFileName(file.name) + + // Check file type - be more permissive for text files + const acceptedTypes = accept.split(',').map((t) => t.trim()) + const isAccepted = acceptedTypes.some((type) => { + if (type.startsWith('.')) { + return file.name.toLowerCase().endsWith(type.toLowerCase()) + } + return file.type.includes(type) || file.type === '' + }) + + if (!isAccepted && file.type !== '') { + setError(`Please upload a text file (${accept})`) + return + } + + // Check file size + if (file.size > maxSize) { + setError(`File is too large. Maximum size is ${maxSize / 1024 / 1024}MB`) + return + } + + // If VOSpace upload is enabled, upload to VOSpace + if (useVOSpaceUpload) { + setIsLoading(true) + try { + // First read the file to get preview + const textContent = await readFileAsText(file) + if (showPreview) { + const previewText = + textContent.length > 500 ? textContent.substring(0, 500) + '...' : textContent + setTextPreview(previewText) + } + setShowTextPreview(true) + + // Then upload + const filename = customFilename || file.name + const result = await uploadToVOSpace(file, filename) + + if (result.success && result.fileReference) { + setHasFile(true) + setCurrentFileRef(result.fileReference) + onFileLoaded(result.fileReference) + } else { + setError(result.error || 'Upload failed') + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Upload failed') + } finally { + setIsLoading(false) + } + return + } + + // Legacy mode: store text content inline + setIsLoading(true) + try { + const textContent = await readFileAsText(file) + setHasFile(true) + setCurrentFileRef(null) + + // Store the first 500 characters for preview + if (showPreview) { + const previewText = + textContent.length > 500 ? textContent.substring(0, 500) + '...' : textContent + setTextPreview(previewText) + } + + // Pass the full text content to the parent + onFileLoaded(textContent) + setShowTextPreview(true) + } catch (err) { + console.error(err) + setError('Error processing text file') + } finally { + setIsLoading(false) + } + } + + // Helper to read file as text + const readFileAsText = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (e) => resolve(e.target?.result as string) + reader.onerror = () => reject(new Error('Error reading file')) + reader.readAsText(file) + }) + } + + const handleDragOver = (event: React.DragEvent) => { + event.preventDefault() + event.stopPropagation() + } + + const handleClick = () => { + if (!hasFile && fileInputRef.current) { + fileInputRef.current.click() + } + } + + const handleClear = async () => { + // If we have a FileReference and doiIdentifier, delete from VOSpace + if (currentFileRef && doiIdentifier) { + try { + await deleteFile(currentFileRef.filename) + } catch (err) { + console.error('[TextFileUpload] Failed to delete from VOSpace:', err) + // Continue with UI clear even if delete fails + } + } + + setError(null) + setFileName('') + setHasFile(false) + setTextPreview('') + setShowTextPreview(false) + setCurrentFileRef(null) + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + onClear() + } + + const togglePreview = () => { + setShowTextPreview(!showTextPreview) + } + + // Combine loading states + const showLoading = isLoading || isUploading + const displayError = error || uploadError + + return ( + + + + {label} + + {useVOSpaceUpload && ( + + + + )} + + + + + {!hasFile ? ( + + {!doiIdentifier ? ( + + + + Save the form first to enable file uploads + + + ) : showLoading ? ( + + + {isUploading && ( + + + + Uploading to cloud storage... + + + )} + + ) : ( + <> + {useVOSpaceUpload ? ( + + ) : ( + + )} + + {hint} + + + + )} + + ) : ( + + + + + {currentFileRef && ( + + + Stored + + )} + + + {fileName || 'Text file loaded'} + + + {showPreview && textPreview && ( + + {showTextPreview ? : } + + )} + + + + + + + {/* Using the extracted TextPreview component */} + {showPreview && textPreview && showTextPreview && } + + )} + + {displayError && ( + + {displayError} + + )} + + ) +} + +export default TextFileUpload diff --git a/rafts/frontend/src/components/Form/FileUpload/TextPreview.tsx b/rafts/frontend/src/components/Form/FileUpload/TextPreview.tsx new file mode 100644 index 0000000..f397806 --- /dev/null +++ b/rafts/frontend/src/components/Form/FileUpload/TextPreview.tsx @@ -0,0 +1,118 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// TextPreview.tsx - Independent component for previewing text content +import React from 'react' +import { Paper, Typography, useTheme } from '@mui/material' + +interface TextPreviewProps { + text: string + maxHeight?: string | number +} + +const TextPreview: React.FC = ({ text, maxHeight = '200px' }) => { + const theme = useTheme() + + if (!text) return null + + return ( + + + Preview: + + {/* Use pre tag to preserve whitespace and formatting */} +
    +        {text}
    +      
    +
    + ) +} + +export default TextPreview diff --git a/rafts/frontend/src/components/Form/FileUpload/aster_1.png b/rafts/frontend/src/components/Form/FileUpload/aster_1.png new file mode 100644 index 0000000..08d5560 Binary files /dev/null and b/rafts/frontend/src/components/Form/FileUpload/aster_1.png differ diff --git a/rafts/frontend/src/components/Form/FileUpload/mock.json b/rafts/frontend/src/components/Form/FileUpload/mock.json new file mode 100644 index 0000000..bf1c2d5 --- /dev/null +++ b/rafts/frontend/src/components/Form/FileUpload/mock.json @@ -0,0 +1,50 @@ +{ + "authorInfo": { + "title": "Discovery of Activity in 2022 RN3", + "correspondingAuthor": { + "firstName": "S. Serhii", + "lastName": "Zautkin", + "affiliation": "NRC", + "authorORCID": "7897-8978-9789-7897", + "email": "szautkin@gmail.com" + }, + "contributingAuthors": [] + }, + "observationInfo": { + "topic": "comet", + "objectName": "2023 RN3", + "abstract": "We report observations of 2023 RN3 in r' -, g' , and w' -band (3 x 245 s in each filter) obtained by the LCO 1m telescope at Teide Observatory in Tenerife, Canary Islands, Spain (observatory code Z31) that show that 2023 RN3 is clearly active. It has an elliptical coma centered on the nucleus with the major axis oriented at a PA of 155 degrees East of North,", + "figure": "", + "acknowledgements": "" + }, + "technical": { + "ephemeris": "", + "orbitalElements": "", + "mpcId": "2023 RN3", + "alertId": "", + "mjd": "", + "telescope": "", + "instrument": "" + }, + "measurementInfo": { + "photometry": { + "wavelength": "r’ , g’ , w’ (LCO 1m); g_p, r _p, i _p, z _ s (FTN)", + "brightness": "", + "errors": "" + }, + "spectroscopy": { + "wavelength": "", + "flux": "", + "errors": "" + }, + "astrometry": { + "position": "", + "timeObserved": "" + } + }, + "miscInfo": { + "misc": [] + }, + "status": "review_ready", + "id": "25.0053" +} diff --git a/rafts/frontend/src/components/Form/FileUpload/obs1.jpg b/rafts/frontend/src/components/Form/FileUpload/obs1.jpg new file mode 100644 index 0000000..7edc9c7 Binary files /dev/null and b/rafts/frontend/src/components/Form/FileUpload/obs1.jpg differ diff --git a/rafts/frontend/src/components/Form/FileUpload/utils.ts b/rafts/frontend/src/components/Form/FileUpload/utils.ts new file mode 100644 index 0000000..1fd2953 --- /dev/null +++ b/rafts/frontend/src/components/Form/FileUpload/utils.ts @@ -0,0 +1,97 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { TRaftContext } from '@/context/types' + +/** + * Convert a JSON object to a downloadable file + * @param data The JSON data to convert + * @param filename The name of the file to download + */ +export const downloadJsonAsFile = ( + data: TRaftContext, + filename: string = 'raft-data.json', +): void => { + try { + const jsonString = JSON.stringify(data, null, 2) + const blob = new Blob([jsonString], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + + const link = document.createElement('a') + link.href = url + link.download = filename + document.body.appendChild(link) + link.click() + + // Clean up + document.body.removeChild(link) + URL.revokeObjectURL(url) + } catch (error) { + console.error('Error downloading JSON file:', error) + throw new Error('Failed to download JSON file') + } +} diff --git a/rafts/frontend/src/components/Form/FormLayoutWithContext.tsx b/rafts/frontend/src/components/Form/FormLayoutWithContext.tsx new file mode 100644 index 0000000..2973b23 --- /dev/null +++ b/rafts/frontend/src/components/Form/FormLayoutWithContext.tsx @@ -0,0 +1,871 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { + useState, + useEffect, + useMemo, + useCallback, + lazy, + Suspense, + useRef, + useReducer, + memo, +} from 'react' +import FormNavigation from '@/components/Form/FormNavigation' +import ReviewForm from '@/components/Form/ReviewForm' +import { useRaftForm } from '@/context/RaftFormContext' +import { + PROP_AUTHOR_INFO, + PROP_OBSERVATION_INFO, + PROP_TECHNICAL_INFO, + PROP_MISC_INFO, + PROP_MISC, + FORM_SECTIONS, + PROP_STATUS, + OPTION_DRAFT, + OPTION_REVIEW, + PROP_TITLE, + PROP_GENERAL_INFO, + PROP_POST_OPT_OUT, +} from '@/shared/constants' +import { useTranslations } from 'next-intl' +import { Button, Alert, Snackbar, Grid } from '@mui/material' +import RaftBreadcrumbs from '@/components/RaftDetail/components/RaftBreadcrumbs' +import { useRouter } from '@/i18n/routing' +import { TAuthor, TMiscInfo, TObservation, TRaftSubmission, TTechInfo } from '@/shared/model' +import JsonImportComponent from '@/components/Form/FileUpload/JsonImportComponent' +import WarningDialog from '@/components/Layout/WarningDialog' +import { AttentionBanner } from '@/components/Layout/AttentionBanner' +import { FINAL_REVIEW_STEP, FORM_INFO, REVIEW_SECTION } from '@/components/Form/constants' +import { BACKEND_STATUS } from '@/shared/backendStatus' +import { Paper, Typography, Chip } from '@mui/material' +import { VALIDATION_SCHEMAS } from '@/context/constants' +import { InputField } from '@/components/Form/InputFormField' +import { FormSectionLoader } from '@/components/Form/common/FormSectionLoader' +import { generalSchema } from '@/shared/model' +import type { AuthorFormRef } from '@/components/Form/AuthorForm' +import type { AnnouncementFormRef } from '@/components/Form/AnnouncementForm' +import type { ObservationInfoFormRef } from '@/components/Form/ObservationInfoForm' +import type { MiscellaneousInfoFormRef } from '@/components/Form/MiscellaneousInfoForm' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' + +// Lazy load form sections for better performance +const AuthorForm = lazy(() => import('@/components/Form/AuthorForm')) +const AnnouncementForm = lazy(() => import('@/components/Form/AnnouncementForm')) +const ObservationInfoForm = lazy(() => import('@/components/Form/ObservationInfoForm')) +const MiscellaneousInfoForm = lazy(() => import('@/components/Form/MiscellaneousInfoForm')) + +const DIRTY_FORM = FORM_SECTIONS.reduce( + (cForm, section) => { + cForm[section] = false + return cForm + }, + {} as { [key: string]: boolean }, +) + +type AlertSeverity = 'success' | 'error' | 'warning' | 'info' + +interface AlertState { + open: boolean + message: string + severity: AlertSeverity +} + +type AlertAction = { type: 'show'; severity: AlertSeverity; message: string } | { type: 'close' } + +function alertReducer(state: AlertState, action: AlertAction): AlertState { + switch (action.type) { + case 'show': + return { open: true, severity: action.severity, message: action.message } + case 'close': + return { ...state, open: false } + } +} + +// Schema for title-only validation (used by TitleInput's RHF instance) +const titleSchema = z.object({ + [PROP_TITLE]: generalSchema.shape[PROP_TITLE], +}) + +// Isolated title input — uses RHF register() for uncontrolled input (zero re-renders during typing) +const TitleInput = memo(function TitleInput({ + savedTitle, + onBlur, + label, + errorText, + helperText, + required, +}: { + savedTitle: string + onBlur: (value: string) => void + label: string + errorText: string + helperText: string + required: boolean +}) { + const { + register, + formState: { errors }, + reset, + } = useForm({ + mode: 'onBlur', + resolver: zodResolver(titleSchema), + defaultValues: { [PROP_TITLE]: savedTitle }, + }) + + // Sync when saved data changes externally (load, import, reset) + useEffect(() => { + reset({ [PROP_TITLE]: savedTitle }) + }, [savedTitle, reset]) + + const { ref, ...titleField } = register(PROP_TITLE, { + onBlur: (e) => onBlur(e.target.value), + }) + + return ( + + ) +}) + +const FormLayoutWithContext = () => { + const [currentStep, setCurrentStep] = useState(0) + const [alert, dispatchAlert] = useReducer(alertReducer, { + open: false, + message: '', + severity: 'info' as AlertSeverity, + }) + const [formIsDirty, setFormIsDirty] = useState(DIRTY_FORM) + const [warningModal, setIsWarningModalOpen] = useState({ isOpen: false, nextStep: 0 }) + const [cancelWarningOpen, setCancelWarningOpen] = useState(false) + const [resetWarningOpen, setResetWarningOpen] = useState(false) + const [postOptOut, setPostOptOut] = useState(false) + const [isTitleValid, setIsTitleValid] = useState(false) + const [submittingAction, setSubmittingAction] = useState<'draft' | 'submit' | null>(null) + + // Refs to read current title/optOut in handlers without state deps + const titleValueRef = useRef('') + const postOptOutRef = useRef(postOptOut) + postOptOutRef.current = postOptOut + + // Refs for form sections to get current values before submit + const authorFormRef = useRef(null) + const announcementFormRef = useRef(null) + const observationFormRef = useRef(null) + const miscFormRef = useRef(null) + + const { + raftData, + isLoading, + updateRaftSection, + resetForm, + submitForm, + isSectionCompleted, + allSectionsCompleted, + validateSection, + validateAllSections, + doiIdentifier, + } = useRaftForm() + + const router = useRouter() + const t = useTranslations('submission_form') + + // Derive stable reference for narrowed effect deps + const generalInfo = raftData?.[PROP_GENERAL_INFO] + + // Initialize opt-out and title ref from raftData - only when generalInfo changes + // TitleInput handles its own value sync via savedTitle prop + useEffect(() => { + if (generalInfo) { + titleValueRef.current = generalInfo[PROP_TITLE] || '' + + const optOut = generalInfo[PROP_POST_OPT_OUT] || false + setPostOptOut(optOut) + + try { + generalSchema.shape[PROP_TITLE].parse(generalInfo[PROP_TITLE] || '') + setIsTitleValid(true) + } catch { + setIsTitleValid(false) + } + } + }, [generalInfo]) + + // Memoize form info messages to prevent recalculation + const formInfoMessages = useMemo( + () => FORM_INFO[FORM_SECTIONS[currentStep] ?? REVIEW_SECTION]?.messages.map((mKey) => t(mKey)), + [currentStep, t], + ) + + // Note: We don't force navigation to incomplete sections anymore + // Users can freely navigate between all sections regardless of completion status + + // Memoize completed steps array to prevent recreation + // Misc Info: only show green check if user actually added data + const hasMiscData = useMemo(() => { + const miscInfo = raftData?.[PROP_MISC_INFO] as TMiscInfo | undefined + const miscArray = miscInfo?.[PROP_MISC] + return Array.isArray(miscArray) && miscArray.some((item) => item.miscKey || item.miscValue) + }, [raftData]) + + const completedSteps = useMemo( + () => [ + isSectionCompleted(PROP_AUTHOR_INFO), + isSectionCompleted(PROP_OBSERVATION_INFO), + isSectionCompleted(PROP_TECHNICAL_INFO), + hasMiscData && isSectionCompleted(PROP_MISC_INFO), + false, // Review step + ], + [isSectionCompleted, hasMiscData], + ) + + const changeStep = useCallback((step: number) => { + // Allow free navigation to any step between 0 and FINAL_REVIEW_STEP + if (step >= 0 && step <= FINAL_REVIEW_STEP) { + setCurrentStep(step) + } + }, []) + + // Handle step changes + const handleStepChange = useCallback( + (step: number) => { + // Get the current section name + const currentSection = + currentStep < FORM_SECTIONS.length ? FORM_SECTIONS[currentStep] : 'review' + + // Check if the current section is dirty (has unsaved changes) + const currentSectionIsDirty = + currentSection === 'review' ? false : formIsDirty[currentSection] + + // Also check if general info (title) is dirty + const isGeneralInfoDirty = formIsDirty[PROP_GENERAL_INFO] + + // Only show warning if current section or general info has unsaved changes + if (currentSectionIsDirty || isGeneralInfoDirty) { + setIsWarningModalOpen({ isOpen: true, nextStep: step }) + return + } else { + changeStep(step) + } + }, + [formIsDirty, changeStep, currentStep], + ) + + // Called by TitleInput on blur — receives the current value + const handleTitleBlur = useCallback( + (value: string) => { + titleValueRef.current = value + + try { + generalSchema.shape[PROP_TITLE].parse(value) + setIsTitleValid(true) + } catch { + setIsTitleValid(false) + } + + if (value !== raftData?.[PROP_GENERAL_INFO]?.[PROP_TITLE]) { + updateRaftSection(PROP_GENERAL_INFO, { + [PROP_TITLE]: value, + [PROP_POST_OPT_OUT]: postOptOutRef.current, + [PROP_STATUS]: raftData?.[PROP_GENERAL_INFO]?.[PROP_STATUS] ?? OPTION_DRAFT, + }) + setFormIsDirty((prev) => ({ ...prev, [PROP_GENERAL_INFO]: false })) + } + }, + [raftData, updateRaftSection, setFormIsDirty], + ) + + // Handle opt-out checkbox change + const handleOptOutChange = useCallback( + (checked: boolean) => { + setPostOptOut(checked) + + updateRaftSection(PROP_GENERAL_INFO, { + [PROP_TITLE]: titleValueRef.current, + [PROP_POST_OPT_OUT]: checked, + [PROP_STATUS]: raftData?.[PROP_GENERAL_INFO]?.[PROP_STATUS] ?? OPTION_DRAFT, + }) + + setFormIsDirty((prev) => ({ ...prev, [PROP_GENERAL_INFO]: true })) + }, + [raftData, updateRaftSection, setFormIsDirty], + ) + + // Handle section save - updates form data in context, marks as clean, and validates + const handleSectionSave = useCallback( + (section: string) => { + setFormIsDirty((prev) => ({ ...prev, [section]: false })) + + // Validate the section on save (user clicked Save button) + if (section in VALIDATION_SCHEMAS) { + validateSection(section as keyof typeof VALIDATION_SCHEMAS) + } + }, + [validateSection], + ) + + // Memoized callbacks for form sections to prevent re-renders + const handleAuthorSubmit = useCallback( + (data: TAuthor) => { + updateRaftSection(PROP_AUTHOR_INFO, data) + handleSectionSave(PROP_AUTHOR_INFO) + }, + [updateRaftSection, handleSectionSave], + ) + + const handleObservationSubmit = useCallback( + (data: TObservation) => { + updateRaftSection(PROP_OBSERVATION_INFO, data) + handleSectionSave(PROP_OBSERVATION_INFO) + }, + [updateRaftSection, handleSectionSave], + ) + + const handleTechnicalSubmit = useCallback( + (data: TTechInfo) => { + updateRaftSection(PROP_TECHNICAL_INFO, data) + handleSectionSave(PROP_TECHNICAL_INFO) + }, + [updateRaftSection, handleSectionSave], + ) + + const handleMiscellaneousSubmit = useCallback( + (data: TMiscInfo) => { + updateRaftSection(PROP_MISC_INFO, data) + handleSectionSave(PROP_MISC_INFO) + }, + [updateRaftSection, handleSectionSave], + ) + + // Memoized dirty handlers + const handleAuthorDirty = useCallback( + (isDirty: boolean) => setFormIsDirty((f) => ({ ...f, [PROP_AUTHOR_INFO]: isDirty })), + [], + ) + + const handleObservationDirty = useCallback( + (isDirty: boolean) => setFormIsDirty((f) => ({ ...f, [PROP_OBSERVATION_INFO]: isDirty })), + [], + ) + + const handleTechnicalDirty = useCallback( + (isDirty: boolean) => setFormIsDirty((f) => ({ ...f, [PROP_TECHNICAL_INFO]: isDirty })), + [], + ) + + const handleMiscellaneousDirty = useCallback( + (isDirty: boolean) => setFormIsDirty((f) => ({ ...f, [PROP_MISC_INFO]: isDirty })), + [], + ) + + // Sync current form values to context before submit + // This ensures "Save as Draft" captures unsaved form data + // Returns the synced data directly to avoid async state race condition + // Always reads current values from the active form ref regardless of dirty state, + // since dirty tracking is unreliable for gating saves (some forms only report dirty=true, never false) + const syncCurrentFormToContext = useCallback(() => { + // Start with current raftData + let syncedData = { ...raftData } + + // Get current values from the active form section and sync to context + switch (currentStep) { + case 0: // Author form + if (authorFormRef.current) { + const values = authorFormRef.current.getCurrentValues() + syncedData = { ...syncedData, [PROP_AUTHOR_INFO]: values } + updateRaftSection(PROP_AUTHOR_INFO, values) + } + break + case 1: // Announcement/Observation form + if (announcementFormRef.current) { + const values = announcementFormRef.current.getCurrentValues() + syncedData = { ...syncedData, [PROP_OBSERVATION_INFO]: values } + updateRaftSection(PROP_OBSERVATION_INFO, values) + } + break + case 2: // Technical/Observation info form + if (observationFormRef.current) { + const values = observationFormRef.current.getCurrentValues() + syncedData = { ...syncedData, [PROP_TECHNICAL_INFO]: values } + updateRaftSection(PROP_TECHNICAL_INFO, values) + } + break + case 3: // Miscellaneous form + if (miscFormRef.current) { + const values = miscFormRef.current.getCurrentValues() + syncedData = { ...syncedData, [PROP_MISC_INFO]: values } + updateRaftSection(PROP_MISC_INFO, values) + } + break + } + + // Always sync title and opt-out values to capture any unsaved changes + syncedData = { + ...syncedData, + [PROP_GENERAL_INFO]: { + ...syncedData[PROP_GENERAL_INFO], + [PROP_TITLE]: titleValueRef.current, + [PROP_POST_OPT_OUT]: postOptOutRef.current, + [PROP_STATUS]: syncedData[PROP_GENERAL_INFO]?.[PROP_STATUS] ?? OPTION_DRAFT, + }, + } + + return syncedData + }, [currentStep, updateRaftSection, raftData]) + + // Handle form submission + const handleSubmit = useCallback( + async (isDraft = false) => { + try { + setSubmittingAction(isDraft ? 'draft' : 'submit') + + const isNewRaft = !raftData?.id + const syncedData = syncCurrentFormToContext() + + // For full submit (not draft), validate all sections first + if (!isDraft) { + const allValid = validateAllSections(syncedData) + if (!allValid) { + dispatchAlert({ + type: 'show', + severity: 'warning', + message: t('validation_incomplete'), + }) + setSubmittingAction(null) + return + } + } else { + // For draft, validate general info (title required) and current section + validateSection(PROP_GENERAL_INFO) + } + + const res = await submitForm(isDraft, syncedData) + + if (res.success) { + dispatchAlert({ type: 'show', severity: 'success', message: t('submission_success') }) + setFormIsDirty(DIRTY_FORM) + + if (isDraft && isNewRaft && res.data) { + const newId = typeof res.data === 'string' ? res.data.split('/').pop() : null + if (newId) { + router.replace(`/form/edit/${newId}`) + } + } + } else { + dispatchAlert({ + type: 'show', + severity: 'error', + message: `${t('submission_error')} [${res.message}]`, + }) + } + + if (!isDraft) { + setTimeout(() => { + router.push('/view/rafts') + }, 3000) + } + } catch { + dispatchAlert({ type: 'show', severity: 'error', message: t('submission_error') }) + } finally { + setSubmittingAction(null) + } + }, + [ + syncCurrentFormToContext, + submitForm, + t, + router, + raftData?.id, + validateAllSections, + validateSection, + ], + ) + + // Handle form reset - opens confirmation dialog + const handleReset = useCallback(() => { + setResetWarningOpen(true) + }, []) + + // Confirm reset action + const confirmReset = useCallback(() => { + setResetWarningOpen(false) + resetForm() + setCurrentStep(0) + setFormIsDirty(DIRTY_FORM) + titleValueRef.current = '' + setPostOptOut(false) + setIsTitleValid(false) + dispatchAlert({ type: 'show', severity: 'info', message: t('form_reset') }) + }, [resetForm, t]) + + // Check if any form section has unsaved changes + const hasUnsavedChanges = useMemo(() => { + return Object.values(formIsDirty).some((isDirty) => isDirty) + }, [formIsDirty]) + + // Handle cancel button click + const handleCancel = useCallback(() => { + if (hasUnsavedChanges) { + setCancelWarningOpen(true) + } else { + router.push('/view/rafts') + } + }, [hasUnsavedChanges, router]) + + // Confirm cancel and navigate away + const handleConfirmCancel = useCallback(() => { + setCancelWarningOpen(false) + router.push('/view/rafts') + }, [router]) + + // Determine breadcrumb title based on create vs edit mode + const breadcrumbTitle = useMemo(() => { + if (raftData?.id) { + const title = generalInfo?.[PROP_TITLE] + return title ? `${t('edit')}: ${title}` : t('edit_raft') + } + return t('create_new_raft') + }, [raftData?.id, generalInfo, t]) + + const handleSaveDraft = useCallback(() => handleSubmit(true), [handleSubmit]) + const handleSubmitFinal = useCallback(() => handleSubmit(), [handleSubmit]) + const handleAlertClose = useCallback(() => dispatchAlert({ type: 'close' }), []) + + // Memoize button visibility to avoid recalculating in JSX + const currentStatus = generalInfo?.[PROP_STATUS] + const showSaveAsDraft = useMemo( + () => + !currentStatus || + currentStatus === OPTION_DRAFT || + currentStatus === BACKEND_STATUS.IN_PROGRESS || + currentStatus === OPTION_REVIEW, + [currentStatus], + ) + const showSubmit = useMemo( + () => + !currentStatus || + currentStatus === OPTION_DRAFT || + currentStatus === BACKEND_STATUS.IN_PROGRESS, + [currentStatus], + ) + const draftButtonLabel = useMemo( + () => + submittingAction === 'draft' + ? t('saving') + : t(currentStatus === OPTION_REVIEW ? 'revert_to_draft' : 'save_as_draft'), + [submittingAction, currentStatus, t], + ) + + return ( +
    + {isLoading ? ( +
    +
    +
    + ) : ( + <> + +
    +

    {t('raft_form_title')}

    + +
    + + + } + /> + {formInfoMessages?.length ? ( +
    + +
    + ) : null} + + + {currentStep === 0 && ( + }> + + + )} + + {currentStep === 1 && ( + }> + + + )} + + {currentStep === 2 && ( + }> + + + )} + + {currentStep === 3 && ( + }> + + + )} + {currentStep === 4 && ( + + )} + + +
    + {!doiIdentifier && ( +

    {t('save_as_draft_helper')}

    + )} + {showSaveAsDraft && ( + + )} + {showSubmit && ( + + )} + + +
    +
    +
    + {/* Metadata info panel — only shown in edit mode */} + {raftData?.id && ( + + {raftData.createdBy && ( + + Created by: {raftData.createdBy} + + )} + {raftData.version && ( + + Version: v{raftData.version} + + )} + {raftData.generalInfo?.status && ( + + )} + + )} + + {/* Alert for notifications */} + + + {alert.message} + + + + )} + setIsWarningModalOpen({ isOpen: false, nextStep: currentStep })} + onOk={() => { + setIsWarningModalOpen({ isOpen: false, nextStep: warningModal.nextStep }) + // Only clear the dirty state of the current section when navigating away + const currentSection = + currentStep < FORM_SECTIONS.length ? FORM_SECTIONS[currentStep] : 'review' + setFormIsDirty((prev) => ({ + ...prev, + [currentSection]: false, + [PROP_GENERAL_INFO]: false, // Also clear general info (title) dirty state + })) + changeStep(warningModal.nextStep) + }} + options={{ + title: t('modal_changes_title'), + message: t('modal_changes_message'), + cancelCaption: t('modal_changes_cancel_caption'), + okCaption: t('modal_changes_ok_caption'), + }} + /> + {/* Cancel confirmation dialog */} + setCancelWarningOpen(false)} + onOk={handleConfirmCancel} + options={{ + title: t('modal_cancel_title'), + message: t('modal_cancel_message'), + cancelCaption: t('modal_cancel_stay'), + okCaption: t('modal_cancel_leave'), + }} + /> + {/* Reset confirmation dialog */} + setResetWarningOpen(false)} + onOk={confirmReset} + options={{ + title: t('modal_reset_title'), + message: t('confirm_reset'), + cancelCaption: t('cancel'), + okCaption: t('reset_form'), + }} + /> +
    + ) +} + +export default FormLayoutWithContext diff --git a/rafts/frontend/src/components/Form/FormNavigation.tsx b/rafts/frontend/src/components/Form/FormNavigation.tsx new file mode 100644 index 0000000..9e299d1 --- /dev/null +++ b/rafts/frontend/src/components/Form/FormNavigation.tsx @@ -0,0 +1,182 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' +import { ReactNode, FC } from 'react' +import { CheckCircle, Circle, HelpCircle } from 'lucide-react' +import { useTranslations } from 'next-intl' +import dynamic from 'next/dynamic' +import { Paper, IconButton, Tooltip } from '@mui/material' +import { useFormTutorial } from '@/hooks/useFormTutorial' + +const FormTutorial = dynamic(() => import('@/components/Tutorial/FormTutorial'), { ssr: false }) + +interface Step { + title: string +} + +interface FormNavigationProps { + currentStep: number // 0-based index + onStepChange: (step: number) => void // Callback function for step change + completedSteps?: boolean[] // Array indicating which steps are completed + title: ReactNode +} + +const FormNavigation: FC = ({ + currentStep, + onStepChange, + completedSteps = [], + title, +}) => { + const t = useTranslations('submission_form') + const { run, stepIndex, handleJoyrideCallback, startTutorial } = useFormTutorial() + + const steps: Step[] = [ + { title: t('author_info_step') }, + { title: t('announcement_step') }, + { title: t('observation_step') }, + { title: t('miscellaneous_step') }, + { title: t('review_step') }, + ] + + return ( + <> + + + {/* Tutorial Help Button */} +
    + + + + + +
    + + {/* Form Title */} +
    + {title} +
    + + {/* Navigation Steps */} +
    + {steps.map((step, index) => { + const isCompleted = completedSteps[index] === true + const isActive = index === currentStep + + return ( +
    onStepChange(index)} + > +
    + {isCompleted ? ( + + ) : ( + + )} + {index < steps.length - 1 && ( +
    +
    +
    + )} +
    + + {step.title} + +
    + ) + })} +
    + + + ) +} + +export default FormNavigation diff --git a/rafts/frontend/src/components/Form/InputFormField.tsx b/rafts/frontend/src/components/Form/InputFormField.tsx new file mode 100644 index 0000000..24db007 --- /dev/null +++ b/rafts/frontend/src/components/Form/InputFormField.tsx @@ -0,0 +1,103 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { memo } from 'react' +import { TextField, TextFieldProps } from '@mui/material' + +const InputFormField = memo(function InputFormField(props: TextFieldProps) { + return ( +
    + +
    + ) +}) + +export const InputField = memo(function InputField(props: TextFieldProps) { + return ( + + ) +}) + +export default InputFormField diff --git a/rafts/frontend/src/components/Form/MeasurementInfoForm.tsx b/rafts/frontend/src/components/Form/MeasurementInfoForm.tsx new file mode 100644 index 0000000..9774028 --- /dev/null +++ b/rafts/frontend/src/components/Form/MeasurementInfoForm.tsx @@ -0,0 +1,265 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { TMeasurementInfo, measurementInfoSchema } from '@/shared/model' +import { useEffect } from 'react' + +// Hooks +import { useTranslations } from 'next-intl' + +// Constants +import { + PROP_PHOTOMETRY, + PROP_SPECTROSCOPY, + PROP_ASTROMETRY, + PROP_WAVELENGTH, + PROP_BRIGHTNESS, + PROP_FLUX, + PROP_ERRORS, + PROP_POSITION, + PROP_TIME_OBSERVED, +} from '@/shared/constants' + +// Components +import InputFormField from '@/components/Form/InputFormField' +import Button from '@mui/material/Button' +import SaveIcon from '@mui/icons-material/Save' +import { Paper } from '@mui/material' + +const MeasurementInfoForm = ({ + onSubmitMeasurement, + initialData = null, + formIsDirty, +}: { + onSubmitMeasurement: (values: TMeasurementInfo) => void + formIsDirty: (value: boolean) => void + initialData?: TMeasurementInfo | null +}) => { + const { + register, + handleSubmit, + reset, + formState: { errors, isDirty }, + } = useForm({ + resolver: zodResolver(measurementInfoSchema), + defaultValues: initialData || { + [PROP_PHOTOMETRY]: { + [PROP_WAVELENGTH]: '', + [PROP_BRIGHTNESS]: '', + [PROP_ERRORS]: '', + }, + [PROP_SPECTROSCOPY]: { + [PROP_WAVELENGTH]: '', + [PROP_FLUX]: '', + [PROP_ERRORS]: '', + }, + [PROP_ASTROMETRY]: { + [PROP_POSITION]: '', + [PROP_TIME_OBSERVED]: '', + }, + }, + }) + + const t = useTranslations('submission_form') + + // Reset form with initialData when it changes + useEffect(() => { + if (initialData) { + reset(initialData) + } + }, [initialData, reset]) + + useEffect(() => { + formIsDirty(isDirty) + }, [isDirty, formIsDirty]) + + const onSubmit = (data: TMeasurementInfo) => { + onSubmitMeasurement(data) + } + + return ( + +
    +

    {t('measurement_info')}

    + + {/* Photometry Section */} +
    + {t('photometry')} + +
    + + + + + +
    +
    + + {/* Spectroscopy Section */} +
    + {t('spectroscopy')} + +
    + + + + + +
    +
    + + {/* Astrometry Section */} +
    + {t('astrometry')} + +
    + + + +
    +
    + + {/* Submit Button */} + +
    +
    + ) +} +export default MeasurementInfoForm diff --git a/rafts/frontend/src/components/Form/MiscellaneousInfoForm.tsx b/rafts/frontend/src/components/Form/MiscellaneousInfoForm.tsx new file mode 100644 index 0000000..bc721c8 --- /dev/null +++ b/rafts/frontend/src/components/Form/MiscellaneousInfoForm.tsx @@ -0,0 +1,359 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useForm, useFieldArray } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { TMiscInfo, miscInfoSchema } from '@/shared/model' +import { useEffect, useMemo, useImperativeHandle, forwardRef } from 'react' + +// Hooks +import { useTranslations } from 'next-intl' +import { useSectionTutorial } from '@/hooks/useSectionTutorial' + +// Constants +import { PROP_MISC, PROP_MISC_KEY, PROP_MISC_VALUE, PROP_MISC_TYPE } from '@/shared/constants' + +// Components +import InputFormField from '@/components/Form/InputFormField' +import Button from '@mui/material/Button' +import DeleteIcon from '@mui/icons-material/Delete' +import AddIcon from '@mui/icons-material/Add' +import SaveIcon from '@mui/icons-material/Save' +import { Paper, IconButton, Tooltip, Typography, ButtonGroup, Chip } from '@mui/material' +import { HelpCircle } from 'lucide-react' +import dynamic from 'next/dynamic' +import type { Step } from 'react-joyride' + +const SectionTutorial = dynamic(() => import('@/components/Tutorial/SectionTutorial'), { + ssr: false, +}) +import TextFileUpload from '@/components/Form/FileUpload/TextFileUpload' +import { FileReference, isFileReference, parseStoredAttachment } from '@/types/attachments' + +const EMPTY_TEXT_ITEM = { + [PROP_MISC_KEY]: '', + [PROP_MISC_VALUE]: '', + [PROP_MISC_TYPE]: 'text' as const, +} + +const EMPTY_FILE_ITEM = { + [PROP_MISC_KEY]: '', + [PROP_MISC_VALUE]: '', + [PROP_MISC_TYPE]: 'file' as const, +} + +export interface MiscellaneousInfoFormRef { + getCurrentValues: () => TMiscInfo +} + +const MiscellaneousInfoForm = forwardRef< + MiscellaneousInfoFormRef, + { + onSubmitMiscellaneous: (values: TMiscInfo) => void + formIsDirty: (value: boolean) => void + initialData?: TMiscInfo | null + doiIdentifier?: string | null + } +>(({ onSubmitMiscellaneous, initialData = null, formIsDirty, doiIdentifier }, ref) => { + const t = useTranslations('submission_form') + const tTutorial = useTranslations('tutorial') + + // Tutorial setup + const { run, stepIndex, handleJoyrideCallback, startTutorial } = useSectionTutorial({ + sectionName: 'misc', + autoStart: false, + }) + + // Tutorial steps + const tutorialSteps: Step[] = useMemo( + () => [ + { + target: '.misc-section-header', + content: tTutorial('misc_section_welcome'), + placement: 'bottom', + disableBeacon: true, + }, + { + target: '.key-value-section', + content: tTutorial('misc_key_value'), + placement: 'bottom', + }, + { + target: '.add-misc-button', + content: tTutorial('misc_additional_files'), + placement: 'top', + }, + { + target: '.save-misc-button', + content: tTutorial('misc_save'), + placement: 'top', + }, + ], + [tTutorial], + ) + + const { + register, + handleSubmit, + control, + reset, + getValues, + setValue, + watch, + formState: { errors, isDirty }, + } = useForm({ + resolver: zodResolver(miscInfoSchema), + mode: 'onBlur', + defaultValues: initialData || { + [PROP_MISC]: [], + }, + }) + + const { fields, append, remove } = useFieldArray({ + control, + name: PROP_MISC, + }) + + // Reset form with initialData when it changes + useEffect(() => { + if (initialData) { + reset(initialData) + } + }, [initialData, reset]) + + useEffect(() => { + formIsDirty(isDirty) + }, [isDirty, formIsDirty]) + + // Expose getCurrentValues via ref for parent to get form values before submit + useImperativeHandle(ref, () => ({ + getCurrentValues: () => getValues(), + })) + + const onSubmit = (data: TMiscInfo) => { + onSubmitMiscellaneous(data) + } + + // Count file-type entries + const watchedFields = watch(PROP_MISC) + const fileCount = useMemo( + () => watchedFields?.filter((f) => f?.[PROP_MISC_TYPE] === 'file').length ?? 0, + [watchedFields], + ) + + const serializeAttachmentValue = (data: string | FileReference): string => { + if (isFileReference(data)) { + return JSON.stringify(data) + } + return data + } + + return ( + <> + + + {/* Tutorial Help Button */} +
    + + + + + +
    + +
    +

    + {t('miscellaneous_info')} + {fileCount > 0 && ( + + )} +

    + +
    + {fields.map((field, index) => { + const entryType = watchedFields?.[index]?.[PROP_MISC_TYPE] || 'text' + const isFileType = entryType === 'file' + + return ( +
    +
    + + {isFileType ? t('misc_file_entry') : t('misc_text_entry')} + + +
    + {/* Hidden field for miscType */} + +
    + + + {isFileType ? ( + { + setValue( + `${PROP_MISC}.${index}.${PROP_MISC_VALUE}`, + serializeAttachmentValue(data), + ) + }} + onClear={() => { + setValue(`${PROP_MISC}.${index}.${PROP_MISC_VALUE}`, '') + }} + initialText={parseStoredAttachment( + watchedFields?.[index]?.[PROP_MISC_VALUE], + )} + doiIdentifier={doiIdentifier || undefined} + customFilename={`misc_${index + 1}`} + /> + ) : ( + + )} +
    +
    + ) + })} + + + + + +
    + + +
    +
    + + ) +}) + +MiscellaneousInfoForm.displayName = 'MiscellaneousInfoForm' + +export default MiscellaneousInfoForm diff --git a/rafts/frontend/src/components/Form/ObservationInfoForm.tsx b/rafts/frontend/src/components/Form/ObservationInfoForm.tsx new file mode 100644 index 0000000..ca1fc35 --- /dev/null +++ b/rafts/frontend/src/components/Form/ObservationInfoForm.tsx @@ -0,0 +1,459 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { TTechInfo, technicalInfoSchema } from '@/shared/model' +import { useEffect, useMemo, useImperativeHandle, forwardRef, useCallback } from 'react' + +// Hooks +import { useTranslations } from 'next-intl' +import { useSectionTutorial } from '@/hooks/useSectionTutorial' + +// Constants +import { + PROP_EPHEMERIS, + PROP_ORBITAL_ELEMENTS, + PROP_MPC_ID, + PROP_ALERT_ID, + PROP_MJD, + PROP_TELESCOPE, + PROP_PHOTOMETRY, + PROP_WAVELENGTH, + PROP_BRIGHTNESS, + PROP_ERRORS, + PROP_SPECTROSCOPY, + PROP_ASTROMETRY, +} from '@/shared/constants' + +// Components +import InputFormField from '@/components/Form/InputFormField' +import Button from '@mui/material/Button' +import SaveIcon from '@mui/icons-material/Save' +import { FormHelperText, Paper, IconButton, Tooltip } from '@mui/material' +import { HelpCircle } from 'lucide-react' +import dynamic from 'next/dynamic' +import TextFileUpload from '@/components/Form/FileUpload/TextFileUpload' +import ADESFileUpload from '@/components/Form/FileUpload/ADESFileUpload' +import type { Step } from 'react-joyride' + +const SectionTutorial = dynamic(() => import('@/components/Tutorial/SectionTutorial'), { + ssr: false, +}) +import { FileReference, isFileReference, parseStoredAttachment } from '@/types/attachments' + +export interface ObservationInfoFormRef { + getCurrentValues: () => TTechInfo +} + +const ObservationInfoForm = forwardRef< + ObservationInfoFormRef, + { + onSubmitTechnical: (values: TTechInfo) => void + formIsDirty: (value: boolean) => void + initialData?: TTechInfo | null + doiIdentifier?: string | null + } +>(({ onSubmitTechnical, initialData = null, formIsDirty, doiIdentifier }, ref) => { + const t = useTranslations('submission_form') + const tTutorial = useTranslations('tutorial') + + // Tutorial setup + const { run, stepIndex, handleJoyrideCallback, startTutorial } = useSectionTutorial({ + sectionName: 'observation', + autoStart: false, + }) + + // Tutorial steps + const tutorialSteps: Step[] = useMemo( + () => [ + { + target: '.observation-section-header', + content: tTutorial('observation_section_welcome'), + placement: 'bottom', + disableBeacon: true, + }, + { + target: '.coordinates-section', + content: tTutorial('observation_coordinates'), + placement: 'bottom', + }, + { + target: '.brightness-section', + content: tTutorial('observation_brightness'), + placement: 'bottom', + }, + { + target: '.telescope-field', + content: tTutorial('observation_telescope'), + placement: 'bottom', + }, + { + target: '.observation-files', + content: tTutorial('observation_files'), + placement: 'top', + }, + { + target: '.save-observation-button', + content: tTutorial('observation_save'), + placement: 'top', + }, + ], + [tTutorial], + ) + + const { + register, + handleSubmit, + reset, + setValue, + watch, + getValues, + formState: { errors, isDirty }, + } = useForm({ + resolver: zodResolver(technicalInfoSchema), + mode: 'onBlur', + defaultValues: initialData || { + [PROP_PHOTOMETRY]: { + [PROP_WAVELENGTH]: '', + [PROP_BRIGHTNESS]: '', + [PROP_ERRORS]: '', + }, + [PROP_EPHEMERIS]: '', + [PROP_ORBITAL_ELEMENTS]: '', + [PROP_MPC_ID]: '', + [PROP_ALERT_ID]: '', + [PROP_MJD]: '', + [PROP_TELESCOPE]: '', + [PROP_SPECTROSCOPY]: '', + [PROP_ASTROMETRY]: '', + }, + }) + + // Reset form with initialData when it changes + useEffect(() => { + if (initialData) { + reset(initialData) + } + }, [initialData, reset]) + + useEffect(() => { + formIsDirty(isDirty) + }, [isDirty, formIsDirty]) + + // Expose getCurrentValues via ref for parent to get form values before submit + useImperativeHandle(ref, () => ({ + getCurrentValues: () => getValues(), + })) + + const orbitalElementsValue = watch(PROP_ORBITAL_ELEMENTS) + const ephemerisValue = watch(PROP_EPHEMERIS) + const spectrumValue = watch(PROP_SPECTROSCOPY) + const astrometryValue = watch(PROP_ASTROMETRY) + const onSubmit = (data: TTechInfo) => { + onSubmitTechnical(data) + } + + // Helper to serialize attachment value for form storage + const serializeAttachmentValue = useCallback((data: string | FileReference): string => { + if (isFileReference(data)) { + return JSON.stringify(data) + } + return data + }, []) + + const handleSpectrumLoaded = useCallback( + (data: string | FileReference) => { + setValue(PROP_SPECTROSCOPY, serializeAttachmentValue(data)) + }, + [setValue, serializeAttachmentValue], + ) + + const handleSpectrumClear = useCallback(() => { + setValue(PROP_SPECTROSCOPY, undefined) + }, [setValue]) + + const handleOrbitalLoaded = useCallback( + (data: string | FileReference) => { + setValue(PROP_ORBITAL_ELEMENTS, serializeAttachmentValue(data)) + }, + [setValue, serializeAttachmentValue], + ) + + const handleOrbitalClear = useCallback(() => { + setValue(PROP_ORBITAL_ELEMENTS, undefined) + }, [setValue]) + + const handleEphemerisLoaded = useCallback( + (data: string | FileReference) => { + setValue(PROP_EPHEMERIS, serializeAttachmentValue(data)) + }, + [setValue, serializeAttachmentValue], + ) + + const handleEphemerisClear = useCallback(() => { + setValue(PROP_EPHEMERIS, undefined) + }, [setValue]) + + const handleADESFileLoaded = useCallback( + (data: string | FileReference) => { + setValue(PROP_ASTROMETRY, serializeAttachmentValue(data)) + }, + [setValue, serializeAttachmentValue], + ) + + const handleADESFileClear = useCallback(() => { + setValue(PROP_ASTROMETRY, undefined) + }, [setValue]) + + return ( + <> + + + {/* Tutorial Help Button */} +
    + + + + + +
    + +
    +

    + {t('technical_info')} +

    + + {/* Photometry Section */} +
    + {t('photometry')} + +
    + + + + + +
    +
    + {/* Identifiers Section */} +
    +

    {t('identifiers_helper')}

    + {/* MPC ID (Optional) */} + +
    + + {/* Alert ID (Optional) */} + + + {/* MJD (Optional) */} + + + {/* Telescope (Optional) */} + + {/* File Upload Section */} +

    {t('multiple_observations_helper')}

    +
    + {/* Ephemeris (Optional) */} + + + {errors[PROP_EPHEMERIS] ? t(errors[PROP_EPHEMERIS]?.message) : undefined} + + + {/* Orbital Elements (Optional) */} + + + {errors[PROP_ORBITAL_ELEMENTS] + ? t(errors[PROP_ORBITAL_ELEMENTS]?.message) + : undefined} + + {/*Spectrum File upload*/} + + + {/*Astrometry as ADES File upload*/} + +
    + + {/* Submit Button */} + + +
    + + ) +}) + +ObservationInfoForm.displayName = 'ObservationInfoForm' + +export default ObservationInfoForm diff --git a/rafts/frontend/src/components/Form/ReviewForm.tsx b/rafts/frontend/src/components/Form/ReviewForm.tsx new file mode 100644 index 0000000..81bed81 --- /dev/null +++ b/rafts/frontend/src/components/Form/ReviewForm.tsx @@ -0,0 +1,479 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { + PROP_AUTHOR_INFO, + PROP_OBSERVATION_INFO, + PROP_TECHNICAL_INFO, + PROP_MISC_INFO, + PROP_TITLE, + PROP_CORRESPONDING_AUTHOR, + PROP_CONTRIBUTING_AUTHORS, + PROP_COLLABORATIONS, + PROP_AUTHOR_FIRST_NAME, + PROP_AUTHOR_LAST_NAME, + PROP_AUTHOR_AFFILIATION, + PROP_AUTHOR_EMAIL, + PROP_TOPIC, + PROP_OBJECT_NAME, + PROP_ABSTRACT, + PROP_FIGURE, + PROP_ACKNOWLEDGEMENTS, + PROP_EPHEMERIS, + PROP_ORBITAL_ELEMENTS, + PROP_MPC_ID, + PROP_ALERT_ID, + PROP_MJD, + PROP_TELESCOPE, + PROP_SPECTROSCOPY, + PROP_ASTROMETRY, + PROP_MISC, + PROP_MISC_KEY, + PROP_MISC_VALUE, + PROP_AUTHOR_ORCID, + PROP_PREVIOUS_RAFTS, + PROP_PHOTOMETRY, + PROP_WAVELENGTH, + PROP_BRIGHTNESS, + PROP_ERRORS, + PROP_POST_OPT_OUT, +} from '@/shared/constants' +import { useTranslations } from 'next-intl' +import { + Paper, + Typography, + Divider, + Box, + Chip, + useTheme, + Checkbox, + FormControlLabel, +} from '@mui/material' +import { TPerson, TRaftSubmission } from '@/shared/model' +import React from 'react' +import AttachmentImage from '@/components/common/AttachmentImage' +import AttachmentText from '@/components/common/AttachmentText' + +interface ReviewFormProps { + raftData: TRaftSubmission + onOptOutChange?: (checked: boolean) => void + /** DOI identifier for resolving FileReference attachments */ + doiId?: string +} + +const ReviewForm = ({ raftData, onOptOutChange, doiId }: ReviewFormProps) => { + const t = useTranslations('submission_form') + const theme = useTheme() + + // Helper function to render author information + const renderAuthor = (author: TPerson, isCorresponding = false) => { + if (!author) return null + + return ( + + + {author[PROP_AUTHOR_FIRST_NAME]} {author[PROP_AUTHOR_LAST_NAME]} + + {isCorresponding && ( + + )} + + ORCID: {author[PROP_AUTHOR_ORCID]} + + + {author[PROP_AUTHOR_AFFILIATION]} + + + {author[PROP_AUTHOR_EMAIL]} + + + ) + } + + // Extract data sections + const authorInfo = raftData[PROP_AUTHOR_INFO] + const observationInfo = raftData[PROP_OBSERVATION_INFO] + const technicalInfo = raftData[PROP_TECHNICAL_INFO] + const miscInfo = raftData[PROP_MISC_INFO] + + // Helper function to render a text section if it exists + const renderSection = (title: string, content: string | undefined) => { + if (!content || content?.length === 0) return null + + return ( + + + {title} + + + {content} + + + ) + } + + return ( + + + {t('review_title')} + + + {/* RAFT Title */} + {raftData.generalInfo && ( + + {raftData.generalInfo[PROP_TITLE]} + + )} + + + + {/* Author Information */} + {authorInfo && ( + + + {t('author_info')} + + + {/* Corresponding Author */} + {renderAuthor(authorInfo[PROP_CORRESPONDING_AUTHOR], true)} + + {/* Contributing Authors */} + {authorInfo[PROP_CONTRIBUTING_AUTHORS] && + authorInfo[PROP_CONTRIBUTING_AUTHORS]?.length > 0 && ( + <> + + {t('con_authors')} + + {authorInfo[PROP_CONTRIBUTING_AUTHORS].map((author, index) => ( + + {renderAuthor(author)} + + ))} + + )} + + {/* Collaborations */} + {authorInfo[PROP_COLLABORATIONS] && authorInfo[PROP_COLLABORATIONS]?.length > 0 && ( + <> + + {t('collaborations')} + + {authorInfo[PROP_COLLABORATIONS].map((collab, index) => ( + + + {collab} + + + ))} + + )} + + )} + + + + {/* Observation Information */} + {observationInfo && ( + + + {t('observation_info')} + + + + + {t('topic')}:{' '} + + + + {(observationInfo[PROP_TOPIC] as string[]).map((value) => ( + + ))} + + + + + + {t('object_name')}: + {' '} + {observationInfo[PROP_OBJECT_NAME]} + + + + {renderSection(t('abstract'), observationInfo[PROP_ABSTRACT])} + {observationInfo[PROP_FIGURE] && ( + + + {t('figure')} + + + + )} + {renderSection(t('acknowledgements'), observationInfo[PROP_ACKNOWLEDGEMENTS])} + {renderSection(t('previouslyPublishedRafts'), observationInfo[PROP_PREVIOUS_RAFTS])} + + )} + + + + {/* Technical Information */} + {technicalInfo && ( + + + {t('technical_info')} + + + + {technicalInfo[PROP_PHOTOMETRY] && ( + + + {t('photometry')} + + + + {t('wavelength')}: + {' '} + {technicalInfo[PROP_PHOTOMETRY]?.[PROP_WAVELENGTH]} + + + + {t('brightness')}: + {' '} + {technicalInfo[PROP_PHOTOMETRY]?.[PROP_BRIGHTNESS]} + + + + {t('errors')}: + {' '} + {technicalInfo[PROP_PHOTOMETRY]?.[PROP_ERRORS]} + + + )} + {technicalInfo[PROP_EPHEMERIS] && ( + + + {t('ephemeris')}: + + + + )} + {technicalInfo[PROP_ORBITAL_ELEMENTS] && ( + + + {t('orbital_elements')}: + + + + )} + {technicalInfo[PROP_MPC_ID] && ( + + + {t('mpc_id')}: + {' '} + {technicalInfo[PROP_MPC_ID]} + + )} + + {technicalInfo[PROP_ALERT_ID] && ( + + + {t('alert_id')}: + {' '} + {technicalInfo[PROP_ALERT_ID]} + + )} + + {technicalInfo[PROP_MJD] && ( + + + {t('mjd')}: + {' '} + {technicalInfo[PROP_MJD]} + + )} + + {technicalInfo[PROP_TELESCOPE] && ( + + + {t('telescope')}: + {' '} + {technicalInfo[PROP_TELESCOPE]} + + )} + + {technicalInfo[PROP_SPECTROSCOPY] && ( + + + {t('spectroscopy')}: + + + + )} + {technicalInfo[PROP_ASTROMETRY] && ( + + + {t('astrometry')}: + + + + )} + + + )} + + + + {/* Miscellaneous Information */} + {miscInfo && miscInfo[PROP_MISC] && miscInfo[PROP_MISC]?.length > 0 && ( + + + {t('miscellaneous_info')} + + + + {miscInfo[PROP_MISC].map((item, index) => ( + + + {item[PROP_MISC_KEY]}: + {' '} + {item[PROP_MISC_VALUE]} + + ))} + + + )} + + + + {/* Post Opt Out Checkbox */} + + onOptOutChange?.(e.target.checked)} + color="primary" + disabled={!onOptOutChange} + /> + } + label={t('opt_out_community_post')} + sx={{ + '& .MuiFormControlLabel-label': { + color: theme.palette.text.primary, + }, + }} + /> + + + ) +} + +export default ReviewForm diff --git a/rafts/frontend/src/components/Form/common/FormSectionLoader.tsx b/rafts/frontend/src/components/Form/common/FormSectionLoader.tsx new file mode 100644 index 0000000..4921bb5 --- /dev/null +++ b/rafts/frontend/src/components/Form/common/FormSectionLoader.tsx @@ -0,0 +1,74 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Box, CircularProgress } from '@mui/material' + +export const FormSectionLoader = () => ( + + + +) diff --git a/rafts/frontend/src/components/Form/constants.ts b/rafts/frontend/src/components/Form/constants.ts new file mode 100644 index 0000000..6bee6b2 --- /dev/null +++ b/rafts/frontend/src/components/Form/constants.ts @@ -0,0 +1,117 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { + FORM_SECTIONS, + PROP_AUTHOR_AFFILIATION, + PROP_AUTHOR_EMAIL, + PROP_AUTHOR_FIRST_NAME, + PROP_AUTHOR_INFO, + PROP_AUTHOR_LAST_NAME, + PROP_AUTHOR_ORCID, + PROP_GENERAL_INFO, + PROP_MEASUREMENT_INFO, + PROP_MISC_INFO, + PROP_OBSERVATION_INFO, + PROP_TECHNICAL_INFO, +} from '@/shared/constants' + +export const EMPTY_AUTHOR = { + [PROP_AUTHOR_FIRST_NAME]: '', + [PROP_AUTHOR_LAST_NAME]: '', + [PROP_AUTHOR_ORCID]: '', + [PROP_AUTHOR_AFFILIATION]: '', + [PROP_AUTHOR_EMAIL]: '', +} + +export const FINAL_REVIEW_STEP = FORM_SECTIONS.length + +export const REVIEW_SECTION = 'review_section' + +export const FORM_INFO = { + [PROP_AUTHOR_INFO]: { + messages: ['author_form_message_one', 'author_form_message_two'], + }, + [PROP_OBSERVATION_INFO]: { + messages: [], + }, + [PROP_MISC_INFO]: { + messages: ['misc_form_message_one'], + }, + [PROP_TECHNICAL_INFO]: { + messages: ['announcement_form_message_one'], + }, + [PROP_MEASUREMENT_INFO]: { + messages: [], + }, + [PROP_GENERAL_INFO]: { + messages: [], + }, + [REVIEW_SECTION]: { + messages: ['review_form_message_one'], + }, +} as const diff --git a/rafts/frontend/src/components/HomeClient.tsx b/rafts/frontend/src/components/HomeClient.tsx new file mode 100644 index 0000000..efc4112 --- /dev/null +++ b/rafts/frontend/src/components/HomeClient.tsx @@ -0,0 +1,84 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useTranslations } from 'next-intl' +import { Link } from '@/i18n/routing' + +export default function HomeClient() { + const t = useTranslations('navigation') + + return ( +
    +

    {t('about')}

    + + FR + EN +
    + ) +} diff --git a/rafts/frontend/src/components/LandingPage/LandingChoice.tsx b/rafts/frontend/src/components/LandingPage/LandingChoice.tsx new file mode 100644 index 0000000..afc60c6 --- /dev/null +++ b/rafts/frontend/src/components/LandingPage/LandingChoice.tsx @@ -0,0 +1,282 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' +import { useRouter } from '@/i18n/routing' +import { useTranslations } from 'next-intl' +import { + Container, + Typography, + Paper, + Grid, + Box, + Card, + CardContent, + Divider, + useTheme, +} from '@mui/material' +import { + PostAdd as CreateIcon, + EditNote as ViewIcon, + ManageSearch as PublicViewIcon, + List as ListIcon, + Announcement as AnnouncementIcon, + Science as ScienceIcon, + Visibility as VisibilityIcon, +} from '@mui/icons-material' + +import solar from '@/assets/systeme-solaire-og.jpg' +import { useMemo } from 'react' +import { Session } from 'next-auth' + +const LandingChoice = ({ session }: { session: Session | null }) => { + const router = useRouter() + const userRole = session?.user?.role + const t = useTranslations('landing_page') + const theme = useTheme() + const features = [ + { + icon: , + title: t('rapidPublications'), + description: t('rapid_publication_desc'), + }, + { + icon: , + title: t('solar_system_science'), + description: t('solar_system_science_desc'), + }, + { + icon: , + title: t('community_access'), + description: t('community_access_desc'), + }, + ] + + const actionCards = useMemo( + () => + [ + { + title: t('create_raft'), + description: t('create_raft_desc'), + icon: , + color: theme.palette.primary.main, + path: '/form/create', + roles: ['contributor', 'reviewer', 'admin'], + }, + { + title: t('view_rafts'), + description: t('view_rafts_desc'), + icon: , + color: theme.palette.secondary.main, + path: '/view/rafts', + roles: ['contributor', 'reviewer'], + }, + { + title: t('browse_published'), + description: t('browse_published_desc'), + icon: ( + + ), + color: theme.palette.secondary.main, + path: '/public-view/rafts', + roles: [], + }, + { + title: t('review_rafts'), + description: t('review_rafts_desc'), + icon: , + color: theme.palette.secondary.main, + path: '/review/rafts', + roles: ['reviewer', 'admin'], + }, + ].filter((c) => { + return ( + (userRole && (c?.roles.includes(userRole) || c?.roles.length === 0)) || + (!userRole && c?.roles.length === 0) + ) + }), + [userRole, theme.palette.primary.main, theme.palette.secondary.main, t], + ) + return ( + + + + + + {t('hero_title')} + + + {t('hero_subtitle')} + + + {t('hero_description')} + + + + + + + + + + {features.map((feature, index) => ( + + + + {feature.icon} + + {feature.title} + + + {feature.description} + + + + + ))} + + + + + + {t('what_to_do')} + + + + {actionCards.map((card, index) => ( + + (card?.path ? router.push(card.path) : null)} + sx={{ + width: { xs: '100%', sm: 240 }, + maxWidth: 280, + height: 220, + transition: 'transform 0.2s, box-shadow 0.2s', + cursor: 'pointer', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + padding: 2, + '&:hover': { + transform: 'translateY(-8px)', + boxShadow: 6, + bgcolor: `${card?.color}10`, // 10% opacity of the card color + }, + }} + > + {card?.icon} + + {card?.title} + + + {card?.description} + + + + ))} + + + + + {t('published_info')} + + + + + + {t('footer_text')} + + + + ) +} + +export default LandingChoice diff --git a/rafts/frontend/src/components/LandingPage/Layout.tsx b/rafts/frontend/src/components/LandingPage/Layout.tsx new file mode 100644 index 0000000..71e91af --- /dev/null +++ b/rafts/frontend/src/components/LandingPage/Layout.tsx @@ -0,0 +1,80 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { ReactNode } from 'react' +const Layout = ({ children }: { children: ReactNode }) => { + return ( +
    +
    {children}
    +
    +
    Footer
    +
    +
    + ) +} + +export default Layout diff --git a/rafts/frontend/src/components/Layout/AppBar.tsx b/rafts/frontend/src/components/Layout/AppBar.tsx new file mode 100644 index 0000000..6dc075d --- /dev/null +++ b/rafts/frontend/src/components/Layout/AppBar.tsx @@ -0,0 +1,273 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' +import { MouseEvent } from 'react' +import { useTranslations } from 'next-intl' +import { + AppBar as MuiAppBar, + Toolbar, + IconButton, + Avatar, + Menu, + MenuItem, + Tooltip, + Box, + Divider, + Typography, + Button, + ListItemIcon, + ListItemText, +} from '@mui/material' +import { Home, ListAlt, RateReview } from '@mui/icons-material' +import { Login, Logout, Person } from '@mui/icons-material' +import { useState } from 'react' +import { useRouter, Link } from '@/i18n/routing' +import { Session } from 'next-auth' +import { signOut } from 'next-auth/react' + +import SolarSystem from '@/components/Layout/SolarLogo' +import ThemeToggle from '@/components/Layout/ThemeToggle' + +interface AppBarProps { + session: Session | null +} + +const AppBar = ({ session }: AppBarProps) => { + const t = useTranslations('app_bar') + const router = useRouter() + const [anchorEl, setAnchorEl] = useState(null) + const userRole = session?.user?.role + + const handleOpenMenu = (event: MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleCloseMenu = () => { + setAnchorEl(null) + } + + const onSignOut = async () => { + handleCloseMenu() + const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' + await signOut({ + redirect: true, + redirectTo: basePath ? `${basePath}/` : '/', + }) + } + + const handleSignIn = () => { + router.push('/login') + } + + const redirectProfile = () => { + handleCloseMenu() + router.push('/profile') + } + + const handleNavigation = (path: string) => { + handleCloseMenu() + router.push(path) + } + + const isReviewerOrAdmin = userRole === 'reviewer' || userRole === 'admin' + + return ( + + + {/* Left side - Logo and title */} + +
    + + + +
    + + + Research Announcements For The Solar System (RAFTs) + + +
    + + {/* Right side - Navigation + User menu */} + + {/* Desktop Navigation - visible on md and up */} + {session && ( + + + + {isReviewerOrAdmin && ( + + )} + + )} + + + + + + {session ? ( + <> + + + + {session.user?.name?.[0] || 'U'} + + + + + {/* Mobile Navigation - visible only on mobile */} + + handleNavigation('/')}> + + + + {t('nav_home')} + + handleNavigation('/view/rafts')}> + + + + {t('nav_rafts')} + + {isReviewerOrAdmin && ( + handleNavigation('/review/rafts')}> + + + + {t('nav_review')} + + )} + + + + + + + {t('profile')} + + + + + + {t('sign_out')} + + + + ) : ( + + + + + + + + )} + +
    +
    + ) +} + +export default AppBar diff --git a/rafts/frontend/src/components/Layout/AppLayout.tsx b/rafts/frontend/src/components/Layout/AppLayout.tsx new file mode 100644 index 0000000..bc6a59a --- /dev/null +++ b/rafts/frontend/src/components/Layout/AppLayout.tsx @@ -0,0 +1,92 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { ReactNode } from 'react' +import AppBar from '@/components/Layout/AppBar' +import { auth } from '@/auth/cadc-auth/credentials' +import { VersionInfo } from '@/components/VersionInfo' + +interface AppLayoutProps { + children: ReactNode +} + +const AppLayout = async ({ children }: AppLayoutProps) => { + const session = await auth() + + return ( +
    + +
    {children}
    +
    +
    Footer
    +
    + +
    + ) +} + +export default AppLayout diff --git a/rafts/frontend/src/components/Layout/AttentionBanner.tsx b/rafts/frontend/src/components/Layout/AttentionBanner.tsx new file mode 100644 index 0000000..7a51b48 --- /dev/null +++ b/rafts/frontend/src/components/Layout/AttentionBanner.tsx @@ -0,0 +1,121 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { Box, Typography, useTheme } from '@mui/material' + +interface AttentionBannerProps { + messages: string[] // now supports multiple paragraphs + onClose?: () => void + color?: 'info' | 'warning' | 'success' | 'error' +} + +export const AttentionBanner: React.FC = ({ messages, color = 'info' }) => { + const theme = useTheme() + + if (messages.length === 0) return null + + const backgroundMap = { + info: theme.palette.info.dark, + warning: theme.palette.warning.light, + success: theme.palette.success.light, + error: theme.palette.error.light, + } + + const textMap = { + info: theme.palette.info.contrastText, + warning: theme.palette.warning.contrastText, + success: theme.palette.success.contrastText, + error: theme.palette.error.contrastText, + } + + return ( + + {/* Messages */} + + {messages.map((msg, idx) => ( + + {msg} + + ))} + + + ) +} diff --git a/rafts/frontend/src/components/Layout/LanguageSelector.tsx b/rafts/frontend/src/components/Layout/LanguageSelector.tsx new file mode 100644 index 0000000..bd0d900 --- /dev/null +++ b/rafts/frontend/src/components/Layout/LanguageSelector.tsx @@ -0,0 +1,134 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { useLocale, useTranslations } from 'next-intl' +import { usePathname, useRouter } from '@/i18n/routing' +import { IconButton, Menu, MenuItem, Tooltip } from '@mui/material' +import { Language } from '@mui/icons-material' + +const LANGUAGES = { + en: 'English', + fr: 'Français', +} as const + +const LanguageSelector = () => { + const t = useTranslations('language_selector') + const locale = useLocale() + const router = useRouter() + const pathname = usePathname() + const [anchorEl, setAnchorEl] = useState(null) + + const handleOpenMenu = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleCloseMenu = () => { + setAnchorEl(null) + } + + const handleLanguageChange = (newLocale: string) => { + handleCloseMenu() + router.replace(pathname, { locale: newLocale }) + } + + return ( + <> + + + + + + + {Object.entries(LANGUAGES).map(([code, name]) => ( + handleLanguageChange(code)} + selected={locale === code} + > + {name} + + ))} + + + ) +} + +export default LanguageSelector diff --git a/rafts/frontend/src/components/Layout/LoginFormLayout.tsx b/rafts/frontend/src/components/Layout/LoginFormLayout.tsx new file mode 100644 index 0000000..8d4d92f --- /dev/null +++ b/rafts/frontend/src/components/Layout/LoginFormLayout.tsx @@ -0,0 +1,92 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { ReactNode } from 'react' +import { Card, CardContent, CardHeader, Typography } from '@mui/material' + +interface LoginFormLayoutProps { + children: ReactNode +} + +const LoginFormLayout = ({ children }: LoginFormLayoutProps) => { + return ( +
    + + + Research Announcements For The Solar System (RAFTs) + + } + /> + {children} + +
    + ) +} + +export default LoginFormLayout diff --git a/rafts/frontend/src/components/Layout/Logo.tsx b/rafts/frontend/src/components/Layout/Logo.tsx new file mode 100644 index 0000000..5dc9f57 --- /dev/null +++ b/rafts/frontend/src/components/Layout/Logo.tsx @@ -0,0 +1,90 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import React from 'react' + +const SolarSystemResearchLogo = () => { + return ( + + {/* Background */} + + + {/* Sun */} + + + {/* Planetary Orbits */} + + + + {/* Planets */} + + + + ) +} + +export default SolarSystemResearchLogo diff --git a/rafts/frontend/src/components/Layout/SolarLogo.tsx b/rafts/frontend/src/components/Layout/SolarLogo.tsx new file mode 100644 index 0000000..6177d2b --- /dev/null +++ b/rafts/frontend/src/components/Layout/SolarLogo.tsx @@ -0,0 +1,189 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import React from 'react' + +const SolarSystem = () => { + return ( + + {/* Outer orbit */} + + + {/* Inner orbit */} + + + {/* Sun in center */} + + + {/* Sun rays */} + + + + + + + + + + + {/* Earth with continents */} + + + {/* Venus/Mercury */} + + + ) +} + +export default SolarSystem diff --git a/rafts/frontend/src/components/Layout/ThemeToggle.tsx b/rafts/frontend/src/components/Layout/ThemeToggle.tsx new file mode 100644 index 0000000..12aa141 --- /dev/null +++ b/rafts/frontend/src/components/Layout/ThemeToggle.tsx @@ -0,0 +1,153 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useTheme } from 'next-themes' +import { ReactNode, useEffect, useState, MouseEvent } from 'react' +import { IconButton, Tooltip, Menu, MenuItem, ListItemIcon, ListItemText } from '@mui/material' +import { DarkMode, LightMode, SettingsBrightness, Check } from '@mui/icons-material' +import { useTranslations } from 'next-intl' + +type ThemeOption = 'light' | 'dark' | 'system' + +const ThemeToggle = () => { + const t = useTranslations('theme_toggle') + const { theme, setTheme, resolvedTheme } = useTheme() + const [mounted, setMounted] = useState(false) + const [anchorEl, setAnchorEl] = useState(null) + + useEffect(() => { + setMounted(true) + }, []) + + const handleOpenMenu = (event: MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleCloseMenu = () => { + setAnchorEl(null) + } + + const handleThemeChange = (newTheme: ThemeOption) => { + setTheme(newTheme) + handleCloseMenu() + } + + // Don't render anything until mounted to avoid hydration mismatch + if (!mounted) { + return ( + + + + ) + } + + const getCurrentIcon = () => { + if (theme === 'system') { + return + } + return resolvedTheme === 'dark' ? : + } + + const themeOptions: { value: ThemeOption; label: string; icon: ReactNode }[] = [ + { value: 'light', label: t('light'), icon: }, + { value: 'dark', label: t('dark'), icon: }, + { value: 'system', label: t('system'), icon: }, + ] + + return ( + <> + + + {getCurrentIcon()} + + + + {themeOptions.map((option) => ( + handleThemeChange(option.value)} + selected={theme === option.value} + > + {option.icon} + {option.label} + {theme === option.value && } + + ))} + + + ) +} + +export default ThemeToggle diff --git a/rafts/frontend/src/components/Layout/WarningDialog.tsx b/rafts/frontend/src/components/Layout/WarningDialog.tsx new file mode 100644 index 0000000..f9245f9 --- /dev/null +++ b/rafts/frontend/src/components/Layout/WarningDialog.tsx @@ -0,0 +1,119 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { FC } from 'react' +import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@mui/material' + +interface WarningDialogOptions { + title?: string + message?: string + cancelCaption?: string + okCaption?: string +} + +interface WarningDialogProps { + isOpen: boolean + onCancel: () => void + onOk: () => void + options?: WarningDialogOptions +} + +const WarningDialog: FC = ({ isOpen, onCancel, onOk, options = {} }) => { + const { + title = 'Are you sure?', + message = '', + cancelCaption = 'Cancel', + okCaption = 'OK', + } = options + + return ( + + {title && ( + + {title} + + )} + {message && ( + +

    {message}

    +
    + )} + + + + +
    + ) +} + +export default WarningDialog diff --git a/rafts/frontend/src/components/Layout/logo.svg b/rafts/frontend/src/components/Layout/logo.svg new file mode 100644 index 0000000..2ab4770 --- /dev/null +++ b/rafts/frontend/src/components/Layout/logo.svg @@ -0,0 +1,15 @@ + + {/* Background */} + + + {/* Sun */} + + + {/* Planetary Orbits */} + + + + {/* Planets */} + + + \ No newline at end of file diff --git a/rafts/frontend/src/components/Providers/AuthProvider.tsx b/rafts/frontend/src/components/Providers/AuthProvider.tsx new file mode 100644 index 0000000..5e73bad --- /dev/null +++ b/rafts/frontend/src/components/Providers/AuthProvider.tsx @@ -0,0 +1,90 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { SessionProvider } from 'next-auth/react' +import { ReactNode } from 'react' + +interface AuthProviderProps { + children: ReactNode +} + +export function AuthProvider({ children }: AuthProviderProps) { + const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' + + return ( + + {children} + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/PublishedRaftDetail.tsx b/rafts/frontend/src/components/RaftDetail/PublishedRaftDetail.tsx new file mode 100644 index 0000000..a12a66c --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/PublishedRaftDetail.tsx @@ -0,0 +1,197 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { RaftData } from '@/types/doi' +import { Container, Paper, Box } from '@mui/material' + +// Import modular components +import RaftBreadcrumbs from './components/RaftBreadcrumbs' +import RaftBackButton from './components/RaftBackButton' +import RaftHeader from './components/RaftHeader' +import RaftTabs from './components/RaftTabs' +import RelatedRafts from './components/RelatedRafts' +import DeleteConfirmationDialog from './components/DeleteConfirmationDialog' + +// Import tab content components +import OverviewTab from './tabs/OverviewTab' +import TechnicalInfoTab from './tabs/TechnicalInfoTab' +import MeasurementsTab from './tabs/MeasurementsTab' +import AdditionalInfoTab from './tabs/AdditionalInfoTab' + +interface RaftDetailProps { + raftData: RaftData +} + +export default function RaftDetail({ raftData }: RaftDetailProps) { + const router = useRouter() + const [tabValue, setTabValue] = useState(0) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + + // Check if user can edit/delete based on status + // Note: Backend uses 'in progress' for draft, frontend uses 'draft' + const currentStatus = raftData.generalInfo?.status?.toLowerCase() ?? '' + const isDraft = ['draft', 'in progress'].includes(currentStatus) + const isEditable = isDraft || ['rejected'].includes(currentStatus) + const isDeletable = isDraft + + // Handle tab changes + const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue) + } + + // Handle download + const handleDownload = () => { + // Implement download logic here + } + + // Handle share + const handleShare = () => { + const url = window.location.href + navigator.clipboard.writeText(url) + // You would normally show a toast notification here + } + + // Handle edit + const handleEdit = () => { + router.push(`/form/edit/${raftData._id}`) + } + + // Handle delete action + const handleDelete = () => { + setDeleteDialogOpen(true) + } + + // Confirm delete action + const confirmDelete = async () => { + // Implement deletion logic with server action + setDeleteDialogOpen(false) + // After successful deletion, redirect to the list page + router.push('/view/rafts') + } + + return ( + + {/* Breadcrumbs navigation */} + + + {/* Back button */} + router.back()} /> + + {/* Main content */} + + {/* Header section with title, metadata, and action buttons */} + + + {/* Tabs navigation */} + + + {/* Tab content panels */} + + {tabValue === 0 && ( + + )} + + {tabValue === 1 && ( + + )} + + {tabValue === 2 && } + + {tabValue === 3 && } + + + + {/* Related RAFTs section - if any */} + {raftData.relatedRafts && raftData.relatedRafts.length > 0 && ( + + )} + + {/* Delete confirmation dialog */} + setDeleteDialogOpen(false)} + onConfirm={confirmDelete} + /> + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/RaftDetail.tsx b/rafts/frontend/src/components/RaftDetail/RaftDetail.tsx new file mode 100644 index 0000000..8f8f404 --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/RaftDetail.tsx @@ -0,0 +1,402 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React, { useState } from 'react' +import { useRouter } from 'next/navigation' +import dynamic from 'next/dynamic' +import { RaftData } from '@/types/doi' +import { Container, Paper, Box, Snackbar, Alert } from '@mui/material' + +// Import modular components +import RaftBreadcrumbs from './components/RaftBreadcrumbs' +import RaftBackButton from './components/RaftBackButton' +import RaftHeader from './components/RaftHeader' +import RaftTabs from './components/RaftTabs' +import RelatedRafts from './components/RelatedRafts' + +// Lazy-load components that are conditionally rendered +const DeleteConfirmationDialog = dynamic(() => import('./components/DeleteConfirmationDialog')) + +// Tab content: only one is visible at a time +import OverviewTab from './tabs/OverviewTab' +const TechnicalInfoTab = dynamic(() => import('./tabs/TechnicalInfoTab')) +const AdditionalInfoTab = dynamic(() => import('./tabs/AdditionalInfoTab')) + +// Import server actions +import { submitForReview, revertToDraft } from '@/actions/submitForReview' +import { updateDOIStatus } from '@/actions/updateDOIStatus' +import { deleteRaft } from '@/actions/deleteRaft' +import { BACKEND_STATUS } from '@/shared/backendStatus' + +interface RaftDetailProps { + raftData: Partial +} + +export default function RaftDetail({ raftData }: RaftDetailProps) { + const router = useRouter() + const [tabValue, setTabValue] = useState(0) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [activeAction, setActiveAction] = useState<'submitting' | 'reverting' | 'deleting' | null>( + null, + ) + const [snackbar, setSnackbar] = useState<{ + open: boolean + message: string + severity: 'success' | 'error' + }>({ open: false, message: '', severity: 'success' }) + + if (!raftData) { + return null + } + + // Check if user can edit/delete based on status + const currentStatus = raftData?.generalInfo?.status?.toLowerCase() ?? '' + const isDraft = currentStatus === BACKEND_STATUS.IN_PROGRESS || currentStatus === 'draft' + const isRejected = currentStatus === BACKEND_STATUS.REJECTED + const isEditable = isDraft || isRejected + const isReviewReady = currentStatus === BACKEND_STATUS.REVIEW_READY + const isDeletable = isDraft + const canSubmitForReview = isDraft + const canRevertToDraft = isReviewReady + + // Handle tab changes + const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue) + } + + // Handle download + const handleDownload = () => { + // Implement download logic here + } + + // Handle share + const handleShare = () => { + const url = window.location.href + navigator.clipboard.writeText(url) + // You would normally show a toast notification here + } + + // Handle edit + const handleEdit = async () => { + // If the RAFT is rejected, change status to "in progress" (draft) before editing + if (isRejected && raftData.id) { + try { + const result = await updateDOIStatus(raftData.id, BACKEND_STATUS.IN_PROGRESS, { + dataDirectory: raftData.dataDirectory, + previousStatus: raftData.generalInfo?.status, + }) + if (!result.success) { + console.error('[RaftDetail] Failed to update status:', result.message) + setSnackbar({ + open: true, + message: result.message || 'Failed to revert to draft', + severity: 'error', + }) + return + } + setSnackbar({ + open: true, + message: 'RAFT status changed to Draft.', + severity: 'success', + }) + } catch (error) { + console.error('[RaftDetail] Error updating status:', error) + setSnackbar({ + open: true, + message: 'An error occurred while updating status', + severity: 'error', + }) + return + } + } + + router.push(`/form/edit/${raftData.id}`) + } + + // Handle delete action + const handleDelete = () => { + setDeleteDialogOpen(true) + } + + // Confirm delete action + const confirmDelete = async () => { + if (!raftData.id) { + setSnackbar({ + open: true, + message: 'No RAFT ID available', + severity: 'error', + }) + setDeleteDialogOpen(false) + return + } + + setActiveAction('deleting') + try { + const result = await deleteRaft(raftData.id) + if (result.success) { + setSnackbar({ + open: true, + message: 'RAFT deleted successfully', + severity: 'success', + }) + setDeleteDialogOpen(false) + setTimeout(() => { + router.push('/view/rafts') + }, 1500) + } else { + setSnackbar({ + open: true, + message: result.message || 'Failed to delete RAFT', + severity: 'error', + }) + setDeleteDialogOpen(false) + } + } catch (error) { + console.error('[RaftDetail] Error deleting RAFT:', error) + setSnackbar({ + open: true, + message: 'An error occurred while deleting RAFT', + severity: 'error', + }) + setDeleteDialogOpen(false) + } finally { + setActiveAction(null) + } + } + + // Handle submit for review + const handleSubmitForReview = async () => { + if (!raftData.id) { + setSnackbar({ + open: true, + message: 'No DOI ID available', + severity: 'error', + }) + return + } + + setActiveAction('submitting') + try { + const result = await submitForReview(raftData.id, raftData.dataDirectory) + if (result.success) { + setSnackbar({ + open: true, + message: 'RAFT status changed to Review Ready.', + severity: 'success', + }) + // Refresh the page to reflect the new status + router.refresh() + } else { + setSnackbar({ + open: true, + message: result.message || 'Failed to submit for review', + severity: 'error', + }) + } + } catch (error) { + console.error('[RaftDetail] Error submitting for review:', error) + setSnackbar({ + open: true, + message: 'An error occurred while submitting for review', + severity: 'error', + }) + } finally { + setActiveAction(null) + } + } + + // Handle revert to draft + const handleRevertToDraft = async () => { + if (!raftData.id) { + setSnackbar({ + open: true, + message: 'No DOI ID available', + severity: 'error', + }) + return + } + + setActiveAction('reverting') + try { + const result = await revertToDraft( + raftData.id, + raftData.dataDirectory, + raftData.generalInfo?.status, + ) + if (result.success) { + setSnackbar({ + open: true, + message: 'RAFT status changed to Draft.', + severity: 'success', + }) + router.refresh() + } else { + setSnackbar({ + open: true, + message: result.message || 'Failed to revert to draft', + severity: 'error', + }) + } + } catch (error) { + console.error('[RaftDetail] Error reverting to draft:', error) + setSnackbar({ + open: true, + message: 'An error occurred while reverting to draft', + severity: 'error', + }) + } finally { + setActiveAction(null) + } + } + + // Handle snackbar close + const handleSnackbarClose = () => { + setSnackbar((prev) => ({ ...prev, open: false })) + } + + return ( + + {/* Breadcrumbs navigation */} + + + {/* Back button */} + router.back()} /> + + {/* Main content */} + + {/* Header section with title, metadata, and action buttons */} + + + {/* Tabs navigation */} + + + {/* Tab content panels */} + + {tabValue === 0 && ( + + )} + + {tabValue === 1 && ( + + )} + + {tabValue === 2 && } + + + + {/* Related RAFTs section - if any */} + {raftData.relatedRafts && raftData.relatedRafts.length > 0 && ( + + )} + + {/* Delete confirmation dialog */} + setDeleteDialogOpen(false)} + onConfirm={confirmDelete} + isDeleting={activeAction === 'deleting'} + /> + + {/* Snackbar for feedback */} + + + {snackbar.message} + + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/ReviewRaftDetail.tsx b/rafts/frontend/src/components/RaftDetail/ReviewRaftDetail.tsx new file mode 100644 index 0000000..95898c6 --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/ReviewRaftDetail.tsx @@ -0,0 +1,192 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import dynamic from 'next/dynamic' +import { RaftData } from '@/types/doi' +import { RaftReview } from '@/types/reviews' + +import { Container, Grid, Paper, Box, Alert } from '@mui/material' + +// Import modular components +import RaftBreadcrumbs from './components/RaftBreadcrumbs' +import RaftBackButton from './components/RaftBackButton' +import RaftHeader from './components/RaftHeader' +import RaftTabs from './components/RaftTabs' + +// Lazy-load reviewer panel (only for reviewers) +const ReviewerSidePanel = dynamic(() => import('./components/ReviewerSidePanel')) + +// Tab content: only one is visible at a time +import OverviewTab from './tabs/OverviewTab' +const TechnicalInfoTab = dynamic(() => import('./tabs/TechnicalInfoTab')) +const AdditionalInfoTab = dynamic(() => import('./tabs/AdditionalInfoTab')) + +interface ReviewRaftDetailProps { + raftData: Partial | undefined + review?: RaftReview | undefined + hasReview?: boolean +} + +export default function ReviewRaftDetail({ + raftData, + review, + hasReview = false, +}: ReviewRaftDetailProps) { + const router = useRouter() + const [tabValue, setTabValue] = useState(0) + const [actionMessage, setActionMessage] = useState<{ + type: 'success' | 'error' + text: string + } | null>(null) + + // Handle tab changes + const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue) + } + + // Handle success/error notifications + const handleNotification = (type: 'success' | 'error', text: string) => { + setActionMessage({ type, text }) + } + if (!raftData) { + return null + } + return ( + + {/* Breadcrumbs navigation */} + + + {/* Back button */} + router.push('/review/rafts')} /> + + {/* Action result message */} + {actionMessage && ( + setActionMessage(null)} sx={{ mb: 2 }}> + {actionMessage.text} + + )} + + {/* Main content with reviewer sidebar */} + + {/* Main RAFT content */} + + {/* Review section */} + {/* + {hasReview && } +*/} + + {/* RAFT content */} + + {/* Header section with title and metadata */} + + + {/* Tabs navigation */} + + + {/* Tab content panels */} + + {tabValue === 0 && ( + + )} + + {tabValue === 1 && ( + + )} + + {tabValue === 2 && ( + + )} + + + + + {/* Reviewer sidebar */} + + + + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/components/CommentSection.tsx b/rafts/frontend/src/components/RaftDetail/components/CommentSection.tsx new file mode 100644 index 0000000..cd014e7 --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/CommentSection.tsx @@ -0,0 +1,212 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { RaftReview } from '@/types/reviews' +import { submitReviewComment } from '@/actions/submitReviewComment' +import { + Paper, + Box, + Typography, + TextField, + Divider, + Avatar, + List, + ListItem, + ListItemText, + ListItemAvatar, + IconButton, + Alert, + CircularProgress, + Chip, +} from '@mui/material' +import { Send } from 'lucide-react' +import { formatDate, formatUserName, getUserInitials } from '@/utilities/formatter' + +interface CommentSectionProps { + review?: RaftReview + onNotify: (type: 'success' | 'error', text: string) => void +} + +export default function CommentSection({ review, onNotify }: CommentSectionProps) { + const router = useRouter() + const [newComment, setNewComment] = useState('') + const [actionLoading, setActionLoading] = useState(false) + if (!review) { + return null + } + // Handle comment submission + const handleSubmitComment = async () => { + if (!newComment.trim()) return + + setActionLoading(true) + try { + const result = await submitReviewComment(review._id, { + content: newComment, + }) + + if (result.success) { + setNewComment('') + // Show success message + onNotify('success', 'Comment added successfully') + // Refresh the page to get updated review data + router.refresh() + } else { + onNotify('error', result.error || 'Failed to add comment') + } + } catch (error) { + console.error('Error adding comment:', error) + onNotify('error', 'An unexpected error occurred while adding your comment') + } finally { + setActionLoading(false) + } + } + + return ( + + + Review Comments + + + {review.comments.length === 0 ? ( + + No comments have been added yet. + + ) : ( + + {review.comments.map((comment) => ( + + + {getUserInitials(comment.createdBy)} + + + {formatUserName(comment.createdBy)} - {formatDate(comment.createdAt)} + + } + secondary={ + + + {comment.content} + + {comment.isResolved && ( + + )} + {comment.location && ( + + )} + + } + /> + + ))} + + )} + + + + {/* New comment form */} + + + {/* Use the current user's initials - can be customized based on your auth */}U + + setNewComment(e.target.value)} + variant="outlined" + /> + + {actionLoading ? : } + + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/components/DOILinks.tsx b/rafts/frontend/src/components/RaftDetail/components/DOILinks.tsx new file mode 100644 index 0000000..f3921df --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/DOILinks.tsx @@ -0,0 +1,114 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Button, Box, Tooltip } from '@mui/material' +import { ExternalLink, Database } from 'lucide-react' +import { CITATION_PARTIAL_URL, STORAGE_PARTIAL_URL } from '@/utilities/constants' +import { CITE_ULR } from '@/services/constants' + +interface DOILinksProps { + doi: string +} + +const DOILinks = ({ doi }: DOILinksProps) => { + // Extract the DOI identifier part (e.g., "25.0042" from "10.11570/25.0042") + const doiIdentifier = doi.split('/').pop() || doi + + // Construct the URLs + const landingPageUrl = `${CITATION_PARTIAL_URL}${doiIdentifier}` + const storageUrl = `${STORAGE_PARTIAL_URL}/${CITE_ULR}/${doiIdentifier}/data` + + return ( + + + + + + + + + + ) +} + +export default DOILinks diff --git a/rafts/frontend/src/components/RaftDetail/components/DeleteConfirmationDialog.tsx b/rafts/frontend/src/components/RaftDetail/components/DeleteConfirmationDialog.tsx new file mode 100644 index 0000000..9dc6f4e --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/DeleteConfirmationDialog.tsx @@ -0,0 +1,122 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, + Button, + CircularProgress, +} from '@mui/material' + +interface DeleteConfirmationDialogProps { + open: boolean + onClose: () => void + onConfirm: () => void + isDeleting?: boolean +} + +export default function DeleteConfirmationDialog({ + open, + onClose, + onConfirm, + isDeleting = false, +}: DeleteConfirmationDialogProps) { + return ( + + Delete RAFT + + + Are you sure you want to delete this RAFT? This action cannot be undone. + + + + + + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/components/NoDataMessage.tsx b/rafts/frontend/src/components/RaftDetail/components/NoDataMessage.tsx new file mode 100644 index 0000000..ab21757 --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/NoDataMessage.tsx @@ -0,0 +1,100 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Box, Typography } from '@mui/material' +import { ReactNode } from 'react' + +interface NoDataMessageProps { + icon: ReactNode + title: string + message: string +} + +export default function NoDataMessage({ icon, title, message }: NoDataMessageProps) { + return ( + + {icon} + + {title} + + + {message} + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/components/RaftBackButton.tsx b/rafts/frontend/src/components/RaftDetail/components/RaftBackButton.tsx new file mode 100644 index 0000000..c55d260 --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/RaftBackButton.tsx @@ -0,0 +1,83 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Button } from '@mui/material' +import { ArrowLeft } from 'lucide-react' + +interface RaftBackButtonProps { + onBack: () => void +} + +export default function RaftBackButton({ onBack }: RaftBackButtonProps) { + return ( + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/components/RaftBreadcrumbs.tsx b/rafts/frontend/src/components/RaftDetail/components/RaftBreadcrumbs.tsx new file mode 100644 index 0000000..6442513 --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/RaftBreadcrumbs.tsx @@ -0,0 +1,105 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import Link from 'next/link' +import { Box, Typography, Breadcrumbs } from '@mui/material' +import { Home, FileText } from 'lucide-react' + +interface RaftBreadcrumbsProps { + title?: string + basePath?: string +} + +export default function RaftBreadcrumbs({ title, basePath }: RaftBreadcrumbsProps) { + return ( + + + + + + Home + + + + + + + + RAFTs + + + + + + {title || 'RAFT Details'} + + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/components/RaftHeader.tsx b/rafts/frontend/src/components/RaftDetail/components/RaftHeader.tsx new file mode 100644 index 0000000..3821efc --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/RaftHeader.tsx @@ -0,0 +1,364 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Box, Typography, Chip, Button, Tooltip, CircularProgress } from '@mui/material' +import { + Calendar, + User, + Clock, + Tag, + Download, + Share2, + Pencil, + Trash2, + SendHorizontal, + UserCheck, + Undo2, +} from 'lucide-react' +import dayjs from 'dayjs' +import relativeTime from 'dayjs/plugin/relativeTime' + +// Extend dayjs with the relativeTime plugin +dayjs.extend(relativeTime) +import StatusBadge from '@/components/common/StatusBadge' +import { RaftData } from '@/types/doi' +import DOILinks from '@/components/RaftDetail/components/DOILinks' + +interface RaftHeaderProps { + raftData: Partial + isEditable: boolean + isDeletable: boolean + canSubmitForReview?: boolean + isSubmittingForReview?: boolean + canRevertToDraft?: boolean + isRevertingToDraft?: boolean + onDownload?: () => void + onShare?: () => void + onEdit?: () => void + onDelete?: () => void + onSubmitForReview?: () => void + onRevertToDraft?: () => void +} + +export default function RaftHeader({ + raftData, + isEditable, + isDeletable, + canSubmitForReview = false, + isSubmittingForReview = false, + canRevertToDraft = false, + isRevertingToDraft = false, + onDownload, + onShare, + onEdit, + onDelete, + onSubmitForReview, + onRevertToDraft, +}: RaftHeaderProps) { + const { + authorInfo, + observationInfo, + createdAt, + updatedAt, + createdBy, + doi, + generalInfo, + reviewer, + version, + } = raftData + const status = generalInfo?.status + const title = generalInfo?.title + + return ( + + {/* Desktop: side-by-side layout, Mobile: stacked */} + + {/* Main content section */} + + {observationInfo?.topic && observationInfo.topic.length > 0 && ( + + {observationInfo.topic.map((top) => ( + } + label={top.replace(/_/g, ' ')} + size="small" + color="secondary" + variant="outlined" + sx={{ textTransform: 'capitalize' }} + /> + ))} + + )} + + + {title || 'Untitled RAFT'} + + + + + + {version && version > 0 && ( + + )} + + {reviewer && ( + } + label={`Reviewer: ${reviewer}`} + size="small" + color="primary" + variant="outlined" + /> + )} + + + + + + {dayjs(createdAt).format('MMM D, YYYY')} + + + + + + {authorInfo?.correspondingAuthor + ? `${authorInfo.correspondingAuthor.firstName} ${authorInfo.correspondingAuthor.lastName}` + : createdBy || 'Unknown'} + + + + {createdBy && ( + + + Created by: {createdBy} + + + )} + + + + + Updated {dayjs(updatedAt).format('MMM D, YYYY')} + + + + {doi && } + + + {/* Action buttons - below on mobile, right side on desktop */} + + {canRevertToDraft && ( + + + + )} + + + + + + + + + + {isEditable && ( + + + + )} + + {canSubmitForReview && ( + + + + )} + + {isDeletable && ( + + + + )} + + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/components/RaftTabs.tsx b/rafts/frontend/src/components/RaftDetail/components/RaftTabs.tsx new file mode 100644 index 0000000..6a4c267 --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/RaftTabs.tsx @@ -0,0 +1,102 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import { Box, Tabs, Tab } from '@mui/material' +import { useTranslations } from 'next-intl' +import { RAFT_DETAILS_TABS } from '@/components/RaftDetail/constants' + +interface RaftTabsProps { + value: number + onChange: (event: React.SyntheticEvent, newValue: number) => void +} + +// Get props for each tab for accessibility +const a11yProps = (index: number) => { + return { + id: `raft-tab-${index}`, + 'aria-controls': `raft-tabpanel-${index}`, + } +} + +const RaftTabs = ({ value, onChange }: RaftTabsProps) => { + const t = useTranslations('raft_details') + + return ( + + + {RAFT_DETAILS_TABS.map((tab, index) => ( + + ))} + + + ) +} + +export default RaftTabs diff --git a/rafts/frontend/src/components/RaftDetail/components/RelatedRafts.tsx b/rafts/frontend/src/components/RaftDetail/components/RelatedRafts.tsx new file mode 100644 index 0000000..22b7e4e --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/RelatedRafts.tsx @@ -0,0 +1,89 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Paper, Typography, Divider } from '@mui/material' + +interface RelatedRaftsProps { + relatedRafts: string[] +} + +export default function RelatedRafts({ relatedRafts }: RelatedRaftsProps) { + return ( + + + Related RAFTs + + + {/* Here you would render a list of related RAFTs */} + + This RAFT has {relatedRafts.length} related announcements. + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/components/ReviewerSidePanel.tsx b/rafts/frontend/src/components/RaftDetail/components/ReviewerSidePanel.tsx new file mode 100644 index 0000000..0cb28cc --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/ReviewerSidePanel.tsx @@ -0,0 +1,640 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { useRouter } from 'next/navigation' +import { RaftData } from '@/types/doi' +import { RaftReview } from '@/types/reviews' +import { updateDOIStatus } from '@/actions/updateDOIStatus' +import { BACKEND_STATUS, BackendStatusType, getStatusDisplayName } from '@/shared/backendStatus' +import { claimForReview, releaseReview } from '@/actions/assignReviewer' +import { + Card, + CardContent, + Box, + Typography, + Button, + Divider, + List, + ListItem, + ListItemText, + Tooltip, + CircularProgress, + Chip, +} from '@mui/material' +import { CheckCircle, XCircle, Undo2, UserCheck, UserX } from 'lucide-react' +import StatusBadge from '@/components/common/StatusBadge' +import { formatDate, formatUserName } from '@/utilities/formatter' +import { useTranslations } from 'next-intl' +import { publishRAFTDOI } from '@/actions/publishRaftDoi' + +interface ReviewerSidePanelProps { + raftData?: Partial + review?: RaftReview + hasReview?: boolean + onNotify: (type: 'success' | 'error', text: string) => void +} + +export default function ReviewerSidePanel({ raftData, review, onNotify }: ReviewerSidePanelProps) { + const router = useRouter() + const [statusAction, setStatusAction] = useState(null) + const [actionLoading, setActionLoading] = useState(false) + const [showAllHistory, setShowAllHistory] = useState(false) + const t = useTranslations('raft_table') + + if (!raftData) { + return null + } + + const raftId = raftData._id || raftData.id || '' + // Get the assigned reviewer from the DOI data (if available) + const assignedReviewer = raftData.reviewer || null + + // Handle claiming a RAFT for review + // This sets both reviewer AND status in a single API call + const handleClaimForReview = async () => { + try { + setActionLoading(true) + setStatusAction('claim') + + // claimForReview sets both reviewer and status to "in review" in one call + const result = await claimForReview(raftId, raftData.dataDirectory) + + if (result.success) { + onNotify('success', 'RAFT status changed to In Review.') + setTimeout(() => { + router.refresh() + }, 1000) + } else { + onNotify('error', result.message || 'Failed to claim for review.') + } + } catch (error) { + console.error('Error claiming for review:', error) + onNotify('error', 'An unexpected error occurred.') + } finally { + setActionLoading(false) + } + } + + // Handle releasing a review (unassign reviewer and change status back to review ready) + // This sets both in a single API call + const handleReleaseReview = async () => { + try { + setActionLoading(true) + setStatusAction('release') + + // releaseReview unassigns reviewer and sets status to "review ready" in one call + const result = await releaseReview(raftId, raftData.dataDirectory) + + if (result.success) { + onNotify('success', 'RAFT status changed to Review Ready.') + setTimeout(() => { + router.refresh() + }, 1000) + } else { + onNotify('error', result.message || 'Failed to release review.') + } + } catch (error) { + console.error('Error releasing review:', error) + onNotify('error', 'An unexpected error occurred.') + } finally { + setActionLoading(false) + } + } + + const handlePublishingDOI = async () => { + try { + setActionLoading(true) + + const result = await publishRAFTDOI(raftId, { + dataDirectory: raftData.dataDirectory, + previousStatus: raftData.generalInfo?.status, + }) + + if (result.success) { + onNotify('success', result.message || `RAFT DOI published successfully.`) + + // Refresh the page after a short delay to get updated data + setTimeout(() => { + router.refresh() + }, 2000) + } else { + onNotify('error', result.message || 'Failed to publish RAFT DOI.') + } + } catch (error) { + console.error('Error publishing DOI: ', error) + onNotify('error', 'An unexpected error occurred.') + } + } + + // Handle status change using DOI backend + const handleStatusChange = async (newStatus: BackendStatusType) => { + try { + setActionLoading(true) + setStatusAction(newStatus) + + const result = await updateDOIStatus(raftId, newStatus, { + dataDirectory: raftData.dataDirectory, + previousStatus: raftData.generalInfo?.status, + }) + + if (result.success) { + onNotify( + 'success', + result.message || `RAFT status changed to ${getStatusDisplayName(newStatus)}.`, + ) + + // Refresh the page after a short delay to get updated data + setTimeout(() => { + router.refresh() + }, 2000) + } else { + onNotify('error', result.message || 'Failed to update status.') + } + } catch (error) { + console.error('Error updating status:', error) + onNotify('error', 'An unexpected error occurred.') + } finally { + setActionLoading(false) + } + } + + // Render status history from RAFT.json metadata or review object + const VISIBLE_HISTORY_COUNT = 3 + + const renderStatusHistory = () => { + // Prefer RAFT.json statusHistory, fall back to review statusHistory + const raftHistory = raftData.statusHistory || [] + const reviewHistory = review?.statusHistory || [] + + if (raftHistory.length === 0 && reviewHistory.length === 0) { + return ( + + No status changes recorded + + ) + } + + // Use RAFT.json history if available, otherwise use review history + const useReview = raftHistory.length === 0 + + if (useReview) { + const reversed = [...reviewHistory].reverse() + const visible = showAllHistory ? reversed : reversed.slice(0, VISIBLE_HISTORY_COUNT) + const hiddenCount = reversed.length - VISIBLE_HISTORY_COUNT + + return ( + <> + + {visible.map((change, index) => ( + + + + {t(change.fromStatus)} → {t(change.toStatus)} + + + } + secondary={ + <> + + {formatDate(change.changedAt)} by {formatUserName(change.changedBy)} + + {change.reason && ( + + Reason: {change.reason} + + )} + + } + /> + + ))} + + {hiddenCount > 0 && ( + + )} + + ) + } + + // RAFT.json history (RaftStatusChange with string changedBy) + const reversed = [...raftHistory].reverse() + const visible = showAllHistory ? reversed : reversed.slice(0, VISIBLE_HISTORY_COUNT) + const hiddenCount = reversed.length - VISIBLE_HISTORY_COUNT + + return ( + <> + + {visible.map((change, index) => ( + + + + {t(change.fromStatus)} → {t(change.toStatus)} + + + } + secondary={ + <> + + {formatDate(change.changedAt)} by {change.changedBy} + + {change.reason && ( + + Reason: {change.reason} + + )} + + } + /> + + ))} + + {hiddenCount > 0 && ( + + )} + + ) + } + + // Render assigned reviewers + const renderAssignedReviewers = () => { + // Use raftData.reviewer from DOI backend + if (assignedReviewer) { + return ( + } + label={assignedReviewer} + color="primary" + variant="outlined" + sx={{ justifyContent: 'flex-start' }} + /> + ) + } + + return ( + + No reviewers assigned + + ) + } + + // Determine available actions based on current status (using backend status values) + const getAvailableActions = () => { + const currentStatus = raftData.generalInfo?.status?.toLowerCase() + + switch (currentStatus) { + // "review ready" status: publisher can claim for review + case BACKEND_STATUS.REVIEW_READY: + return ( + + + + + + Claiming this RAFT will assign you as the reviewer and change the status to "In + Review". + + + ) + + // "in review" status: reviewer can approve, reject, or send back for revision + case BACKEND_STATUS.IN_REVIEW: + return ( + + + + + + + + + + + + + + + + + ) + + // "approved" status: can publish DOI or send back for revision + case BACKEND_STATUS.APPROVED: + return ( + + + + + + + + + ) + + // "rejected" status: author can revise and resubmit (moves back to "in progress") + case BACKEND_STATUS.REJECTED: + return ( + + + + + + ) + + // "minted" status: no actions available (published) + case BACKEND_STATUS.MINTED: + return ( + + This RAFT has been published and cannot be modified. + + ) + + // "in progress" status: draft, not yet submitted for review + case BACKEND_STATUS.IN_PROGRESS: + return ( + + This RAFT is a draft and has not been submitted for review yet. + + ) + + default: + // Show debug info for unexpected statuses + return ( + + No actions available for status: {currentStatus || 'unknown'} + + ) + } + } + + return ( + + + + Review Information + + + {/* Assigned Reviewers */} + + + Assigned Reviewers + + {renderAssignedReviewers()} + + + + + + Submission Details + + + + + Created by + + {raftData.createdBy || 'N/A'} + + + + + Submitted + + + {raftData.submittedAt + ? formatDate(raftData.submittedAt) + : raftData.createdAt + ? formatDate(raftData.createdAt) + : 'N/A'} + + + + + + Topic + + {raftData.observationInfo?.topic?.map?.((top) => ( + + {top.replace('_', ' ') || 'N/A'} + + ))} + + + + + Current Version + + + v{raftData.version || review?.currentVersion || 1} + + + + + + Current Status + + + + + {/* Status History Section */} + + + Status History + + {renderStatusHistory()} + + + + + Actions + + {getAvailableActions()} + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/components/StatusFilter.tsx b/rafts/frontend/src/components/RaftDetail/components/StatusFilter.tsx new file mode 100644 index 0000000..3fbb5d4 --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/components/StatusFilter.tsx @@ -0,0 +1,146 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import { Paper, Box, Typography, ButtonGroup, Button, Chip } from '@mui/material' +import { + OPTION_REVIEW, + OPTION_UNDER_REVIEW, + OPTION_APPROVED, + OPTION_REJECTED, +} from '@/shared/constants' +import { useTranslations } from 'next-intl' + +interface StatusFilterProps { + currentStatus: string + counts: Record + onStatusChange: (status: string) => void +} + +const StatusFilter: React.FC = ({ currentStatus, counts, onStatusChange }) => { + const t = useTranslations('review_page') + + // Map status to display name + const getStatusDisplayName = (status: string): string => { + return t(`status_${status}`) + } + + // Map status to button color + const getButtonColor = (status: string) => { + if (status === currentStatus) { + return 'primary' + } + return 'inherit' + } + + const statusOptions = [OPTION_REVIEW, OPTION_UNDER_REVIEW, OPTION_APPROVED, OPTION_REJECTED] + + return ( + + + + {t('filter_by_status')} + + + {statusOptions.map((status) => ( + + ))} + + + + + {getStatusDisplayName(currentStatus)} + + + + ) +} + +export default StatusFilter diff --git a/rafts/frontend/src/components/RaftDetail/constants.ts b/rafts/frontend/src/components/RaftDetail/constants.ts new file mode 100644 index 0000000..4d0d332 --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/constants.ts @@ -0,0 +1,68 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export const RAFT_DETAILS_TABS = ['overview', 'observation', 'misc'] diff --git a/rafts/frontend/src/components/RaftDetail/tabs/AdditionalInfoTab.tsx b/rafts/frontend/src/components/RaftDetail/tabs/AdditionalInfoTab.tsx new file mode 100644 index 0000000..a08ff4a --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/tabs/AdditionalInfoTab.tsx @@ -0,0 +1,129 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Box, Typography, Paper, Grid, Chip } from '@mui/material' +import { AlertTriangle, FileText, Type } from 'lucide-react' +import { TMiscInfo } from '@/shared/model' +import NoDataMessage from '../components/NoDataMessage' +import AttachmentText from '@/components/common/AttachmentText' + +interface AdditionalInfoTabProps { + miscInfo?: TMiscInfo | null + doiId?: string +} + +export default function AdditionalInfoTab({ miscInfo, doiId }: AdditionalInfoTabProps) { + const hasMiscData = miscInfo?.misc && miscInfo.misc.length > 0 + + if (!hasMiscData) { + return ( + } + title="No Additional Information" + message="This RAFT does not contain any additional information." + /> + ) + } + + return ( + + + {miscInfo?.misc?.map((item, index) => ( + + + + + {item.miscKey} + + : } + label={item.miscType === 'file' ? 'File' : 'Text'} + size="small" + variant="outlined" + color={item.miscType === 'file' ? 'primary' : 'default'} + /> + + {item.miscType === 'file' ? ( + + ) : ( + {item.miscValue} + )} + + + ))} + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/tabs/MeasurementsTab.tsx b/rafts/frontend/src/components/RaftDetail/tabs/MeasurementsTab.tsx new file mode 100644 index 0000000..b49bcaa --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/tabs/MeasurementsTab.tsx @@ -0,0 +1,207 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Box, Typography, Paper, Grid } from '@mui/material' +import { AlertTriangle } from 'lucide-react' +import { TMeasurementInfo } from '@/shared/model' +import NoDataMessage from '../components/NoDataMessage' + +interface MeasurementsTabProps { + measurementInfo?: TMeasurementInfo | null +} + +export default function MeasurementsTab({ measurementInfo }: MeasurementsTabProps) { + // Check if there's any measurement data + const hasMeasurementData = + measurementInfo && + ((measurementInfo.photometry && Object.values(measurementInfo.photometry).some(Boolean)) || + (measurementInfo.spectroscopy && Object.values(measurementInfo.spectroscopy).some(Boolean)) || + (measurementInfo.astrometry && Object.values(measurementInfo.astrometry).some(Boolean))) + + if (!hasMeasurementData) { + return ( + } + title="No Measurement Data" + message="This RAFT does not contain any measurement information." + /> + ) + } + + return ( + + + {/* Photometry */} + {measurementInfo?.photometry && Object.values(measurementInfo.photometry).some(Boolean) && ( + + + Photometry + + + {measurementInfo.photometry.wavelength && ( + + + Wavelength + + {measurementInfo.photometry.wavelength} + + )} + + {measurementInfo.photometry.brightness && ( + + + Brightness + + {measurementInfo.photometry.brightness} + + )} + + {measurementInfo.photometry.errors && ( + + + Errors + + {measurementInfo.photometry.errors} + + )} + + + )} + + {/* Spectroscopy */} + {measurementInfo?.spectroscopy && + Object.values(measurementInfo.spectroscopy).some(Boolean) && ( + + + Spectroscopy + + + {measurementInfo.spectroscopy.wavelength && ( + + + Wavelength + + + {measurementInfo.spectroscopy.wavelength} + + + )} + + {measurementInfo.spectroscopy.flux && ( + + + Flux + + {measurementInfo.spectroscopy.flux} + + )} + + {measurementInfo.spectroscopy.errors && ( + + + Errors + + {measurementInfo.spectroscopy.errors} + + )} + + + )} + + {/* Astrometry */} + {measurementInfo?.astrometry && Object.values(measurementInfo.astrometry).some(Boolean) && ( + + + Astrometry + + + {measurementInfo.astrometry.position && ( + + + Position + + {measurementInfo.astrometry.position} + + )} + + {measurementInfo.astrometry.timeObserved && ( + + + Time Observed + + {measurementInfo.astrometry.timeObserved} + + )} + + + )} + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/tabs/OverviewTab.tsx b/rafts/frontend/src/components/RaftDetail/tabs/OverviewTab.tsx new file mode 100644 index 0000000..b96704c --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/tabs/OverviewTab.tsx @@ -0,0 +1,291 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Box, Typography, Paper, Avatar, Grid, Button, Tooltip } from '@mui/material' +import { TAuthor } from '@/shared/model' +import React from 'react' +import AttachmentImage from '@/components/common/AttachmentImage' +import { Database } from 'lucide-react' +import { STORAGE_PARTIAL_URL } from '@/utilities/constants' + +interface OverviewTabProps { + abstract?: string + objectName?: string + relatedPublishedRafts?: string + authorInfo?: TAuthor | null + acknowledgements?: string + figure?: string + /** DOI identifier for resolving FileReference attachments */ + doiId?: string + /** Data directory path for storage links (e.g., /rafts-test/RAFTS-xxx/data) */ + dataDirectory?: string +} + +export default function OverviewTab({ + abstract, + objectName, + relatedPublishedRafts, + authorInfo, + acknowledgements, + figure, + doiId, + dataDirectory, +}: OverviewTabProps) { + // Construct the storage URL for viewing attachments using dataDirectory + const storageUrl = dataDirectory ? `${STORAGE_PARTIAL_URL}${dataDirectory}` : null + + return ( + + + {/* Data Page Link */} + {storageUrl && ( + + + + + + )} + + + {/* Abstract */} + + {objectName && ( + + + Object Name + + + {objectName} + + + )} + {abstract && ( + + + Abstract + + + {abstract} + + + )} + {figure && ( + + + Figure + + + + )} + + {/* Authors */} + + + Authors + + + {/* Corresponding Author */} + {authorInfo?.correspondingAuthor && ( + + + + Corresponding Author + + + + + {authorInfo.correspondingAuthor.firstName?.[0] || ''} + {authorInfo.correspondingAuthor.lastName?.[0] || ''} + + + + {authorInfo.correspondingAuthor.firstName}{' '} + {authorInfo.correspondingAuthor.lastName} + + + ORCID: {authorInfo.correspondingAuthor.authorORCID} + + + {authorInfo.correspondingAuthor.affiliation} + + + {authorInfo.correspondingAuthor.email} + + + + + {' '} + + )} + {/* Contributing Authors */} + + {authorInfo?.contributingAuthors && authorInfo.contributingAuthors.length > 0 && ( + + + Contributing Authors + + + {authorInfo.contributingAuthors.map((author, index) => ( + + + + + {author.firstName?.[0] || ''} + {author.lastName?.[0] || ''} + + + + {author.firstName} {author.lastName} + + + ORCID: {author.authorORCID} + + + {author.affiliation} + + + {author.email} + + + + + + ))} + + + )} + {' '} + + + {/* Collaborations */} + {authorInfo?.collaborations && authorInfo.collaborations.length > 0 && ( + + + Collaborations + + + {authorInfo.collaborations.map((collab, index) => ( + + + {collab} + + + ))} + + + )} + + + {/* Acknowledgements */} + {acknowledgements && ( + + + + + Acknowledgements + + + + {acknowledgements} + + + + + + + + Related RAFTs + + + + {relatedPublishedRafts} + + + + + + )} + + + ) +} diff --git a/rafts/frontend/src/components/RaftDetail/tabs/TechnicalInfoTab.tsx b/rafts/frontend/src/components/RaftDetail/tabs/TechnicalInfoTab.tsx new file mode 100644 index 0000000..083f0be --- /dev/null +++ b/rafts/frontend/src/components/RaftDetail/tabs/TechnicalInfoTab.tsx @@ -0,0 +1,259 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Box, Typography, Paper, Grid } from '@mui/material' +import { AlertTriangle } from 'lucide-react' +import { TTechInfo } from '@/shared/model' +import NoDataMessage from '../components/NoDataMessage' +import { + PROP_ASTROMETRY, + PROP_EPHEMERIS, + PROP_ORBITAL_ELEMENTS, + PROP_SPECTROSCOPY, +} from '@/shared/constants' +import AttachmentText from '@/components/common/AttachmentText' +import React from 'react' + +interface TechnicalInfoTabProps { + technical?: TTechInfo | null + /** DOI identifier for resolving FileReference attachments */ + doiId?: string +} + +const TechnicalInfoTab = ({ technical, doiId }: TechnicalInfoTabProps) => { + // Check if there's any technical data + const hasTechnicalData = technical && Object.values(technical).some((value) => !!value) + + if (!hasTechnicalData) { + return ( + } + title="No Technical Information" + message="This RAFT does not contain any technical details." + /> + ) + } + + return ( + + + {/* Observation Details */} + + + Observation Details + + + + {technical?.mpcId && ( + + + MPC ID + + {technical.mpcId} + + )} + + {technical?.alertId && ( + + + Alert ID + + {technical.alertId} + + )} + + {technical?.mjd && ( + + + Modified Julian Date + + {technical.mjd} + + )} + + + + + {/* Telescope & Instrument */} + + + Observation Equipment + + + + {technical?.telescope && ( + + + Telescope + + {technical.telescope} + + )} + + + + + {/* Photometry */} + + + Photometry + + + + {technical?.photometry?.wavelength && ( + + + Wavelength + + {technical?.photometry?.wavelength} + + )} + {technical?.photometry?.brightness && ( + + + Brightness + + {technical?.photometry?.brightness} + + )} + {technical?.photometry?.errors && ( + + + Errors + + {technical?.photometry?.errors} + + )} + + + + + {/* Ephemeris */} + {technical?.[PROP_EPHEMERIS] && ( + + + Ephemeris + + + + )} + + {/* Orbital Elements */} + {technical?.[PROP_ORBITAL_ELEMENTS] && ( + + + Orbital Elements + + + + )} + {/* Spectroscopy */} + {technical?.[PROP_SPECTROSCOPY] && ( + + + Spectroscopy + + + + )} + {/* Astrometry */} + {technical?.[PROP_ASTROMETRY] && ( + + + Astrometry + + + + )} + + + ) +} + +export default TechnicalInfoTab diff --git a/rafts/frontend/src/components/RaftTable/ActionMenu.tsx b/rafts/frontend/src/components/RaftTable/ActionMenu.tsx new file mode 100644 index 0000000..2f24813 --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/ActionMenu.tsx @@ -0,0 +1,377 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { + IconButton, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Divider, + Tooltip, + CircularProgress, + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, + Button, + Snackbar, + Alert, +} from '@mui/material' +import { + MoreVertical, + Eye, + Download, + Edit, + Trash2, + Link2, + Copy, + SendHorizontal, +} from 'lucide-react' +import type { RaftData } from '@/types/doi' +import { useRouter } from 'next/navigation' +import { submitForReview } from '@/actions/submitForReview' +import { deleteRaft } from '@/actions/deleteRaft' + +interface ActionMenuProps { + rowData: RaftData + onStatusChange?: () => void +} + +export default function ActionMenu({ rowData, onStatusChange }: ActionMenuProps) { + const [anchorEl, setAnchorEl] = useState(null) + const [isSubmitting, setIsSubmitting] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + const [snackbar, setSnackbar] = useState<{ + open: boolean + message: string + severity: 'success' | 'error' + }>({ open: false, message: '', severity: 'success' }) + const open = Boolean(anchorEl) + const router = useRouter() + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + const handleView = () => { + router.push(`/view/rafts/${rowData._id}`) + handleClose() + } + + const handleDownload = () => { + // Implement download functionality - typically you'd create a download URL + handleClose() + } + + const handleEdit = () => { + router.push(`/form/edit/${rowData._id}`) + handleClose() + } + + const handleDelete = () => { + handleClose() + setDeleteDialogOpen(true) + } + + const confirmDelete = async () => { + if (!rowData.id) { + setSnackbar({ + open: true, + message: 'No RAFT ID available', + severity: 'error', + }) + setDeleteDialogOpen(false) + return + } + + setIsDeleting(true) + try { + const result = await deleteRaft(rowData.id) + if (result.success) { + setSnackbar({ + open: true, + message: 'RAFT deleted successfully', + severity: 'success', + }) + setDeleteDialogOpen(false) + // Refresh the table data + onStatusChange?.() + router.refresh() + } else { + setSnackbar({ + open: true, + message: result.message || 'Failed to delete RAFT', + severity: 'error', + }) + setDeleteDialogOpen(false) + } + } catch (error) { + console.error('[ActionMenu] Error deleting RAFT:', error) + setSnackbar({ + open: true, + message: 'An error occurred while deleting RAFT', + severity: 'error', + }) + setDeleteDialogOpen(false) + } finally { + setIsDeleting(false) + } + } + + const handleSnackbarClose = () => { + setSnackbar((prev) => ({ ...prev, open: false })) + } + + const handleCopyId = () => { + navigator.clipboard.writeText(rowData._id) + // You might want to show a toast notification here + handleClose() + } + + const handleCopyLink = () => { + const url = `${window.location.origin}/raft/${rowData._id}` + navigator.clipboard.writeText(url) + // You might want to show a toast notification here + handleClose() + } + + const handleSubmitForReview = async () => { + if (!rowData.id) { + console.error('[ActionMenu] No DOI ID available') + return + } + + setIsSubmitting(true) + try { + const result = await submitForReview(rowData.id, rowData.dataDirectory) + if (result.success) { + // Refresh the table data + onStatusChange?.() + router.refresh() + } else { + console.error('[ActionMenu] Failed to submit for review:', result.message) + // You might want to show an error toast here + } + } catch (error) { + console.error('[ActionMenu] Error submitting for review:', error) + } finally { + setIsSubmitting(false) + handleClose() + } + } + + // Determine what actions are available based on status + // Note: Backend uses 'in progress' for draft, frontend uses 'draft' + const currentStatus = rowData.generalInfo?.status?.toLowerCase() ?? '' + const isDraft = ['draft', 'in progress'].includes(currentStatus) + const isEditable = isDraft || ['rejected', 'review'].includes(currentStatus) + const isDeletable = isDraft + const canSubmitForReview = isDraft + + return ( +
    + + + + + + + + + + + View + + + + + + + Download + + + + + + + + + Copy ID + + + + + + + Copy Link + + + + + + + + + Edit + + + {canSubmitForReview && ( + + + {isSubmitting ? ( + + ) : ( + + )} + + + {isSubmitting ? 'Submitting...' : 'Review Ready'} + + + )} + + + + + + + Delete + + + + + {/* Delete confirmation dialog */} + setDeleteDialogOpen(false)} + aria-labelledby="delete-dialog-title" + aria-describedby="delete-dialog-description" + > + Delete RAFT + + + Are you sure you want to delete this RAFT? This action cannot be undone. + + + + + + + + + {/* Snackbar for feedback */} + + + {snackbar.message} + + +
    + ) +} diff --git a/rafts/frontend/src/components/RaftTable/ActionsCell.tsx b/rafts/frontend/src/components/RaftTable/ActionsCell.tsx new file mode 100644 index 0000000..e9ed629 --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/ActionsCell.tsx @@ -0,0 +1,103 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import { Tooltip, IconButton } from '@mui/material' +import { Eye } from 'lucide-react' +import { useRouter } from 'next/navigation' +import { RaftData } from '@/types/doi' + +interface ActionsCellProps { + raft: RaftData + currentStatus?: string + onStatusUpdate?: () => void +} + +/** + * Cell component for rendering action buttons in the RAFT review table + */ +const ActionsCell: React.FC = ({ raft }) => { + const router = useRouter() + const raftId = raft._id + + // View details handler + const handleView = () => { + router.push(`/review/rafts/${raftId}`) + } + + return ( + + + + + + ) +} + +export default ActionsCell diff --git a/rafts/frontend/src/components/RaftTable/NoDataPlaceholder.tsx b/rafts/frontend/src/components/RaftTable/NoDataPlaceholder.tsx new file mode 100644 index 0000000..d80dc8b --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/NoDataPlaceholder.tsx @@ -0,0 +1,118 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Box, Typography, Button } from '@mui/material' +import { FileQuestion } from 'lucide-react' +import { useRouter } from 'next/navigation' + +interface NoDataPlaceholderProps { + message: string + subMessage?: string + showCreateButton?: boolean +} + +export default function NoDataPlaceholder({ + message, + subMessage, + showCreateButton = true, +}: NoDataPlaceholderProps) { + const router = useRouter() + + return ( + + + + + {message} + + + {subMessage && ( + + {subMessage} + + )} + + {showCreateButton && ( + + )} + + ) +} diff --git a/rafts/frontend/src/components/RaftTable/PublishedActionMenu.tsx b/rafts/frontend/src/components/RaftTable/PublishedActionMenu.tsx new file mode 100644 index 0000000..2639c56 --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/PublishedActionMenu.tsx @@ -0,0 +1,186 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { + IconButton, + Menu, + MenuItem, + ListItemIcon, + ListItemText, + Divider, + Tooltip, +} from '@mui/material' +import { MoreVertical, Eye, Download, Link2, Copy } from 'lucide-react' +import type { RaftData } from '@/types/doi' +import { useRouter } from 'next/navigation' + +interface ActionMenuProps { + rowData: RaftData +} + +export default function ActionMenu({ rowData }: ActionMenuProps) { + const [anchorEl, setAnchorEl] = useState(null) + const open = Boolean(anchorEl) + const router = useRouter() + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + const handleView = () => { + router.push(`/public-view/rafts/${rowData._id}`) + handleClose() + } + + const handleDownload = () => { + // Implement download functionality - typically you'd create a download URL + handleClose() + } + + const handleCopyId = () => { + navigator.clipboard.writeText(rowData._id) + // You might want to show a toast notification here + handleClose() + } + + const handleCopyLink = () => { + const url = `${window.location.origin}/raft/${rowData._id}` + navigator.clipboard.writeText(url) + // You might want to show a toast notification here + handleClose() + } + + // Determine what actions are available based on status + + return ( +
    + + + + + + + + + + + View + + + + + + + Download + + + + + + + + + Copy ID + + + + + + + Copy Link + + + + +
    + ) +} diff --git a/rafts/frontend/src/components/RaftTable/PublishedRaftTable.tsx b/rafts/frontend/src/components/RaftTable/PublishedRaftTable.tsx new file mode 100644 index 0000000..fd03a94 --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/PublishedRaftTable.tsx @@ -0,0 +1,265 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + flexRender, + SortingState, +} from '@tanstack/react-table' +import type { RaftData } from '@/types/doi' +import { columns } from './publishedColumns' +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TablePagination, + TextField, + Box, + FormControl, + InputAdornment, + Skeleton, +} from '@mui/material' +import { Search } from 'lucide-react' +import NoDataPlaceholder from './NoDataPlaceholder' + +interface RaftTableProps { + data: RaftData[] + isLoading?: boolean +} + +export default function RaftTable({ data, isLoading = false }: RaftTableProps) { + const [sorting, setSorting] = useState([]) + const [globalFilter, setGlobalFilter] = useState(undefined) + const [rowsPerPage, setRowsPerPage] = useState(10) + const table = useReactTable({ + data, + columns, + state: { + sorting, + globalFilter, + }, + onSortingChange: setSorting, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { + pagination: { + pageSize: rowsPerPage, + }, + }, + }) + + // Loading state for entire table + if (isLoading) { + return ( + + + + + + + + + {columns.map((_, index) => ( + + + + ))} + + + + {Array.from({ length: 5 }).map((_, index) => ( + + {Array.from({ length: columns.length }).map((_, cellIndex) => ( + + + + ))} + + ))} + +
    +
    + + + +
    + ) + } + + return ( + + + + setGlobalFilter(e.target.value)} + slotProps={{ + input: { + startAdornment: ( + + + + ), + }, + }} + size="small" + /> + + + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} + + ))} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + + + + )} + +
    +
    + + { + table.setPageIndex(page) + }} + onRowsPerPageChange={(e) => { + const size = e.target.value ? parseInt(e.target.value, 10) : 10 + setRowsPerPage(size) + table.setPageSize(size) + }} + /> +
    + ) +} diff --git a/rafts/frontend/src/components/RaftTable/RaftTable.tsx b/rafts/frontend/src/components/RaftTable/RaftTable.tsx new file mode 100644 index 0000000..53ef072 --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/RaftTable.tsx @@ -0,0 +1,270 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + flexRender, + SortingState, +} from '@tanstack/react-table' +import type { RaftData } from '@/types/doi' +import { columns } from './columns' +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + useTheme, + TableRow, + Paper, + TablePagination, + TextField, + Box, + FormControl, + InputAdornment, + Skeleton, +} from '@mui/material' +import { Search } from 'lucide-react' +import NoDataPlaceholder from './NoDataPlaceholder' + +interface RaftTableProps { + data: RaftData[] + isLoading?: boolean +} + +export default function RaftTable({ data, isLoading = false }: RaftTableProps) { + const theme = useTheme() + const [sorting, setSorting] = useState([]) + const [globalFilter, setGlobalFilter] = useState(undefined) + const [rowsPerPage, setRowsPerPage] = useState(10) + const table = useReactTable({ + data, + columns, + state: { + sorting, + globalFilter, + }, + onSortingChange: setSorting, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { + pagination: { + pageSize: rowsPerPage, + }, + }, + }) + + // Loading state for entire table + if (isLoading) { + return ( + + + + + + + + + {columns.map((_, index) => ( + + + + ))} + + + + {Array.from({ length: 5 }).map((_, index) => ( + + {Array.from({ length: columns.length }).map((_, cellIndex) => ( + + + + ))} + + ))} + +
    +
    + + + +
    + ) + } + + return ( + + + + setGlobalFilter(e.target.value)} + slotProps={{ + input: { + startAdornment: ( + + + + ), + }, + }} + size="small" + /> + + + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} + + ))} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + + + + )} + +
    +
    + + { + table.setPageIndex(page) + }} + onRowsPerPageChange={(e) => { + const size = e.target.value ? parseInt(e.target.value, 10) : 10 + setRowsPerPage(size) + table.setPageSize(size) + }} + /> +
    + ) +} diff --git a/rafts/frontend/src/components/RaftTable/ReviewRaftTable.tsx b/rafts/frontend/src/components/RaftTable/ReviewRaftTable.tsx new file mode 100644 index 0000000..bef9c03 --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/ReviewRaftTable.tsx @@ -0,0 +1,273 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, + getFilteredRowModel, + flexRender, + SortingState, +} from '@tanstack/react-table' +import type { RaftData } from '@/types/doi' +import { columns as baseColumns } from './columns' +import { reviewColumns } from './reviewColumns' +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TablePagination, + TextField, + Box, + FormControl, + InputAdornment, + Typography, + useTheme, + Skeleton, +} from '@mui/material' +import { Search } from 'lucide-react' + +interface RaftTableProps { + data: RaftData[] + isLoading?: boolean + isReviewMode?: boolean + currentStatus?: string + onStatusUpdate?: () => void +} + +export default function RaftTable({ + data, + isLoading = false, + isReviewMode = false, + currentStatus, + onStatusUpdate, +}: RaftTableProps) { + const theme = useTheme() + const [sorting, setSorting] = useState([]) + const [globalFilter, setGlobalFilter] = useState('') + + // Choose columns based on mode + const columns = isReviewMode ? reviewColumns(currentStatus, onStatusUpdate) : baseColumns + + const table = useReactTable({ + data, + columns, + state: { + sorting, + globalFilter, + }, + onSortingChange: setSorting, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { + pagination: { + pageSize: 10, + }, + }, + }) + + // Skeleton loading state + if (isLoading) { + return ( + + + + + + + + + {Array.from({ length: 6 }).map((_, index) => ( + + + + ))} + + + + {Array.from({ length: 5 }).map((_, index) => ( + + {Array.from({ length: 6 }).map((_, cellIndex) => ( + + + + ))} + + ))} + +
    +
    + + + +
    + ) + } + + return ( + + + + setGlobalFilter(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + }} + size="small" + /> + + + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} + + ))} + + ))} + + + {table.getRowModel().rows.length > 0 ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results found + + + )} + +
    +
    + + { + table.setPageIndex(page) + }} + onRowsPerPageChange={(e) => { + const size = e.target.value ? parseInt(e.target.value, 10) : 10 + table.setPageSize(size) + }} + /> +
    + ) +} diff --git a/rafts/frontend/src/components/RaftTable/StatusUpdateButton.tsx b/rafts/frontend/src/components/RaftTable/StatusUpdateButton.tsx new file mode 100644 index 0000000..707db65 --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/StatusUpdateButton.tsx @@ -0,0 +1,117 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import { Tooltip, IconButton } from '@mui/material' +import { updateRaftStatus } from '@/actions/updateRaftStatus' + +interface StatusUpdateButtonProps { + raftId: string + newStatus: string + icon: React.ReactNode + color: string + tooltip: string + onStatusUpdate?: () => void +} + +/** + * Button component for updating a RAFT's status + */ +const StatusUpdateButton: React.FC = ({ + raftId, + newStatus, + icon, + color, + tooltip, + onStatusUpdate, +}) => { + const [isLoading, setIsLoading] = React.useState(false) + + const handleStatusUpdate = async () => { + try { + setIsLoading(true) + await updateRaftStatus(raftId, newStatus) + if (onStatusUpdate) onStatusUpdate() + } catch (error) { + console.error('Failed to update status:', error) + } finally { + setIsLoading(false) + } + } + + return ( + + + {icon} + + + ) +} + +export default StatusUpdateButton diff --git a/rafts/frontend/src/components/RaftTable/SubmitterDetailsCell.tsx b/rafts/frontend/src/components/RaftTable/SubmitterDetailsCell.tsx new file mode 100644 index 0000000..e6f620e --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/SubmitterDetailsCell.tsx @@ -0,0 +1,94 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import { Typography } from '@mui/material' +import { TPerson } from '@/shared/model' + +interface SubmitterDetailsCellProps { + author: TPerson | undefined +} + +/** + * Cell component for displaying submitter details in the RAFT table + */ +const SubmitterDetailsCell: React.FC = ({ author }) => { + if (!author) return null + + return ( + + {author.firstName} {author.lastName} + + {author.affiliation} + + + ) +} + +export default SubmitterDetailsCell diff --git a/rafts/frontend/src/components/RaftTable/columns.tsx b/rafts/frontend/src/components/RaftTable/columns.tsx new file mode 100644 index 0000000..0bacff7 --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/columns.tsx @@ -0,0 +1,200 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { ColumnDef } from '@tanstack/react-table' +import type { RaftData } from '@/types/doi' +import ActionMenu from './ActionMenu' +import StatusBadge from '@/components/common/StatusBadge' +import { Typography, Tooltip, Box, Chip } from '@mui/material' +import dayjs from 'dayjs' +import { TRaftStatus } from '@/shared/model' +import { BACKEND_STATUS } from '@/shared/backendStatus' + +// Define table columns +export const columns: ColumnDef[] = [ + { + accessorKey: 'authorInfo.title', + header: 'Title', + cell: ({ row }) => { + const title = row.original.generalInfo?.title || '' + return ( + + + {title} + + + ) + }, + }, + { + accessorKey: 'observationInfo.objectName', + header: 'Object Name', + cell: ({ row }) => { + const objectName = row.original.observationInfo?.objectName || '' + return {objectName} + }, + }, + { + accessorKey: 'status', + header: 'Status', + cell: ({ row }) => { + const raft = row.original + const status = row.getValue('status') as TRaftStatus + const isResubmission = + raft.statusHistory?.some( + (h) => + h.toStatus === BACKEND_STATUS.IN_PROGRESS && + (raft.statusHistory?.findIndex((s) => s === h) ?? 0) > 0, + ) ?? false + return ( + + + {isResubmission && ( + + )} + + ) + }, + }, + { + accessorKey: 'observationInfo.topic', + header: 'Topic', + cell: ({ row }) => { + const topic = row.original.observationInfo?.topic || '' + return ( + + {topic ? topic?.map?.((t) => t.replace(/_/g, ' ')).join(', ') : 'Not specified'} + + ) + }, + }, + { + accessorKey: 'createdAt', + header: 'Created', + cell: ({ row }) => { + const createdAt = row.original.createdAt + try { + const date = createdAt ? dayjs(createdAt).format('MMM D, YYYY') : '' + return {date} + } catch { + return {createdAt || ''} + } + }, + }, + { + accessorKey: 'createdBy', + header: 'Author', + cell: ({ row }) => { + const createdBy = row.getValue('createdBy') as string + const author = row.original.authorInfo?.correspondingAuthor + const authorName = author ? `${author.firstName} ${author.lastName}` : createdBy || '' + + return ( + + {authorName || 'Unknown'} + + ) + }, + }, + { + id: 'actions', + header: '', + cell: ({ row }) => { + return + }, + }, +] diff --git a/rafts/frontend/src/components/RaftTable/publishedColumns.tsx b/rafts/frontend/src/components/RaftTable/publishedColumns.tsx new file mode 100644 index 0000000..fed7f3b --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/publishedColumns.tsx @@ -0,0 +1,169 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { ColumnDef } from '@tanstack/react-table' +import type { RaftData } from '@/types/doi' +import ActionMenu from './PublishedActionMenu' +import { Typography, Tooltip } from '@mui/material' +import dayjs from 'dayjs' + +// Define table columns +export const columns: ColumnDef[] = [ + { + accessorKey: 'authorInfo.title', + header: 'Title', + cell: ({ row }) => { + const title = row.original.generalInfo?.title || '' + return ( + + + {title} + + + ) + }, + }, + { + accessorKey: 'observationInfo.objectName', + header: 'Object Name', + cell: ({ row }) => { + const objectName = row.original.observationInfo?.objectName || '' + return {objectName} + }, + }, + { + accessorKey: 'status', + header: 'Status', + }, + { + accessorKey: 'observationInfo.topic', + header: 'Topic', + cell: ({ row }) => { + const topic = row.original.observationInfo?.topic || '' + return ( + + {topic ? topic?.map?.((t) => t.replace(/_/g, ' ')).join(', ') : 'Not specified'} + + ) + }, + }, + { + accessorKey: 'createdAt', + header: 'Created', + cell: ({ row }) => { + const createdAt = row.original.createdAt + try { + const date = createdAt ? dayjs(createdAt).format('MMM D, YYYY') : '' + return {date} + } catch { + return {createdAt || ''} + } + }, + }, + { + accessorKey: 'createdBy', + header: 'Author', + cell: ({ row }) => { + const createdBy = row.getValue('createdBy') as string + const author = row.original.authorInfo?.correspondingAuthor + const authorName = author ? `${author.firstName} ${author.lastName}` : createdBy || '' + + return ( + + {authorName || 'Unknown'} + + ) + }, + }, + { + id: 'actions', + header: '', + cell: ({ row }) => { + return + }, + }, +] diff --git a/rafts/frontend/src/components/RaftTable/reviewColumns.tsx b/rafts/frontend/src/components/RaftTable/reviewColumns.tsx new file mode 100644 index 0000000..79b21c6 --- /dev/null +++ b/rafts/frontend/src/components/RaftTable/reviewColumns.tsx @@ -0,0 +1,209 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import { ColumnDef } from '@tanstack/react-table' +import type { RaftData } from '@/types/doi' +import StatusBadge from '@/components/common/StatusBadge' +import { Typography, Tooltip, Chip, Box } from '@mui/material' +import { TRaftStatus } from '@/shared/model' +import SubmitterDetailsCell from './SubmitterDetailsCell' +import ActionsCell from './ActionsCell' +import { BACKEND_STATUS } from '@/shared/backendStatus' + +/** + * Defines the columns configuration for the RAFT review table + * + * @param currentStatus - Current filter status to determine which actions to show + * @param onStatusUpdate - Callback function when a status is updated + * @returns Array of column definitions for the table + */ +export const reviewColumns = ( + currentStatus?: string, + onStatusUpdate?: () => void, +): ColumnDef[] => [ + { + accessorKey: '_id', + header: 'ID', + cell: ({ row }) => { + const id = row.getValue('_id') as string + return ( + + {id.substring(0, 8)}... + + ) + }, + }, + { + accessorKey: 'authorInfo.title', + header: 'Title', + cell: ({ row }) => { + const raft = row.original + return ( + + + {raft.generalInfo?.title || 'No title'} + + + ) + }, + }, + { + accessorKey: 'authorInfo.correspondingAuthor.lastName', + header: 'Submitter', + cell: ({ row }) => { + const raft = row.original + return + }, + }, + { + accessorKey: 'observationInfo.topic', + header: 'Topic', + cell: ({ row }) => { + const topic = row.original.observationInfo?.topic + return ( + + {topic ? topic?.map?.((t) => t.replace(/_/g, ' ')).join(', ') : 'Not specified'} + + ) + }, + }, + { + accessorKey: 'generalInfo.status', + header: 'Status', + cell: ({ row }) => { + const raft = row.original + const status = raft.generalInfo?.status as TRaftStatus + const isResubmission = + raft.statusHistory?.some( + (h) => + h.toStatus === BACKEND_STATUS.IN_PROGRESS && + (raft.statusHistory?.findIndex((s) => s === h) ?? 0) > 0, + ) ?? false + return ( + + + {isResubmission && ( + + )} + + ) + }, + }, + { + accessorKey: 'submittedAt', + header: 'Submitted', + cell: ({ row }) => { + const raft = row.original + const dateStr = raft.submittedAt + if (!dateStr) { + return ( + + N/A + + ) + } + const date = new Date(dateStr) + return ( + + {date.toLocaleDateString()} + + {date.toLocaleTimeString()} + + {raft.version && raft.version > 1 && ( + + v{raft.version} + + )} + + ) + }, + }, + { + id: 'actions', + header: 'Actions', + cell: ({ row }) => ( + + ), + }, +] diff --git a/rafts/frontend/src/components/Tutorial/FormTutorial.tsx b/rafts/frontend/src/components/Tutorial/FormTutorial.tsx new file mode 100644 index 0000000..ca79532 --- /dev/null +++ b/rafts/frontend/src/components/Tutorial/FormTutorial.tsx @@ -0,0 +1,220 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import Joyride, { CallBackProps, Step, Styles } from 'react-joyride' +import { useTheme } from '@mui/material/styles' +import { useTranslations } from 'next-intl' + +interface FormTutorialProps { + run: boolean + stepIndex: number + onCallback: (data: CallBackProps) => void +} + +const FormTutorial: React.FC = ({ run, stepIndex, onCallback }) => { + const theme = useTheme() + const t = useTranslations('tutorial') + + // Define steps for the tutorial + const steps: Step[] = [ + { + target: '.form-navigation-title', + content: t('step_title'), + placement: 'bottom', + disableBeacon: true, + }, + { + target: '.form-navigation-steps', + content: t('step_navigation'), + placement: 'bottom', + }, + { + target: '.step-0', + content: t('step_author_info'), + placement: 'bottom', + }, + { + target: '.step-1', + content: t('step_announcement'), + placement: 'bottom', + }, + { + target: '.step-2', + content: t('step_observation'), + placement: 'bottom', + }, + { + target: '.step-3', + content: t('step_miscellaneous'), + placement: 'bottom', + }, + { + target: '.step-4', + content: t('step_review'), + placement: 'bottom', + }, + { + target: '.form-navigation-progress', + content: t('step_progress'), + placement: 'top', + }, + { + target: '.save-as-draft-button', + content: t('step_save_as_draft'), + placement: 'left', + }, + { + target: '.submit-button', + content: t('step_submit'), + placement: 'left', + }, + ] + + // Custom styles to match Material UI theme + const joyrideStyles: Partial = { + options: { + primaryColor: theme.palette.primary.main, + backgroundColor: theme.palette.background.paper, + textColor: theme.palette.text.primary, + arrowColor: theme.palette.background.paper, + overlayColor: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.5)', + zIndex: 10000, + }, + spotlight: { + backgroundColor: 'transparent', + border: `2px solid ${theme.palette.primary.main}`, + borderRadius: theme.shape.borderRadius, + }, + tooltip: { + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + color: theme.palette.text.primary, + fontSize: theme.typography.body1.fontSize, + padding: theme.spacing(2), + boxShadow: theme.shadows[4], + }, + tooltipContainer: { + textAlign: 'left', + }, + tooltipContent: { + padding: `${theme.spacing(1)} 0`, + }, + buttonNext: { + backgroundColor: theme.palette.primary.main, + borderRadius: theme.shape.borderRadius, + color: theme.palette.primary.contrastText, + fontSize: theme.typography.button.fontSize, + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + }, + buttonBack: { + color: theme.palette.text.secondary, + fontSize: theme.typography.button.fontSize, + marginRight: theme.spacing(1), + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + }, + buttonSkip: { + color: theme.palette.text.secondary, + fontSize: theme.typography.button.fontSize, + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + }, + buttonClose: { + color: theme.palette.text.secondary, + padding: theme.spacing(0.5), + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + }, + } + + return ( + + ) +} + +export default FormTutorial diff --git a/rafts/frontend/src/components/Tutorial/SectionTutorial.tsx b/rafts/frontend/src/components/Tutorial/SectionTutorial.tsx new file mode 100644 index 0000000..71c010d --- /dev/null +++ b/rafts/frontend/src/components/Tutorial/SectionTutorial.tsx @@ -0,0 +1,167 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import Joyride, { CallBackProps, Step, Styles } from 'react-joyride' +import { useTheme } from '@mui/material/styles' +import { useTranslations } from 'next-intl' + +interface SectionTutorialProps { + run: boolean + stepIndex: number + onCallback: (data: CallBackProps) => void + steps: Step[] + sectionName: string +} + +const SectionTutorial: React.FC = ({ run, stepIndex, onCallback, steps }) => { + const theme = useTheme() + const t = useTranslations('tutorial') + + // Custom styles to match Material UI theme + const joyrideStyles: Partial = { + options: { + primaryColor: theme.palette.primary.main, + backgroundColor: theme.palette.background.paper, + textColor: theme.palette.text.primary, + arrowColor: theme.palette.background.paper, + overlayColor: theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.3)' : 'rgba(0, 0, 0, 0.5)', + zIndex: 10000, + }, + spotlight: { + backgroundColor: 'transparent', + border: `2px solid ${theme.palette.primary.main}`, + borderRadius: theme.shape.borderRadius, + }, + tooltip: { + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + color: theme.palette.text.primary, + fontSize: theme.typography.body1.fontSize, + padding: theme.spacing(2), + boxShadow: theme.shadows[4], + }, + tooltipContainer: { + textAlign: 'left', + }, + tooltipContent: { + padding: `${theme.spacing(1)} 0`, + }, + buttonNext: { + backgroundColor: theme.palette.primary.main, + borderRadius: theme.shape.borderRadius, + color: theme.palette.primary.contrastText, + fontSize: theme.typography.button.fontSize, + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + }, + buttonBack: { + color: theme.palette.text.secondary, + fontSize: theme.typography.button.fontSize, + marginRight: theme.spacing(1), + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + }, + buttonSkip: { + color: theme.palette.text.secondary, + fontSize: theme.typography.button.fontSize, + padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + }, + buttonClose: { + color: theme.palette.text.secondary, + padding: theme.spacing(0.5), + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + }, + } + + return ( + + ) +} + +export default SectionTutorial diff --git a/rafts/frontend/src/components/User/LoginForm.tsx b/rafts/frontend/src/components/User/LoginForm.tsx new file mode 100644 index 0000000..5d6c90e --- /dev/null +++ b/rafts/frontend/src/components/User/LoginForm.tsx @@ -0,0 +1,240 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useForm } from 'react-hook-form' +import { useTranslations } from 'next-intl' +import { Button, TextField, InputAdornment, IconButton, Alert } from '@mui/material' +import VisibilityIcon from '@mui/icons-material/Visibility' +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff' +import UserIcon from '@mui/icons-material/VerifiedUser' +import { useState } from 'react' +import Link from 'next/link' +import { AuthState, LoginFormValues } from '@/actions/auth' +import Turnstile from './Turnstile' + +interface LoginFormProps { + authAction: ( + prevState: AuthState | null, + formData: LoginFormValues, + ) => Promise<{ + success: boolean + error: string | null + }> + returnUrl: string +} + +const initialState = { + success: false, + error: null, +} + +const LoginForm = ({ authAction, returnUrl }: LoginFormProps) => { + const t = useTranslations('login') + const [showPassword, setShowPassword] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [state, setState] = useState(initialState) + const [turnstileToken, setTurnstileToken] = useState(null) + + const turnstileSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + username: '', + password: '', + }, + }) + + const handleFormSubmit = async (values: LoginFormValues) => { + // Require Turnstile verification if enabled + if (turnstileSiteKey && !turnstileToken) { + setState({ + success: false, + error: 'Please complete the security verification', + }) + return + } + + setIsSubmitting(true) + + try { + // Call the server action with the form data and turnstile token + const result = await authAction(null, { + ...values, + turnstileToken: turnstileToken || undefined, + }) + setState(result) + + // Handle successful login with a single redirect + if (result.success) { + // Use hard navigation to ensure server components refresh with new session + window.location.href = returnUrl + } + } catch (error) { + console.error('Login error:', error) + setState({ + success: false, + error: 'An unexpected error occurred', + }) + } finally { + setIsSubmitting(false) + } + } + + return ( +
    + {state?.error && ( + + {state.error} + + )} + + + + + ), + }, + }} + disabled={isSubmitting} + /> + + + setShowPassword(!showPassword)} + edge="end" + > + {showPassword ? : } + + + ), + }, + }} + disabled={isSubmitting} + /> + + {t('forgot_password')} + + + {turnstileSiteKey && ( +
    + setTurnstileToken(token)} + onExpire={() => setTurnstileToken(null)} + onError={() => setTurnstileToken(null)} + /> +
    + )} + + + + ) +} + +export default LoginForm diff --git a/rafts/frontend/src/components/User/Profile.tsx b/rafts/frontend/src/components/User/Profile.tsx new file mode 100644 index 0000000..9c3ec63 --- /dev/null +++ b/rafts/frontend/src/components/User/Profile.tsx @@ -0,0 +1,166 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Paper, Typography, Box, Avatar, Divider, Chip, Grid } from '@mui/material' +import { User, Building, Mail, IdCard, UserCog } from 'lucide-react' +import { useTranslations } from 'next-intl' + +interface UserProfileProps { + user: { + name?: string + email?: string + userId?: string + role?: string + affiliation?: string + } +} + +const UserProfile = ({ user }: UserProfileProps) => { + const t = useTranslations('profile') + + return ( + + + + {user.name + ?.split(' ') + ?.map((n) => n[0]) + .join('')} + + + + + {user.name} + + + + } /> + + + + + + + + + + + + {t('email')}: + + + {user.email} + + + + + + + {t('affiliation')}: + + + {user.affiliation} + + + + + + + {t('user_id')}: + + + + {user.userId} + + + + + + + + {t('role')}: + + + + {user.role} + + + + + ) +} + +export default UserProfile diff --git a/rafts/frontend/src/components/User/RegistrationForm.tsx b/rafts/frontend/src/components/User/RegistrationForm.tsx new file mode 100644 index 0000000..cbaff90 --- /dev/null +++ b/rafts/frontend/src/components/User/RegistrationForm.tsx @@ -0,0 +1,302 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useForm } from 'react-hook-form' +import { useTranslations } from 'next-intl' +import { + Button, + TextField, + InputAdornment, + IconButton, + Alert, + Box, + Paper, + Typography, + Link as MuiLink, +} from '@mui/material' +import VisibilityIcon from '@mui/icons-material/Visibility' +import VisibilityOffIcon from '@mui/icons-material/VisibilityOff' +import PersonIcon from '@mui/icons-material/Person' +import EmailIcon from '@mui/icons-material/Email' +import BusinessIcon from '@mui/icons-material/Business' +import { useState } from 'react' +import { zodResolver } from '@hookform/resolvers/zod' +import * as z from 'zod' +import Link from 'next/link' +import { registerUser } from '@/actions/user/registerUser' + +// Define validation schema +const registerSchema = z.object({ + firstName: z.string().min(1, 'First name is required'), + lastName: z.string().min(1, 'Last name is required'), + email: z.string().email('Please enter a valid email address'), + password: z.string().min(8, 'Password must be at least 8 characters'), + affiliation: z.string().optional(), +}) + +type RegisterFormValues = z.infer + +const RegistrationForm = () => { + const t = useTranslations('registration') + const [showPassword, setShowPassword] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [formState, setFormState] = useState({ + success: false, + error: null as string | null, + message: null as string | null, + }) + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + defaultValues: { + firstName: '', + lastName: '', + email: '', + password: '', + affiliation: '', + }, + }) + + const handleFormSubmit = async (values: RegisterFormValues) => { + setIsSubmitting(true) + try { + // Call the server action directly + const result = await registerUser(values) + + if (result.success) { + setFormState({ + success: true, + error: null, + message: + result.message || + 'Registration successful. Please check your email to verify your account.', + }) + } else { + setFormState({ + success: false, + error: result.error || 'Registration failed. Please try again.', + message: null, + }) + } + } catch (error) { + setFormState({ + success: false, + error: 'An unexpected error occurred. Please try again.', + message: null, + }) + console.error('Registration error:', error) + } finally { + setIsSubmitting(false) + } + } + + return ( + + + {t('create_account')} + + + {formState.error && ( + + {formState.error} + + )} + + {formState.success && ( + + {formState.message} + + )} + +
    + + + + + ), + }} + disabled={isSubmitting || formState.success} + /> + + + + + ), + }} + disabled={isSubmitting || formState.success} + /> + + + + + + ), + }} + disabled={isSubmitting || formState.success} + /> + + + setShowPassword(!showPassword)} + edge="end" + disabled={isSubmitting || formState.success} + > + {showPassword ? : } + + + ), + }} + disabled={isSubmitting || formState.success} + /> + + + + + ), + }} + disabled={isSubmitting || formState.success} + /> + + + + + + {t('already_have_account')}{' '} + + {t('sign_in')} + + + + +
    + ) +} + +export default RegistrationForm diff --git a/rafts/frontend/src/components/User/RequestPasswordReset.tsx b/rafts/frontend/src/components/User/RequestPasswordReset.tsx new file mode 100644 index 0000000..1a9a5ee --- /dev/null +++ b/rafts/frontend/src/components/User/RequestPasswordReset.tsx @@ -0,0 +1,191 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useForm } from 'react-hook-form' +import { useTranslations } from 'next-intl' +import { Button, TextField, InputAdornment, Alert, Typography, Box, Paper } from '@mui/material' +import EmailIcon from '@mui/icons-material/Email' +import { useState } from 'react' +import Link from 'next/link' +import { requestPasswordReset } from '@/actions/user/requestPasswordReset' + +export interface RequestPasswordResetFormValues { + email: string +} + +const RequestPasswordResetForm = () => { + const t = useTranslations('password_reset') // Assuming you'll add these translations + const [isSubmitting, setIsSubmitting] = useState(false) + const [submitResult, setSubmitResult] = useState<{ + success?: boolean + error?: string + message?: string + }>({}) + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + email: '', + }, + }) + + const handleFormSubmit = async (values: RequestPasswordResetFormValues) => { + setIsSubmitting(true) + + try { + const result = await requestPasswordReset(values) + setSubmitResult(result) + } catch (error) { + setSubmitResult({ + success: false, + error: error instanceof Error ? error.message : 'An unexpected error occurred', + }) + } finally { + setIsSubmitting(false) + } + } + + return ( + + + + {t('request_reset_title')} + + + + {t('request_reset_description')} + + + {submitResult.error && ( + + {submitResult.error} + + )} + + {submitResult.success && ( + + {submitResult.message} + + )} + +
    + + + + ), + }, + }} + disabled={isSubmitting || submitResult.success} + /> + + + + + + {t('back_to_login')} + + + {t('create_account')} + + + +
    +
    + ) +} + +export default RequestPasswordResetForm diff --git a/rafts/frontend/src/components/User/ResetPasswordPage.tsx b/rafts/frontend/src/components/User/ResetPasswordPage.tsx new file mode 100644 index 0000000..e0bb2e3 --- /dev/null +++ b/rafts/frontend/src/components/User/ResetPasswordPage.tsx @@ -0,0 +1,289 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// src/components/User/ResetPasswordPage.tsx +'use client' + +import { useState } from 'react' +import { Alert, Button, Paper, Typography, CircularProgress, Box, TextField } from '@mui/material' +import { CheckCircle, XCircle, Eye, EyeOff } from 'lucide-react' +import { resetPassword } from '@/actions/user/resetPassword' +import Link from 'next/link' +import LoginFormLayout from '@/components/Layout/LoginFormLayout' +import { useForm } from 'react-hook-form' + +interface ResetPasswordFormValues { + newPassword: string + confirmPassword: string +} + +const ResetPasswordPage = ({ token }: { token: string }) => { + const [resetState, setResetState] = useState<{ + isLoading: boolean + success: boolean | null + message: string | null + }>({ + isLoading: false, + success: null, + message: null, + }) + + const [showPassword, setShowPassword] = useState(false) + + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm({ + defaultValues: { + newPassword: '', + confirmPassword: '', + }, + }) + + const passwordValue = watch('newPassword') + + const onSubmit = async (data: ResetPasswordFormValues) => { + if (!token) { + setResetState({ + isLoading: false, + success: false, + message: 'Invalid reset link. Token is missing.', + }) + return + } + + setResetState({ + ...resetState, + isLoading: true, + }) + + try { + const result = await resetPassword({ + token, + newPassword: data.newPassword, + }) + + setResetState({ + isLoading: false, + success: result.success, + message: result.success ? result.message : result.error, + }) + } catch (error) { + console.error(error) + setResetState({ + isLoading: false, + success: false, + message: 'An error occurred during password reset.', + }) + } + } + + if (!token) { + return ( + + + + Reset Password + + + + + Invalid reset link. Token is missing. + + + + + + ) + } + + return ( + + + + Reset Password + + + {resetState.isLoading ? ( + + + Resetting your password... + + ) : resetState.success ? ( + + + + {resetState.message} + + + + ) : resetState.success === false ? ( + + + + {resetState.message} + + + + ) : ( +
    + setShowPassword(!showPassword)} + variant="text" + sx={{ minWidth: '40px', padding: '5px' }} + > + {showPassword ? : } + + ), + }, + }} + /> + + value === passwordValue || 'Passwords do not match', + })} + error={!!errors.confirmPassword} + helperText={errors.confirmPassword?.message || ' '} + fullWidth + /> + + + + + + Back to Sign In + + + + )} +
    +
    + ) +} + +export default ResetPasswordPage diff --git a/rafts/frontend/src/components/User/Turnstile.tsx b/rafts/frontend/src/components/User/Turnstile.tsx new file mode 100644 index 0000000..55c7ac0 --- /dev/null +++ b/rafts/frontend/src/components/User/Turnstile.tsx @@ -0,0 +1,150 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useEffect, useRef, useCallback } from 'react' + +interface TurnstileProps { + siteKey: string + onVerify: (token: string) => void + onError?: () => void + onExpire?: () => void +} + +declare global { + interface Window { + turnstile: { + render: ( + container: HTMLElement, + options: { + sitekey: string + callback: (token: string) => void + 'error-callback'?: () => void + 'expired-callback'?: () => void + theme?: 'light' | 'dark' | 'auto' + size?: 'normal' | 'compact' + }, + ) => string + reset: (widgetId: string) => void + remove: (widgetId: string) => void + } + onloadTurnstileCallback?: () => void + } +} + +const Turnstile = ({ siteKey, onVerify, onError, onExpire }: TurnstileProps) => { + const containerRef = useRef(null) + const widgetIdRef = useRef(null) + + const renderWidget = useCallback(() => { + if (containerRef.current && window.turnstile && !widgetIdRef.current) { + widgetIdRef.current = window.turnstile.render(containerRef.current, { + sitekey: siteKey, + callback: onVerify, + 'error-callback': onError, + 'expired-callback': onExpire, + theme: 'auto', + size: 'normal', + }) + } + }, [siteKey, onVerify, onError, onExpire]) + + useEffect(() => { + // Check if script is already loaded + if (window.turnstile) { + renderWidget() + return + } + + // Load the Turnstile script + const script = document.createElement('script') + script.src = + 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback' + script.async = true + script.defer = true + + window.onloadTurnstileCallback = () => { + renderWidget() + } + + document.head.appendChild(script) + + return () => { + // Cleanup + if (widgetIdRef.current && window.turnstile) { + window.turnstile.remove(widgetIdRef.current) + widgetIdRef.current = null + } + delete window.onloadTurnstileCallback + } + }, [renderWidget]) + + return
    +} + +export default Turnstile diff --git a/rafts/frontend/src/components/User/VerifyEmailPage.tsx b/rafts/frontend/src/components/User/VerifyEmailPage.tsx new file mode 100644 index 0000000..204e3bb --- /dev/null +++ b/rafts/frontend/src/components/User/VerifyEmailPage.tsx @@ -0,0 +1,170 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useEffect, useState } from 'react' +import { Alert, Button, Paper, Typography, CircularProgress, Box } from '@mui/material' +import { CheckCircle, XCircle } from 'lucide-react' +import { verifyEmail } from '@/actions/user/verifyEmail' +import Link from 'next/link' +import LoginFormLayout from '@/components/Layout/LoginFormLayout' + +const VerifyEmailPage = ({ token }: { token: string }) => { + const [verificationState, setVerificationState] = useState<{ + isLoading: boolean + success: boolean | null + message: string | null + }>({ + isLoading: true, + success: null, + message: null, + }) + + useEffect(() => { + const verify = async () => { + if (!token) { + setVerificationState({ + isLoading: false, + success: false, + message: 'Invalid verification link. Token is missing.', + }) + return + } + + try { + const result = await verifyEmail(token) + setVerificationState({ + isLoading: false, + success: result.success, + message: result.success ? result.message : result.error, + }) + } catch { + setVerificationState({ + isLoading: false, + success: false, + message: 'An error occurred during verification.', + }) + } + } + + verify() + }, [token]) + + return ( + + + + Email Verification + + + {verificationState.isLoading ? ( + + + Verifying your email... + + ) : verificationState.success ? ( + + + + {verificationState.message} + + + + ) : ( + + + + {verificationState.message} + + + + )} + + + ) +} + +export default VerifyEmailPage diff --git a/rafts/frontend/src/components/User/management/ManageUsers.tsx b/rafts/frontend/src/components/User/management/ManageUsers.tsx new file mode 100644 index 0000000..ca0872a --- /dev/null +++ b/rafts/frontend/src/components/User/management/ManageUsers.tsx @@ -0,0 +1,217 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState, useEffect, useCallback } from 'react' +import UserTable from '@/components/User/management/UserTable' +import { User, getUsers } from '@/actions/user/getUsers' +import { Typography, Paper, Box, Chip, Alert } from '@mui/material' +import { Users, AlertCircle } from 'lucide-react' + +export default function ManageUsers() { + const [userData, setUserData] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + const [page, setPage] = useState(0) + const [limit, setLimit] = useState(10) + const [totalCount, setTotalCount] = useState(0) + const [roleFilter, setRoleFilter] = useState('') + const [searchTerm, setSearchTerm] = useState('') + const [isAdmin, setIsAdmin] = useState(false) + + const fetchData = useCallback(async () => { + setIsLoading(true) + setError(null) + + try { + const { success, data, error, meta } = await getUsers({ + page: page + 1, // API uses 1-based indexing + limit, + role: roleFilter || undefined, + search: searchTerm || undefined, + }) + if (success && data) { + setUserData(data) + setTotalCount(meta?.total || data.length) + setIsAdmin(true) // If we got data, we're an admin + } else { + console.error('Error fetching user data:', error) + setError(error || 'Failed to load user data') + setUserData([]) + + // If unauthorized, we're not an admin + if (error?.includes('Unauthorized') || error?.includes('Admin role required')) { + setIsAdmin(false) + } + } + } catch (err) { + console.error('Error in fetchData:', err) + setError('An unexpected error occurred') + setUserData([]) + } finally { + setIsLoading(false) + } + }, [page, limit, roleFilter, searchTerm]) + + // Initial data load + useEffect(() => { + fetchData() + }, [fetchData]) + + const handlePageChange = (newPage: number) => { + setPage(newPage) + } + + const handleLimitChange = (newLimit: number) => { + setLimit(newLimit) + setPage(0) // Reset to first page when changing limit + } + + const handleRoleFilterChange = (newFilter: string) => { + setRoleFilter(newFilter) + setPage(0) // Reset to first page when changing filter + } + + const handleSearchChange = (newSearch: string) => { + setSearchTerm(newSearch) + setPage(0) // Reset to first page when searching + } + + if (!isAdmin && !isLoading) { + return ( + + } sx={{ mb: 4 }}> + Access Denied + + You don't have permission to access the user management section. This area is + restricted to administrators only. + + + + ) + } + + return ( +
    +
    + + User Management + + + Manage users, roles, and permissions for the RAFT system. + + + + + + + All Users + + + + + From this panel, you can view all users, change their roles, and manage their account + status. + + +
    + +
    + {error && !isLoading ? ( + + {error} + + ) : ( + + )} +
    + +
    +
    CADC RAFT Publication System
    +
    +
    + ) +} diff --git a/rafts/frontend/src/components/User/management/UserTable.tsx b/rafts/frontend/src/components/User/management/UserTable.tsx new file mode 100644 index 0000000..fb5ce9f --- /dev/null +++ b/rafts/frontend/src/components/User/management/UserTable.tsx @@ -0,0 +1,301 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState } from 'react' +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + flexRender, + SortingState, +} from '@tanstack/react-table' +import { User } from '@/actions/user/getUsers' +import { userColumns } from './userColumns' +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TablePagination, + TextField, + Box, + FormControl, + InputAdornment, + LinearProgress, + Typography, + Select, + MenuItem, + SelectChangeEvent, + InputLabel, + IconButton, +} from '@mui/material' +import { Search, Filter, X } from 'lucide-react' + +interface UserTableProps { + data: User[] + isLoading?: boolean + onActionComplete?: () => void + totalCount: number + page: number + limit: number + onPageChange: (page: number) => void + onLimitChange: (limit: number) => void + onFilterChange: (filter: string) => void + onSearchChange: (search: string) => void + currentFilter: string + currentSearch?: string +} + +export default function UserTable({ + data, + isLoading = false, + onActionComplete, + totalCount, + page, + limit, + onPageChange, + onLimitChange, + onFilterChange, + onSearchChange, + currentFilter, + currentSearch = '', +}: UserTableProps) { + const [sorting, setSorting] = useState([]) + const [searchTerm, setSearchTerm] = useState(currentSearch) + + const columns = userColumns(onActionComplete) + + const table = useReactTable({ + data, + columns, + state: { + sorting, + }, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + manualPagination: true, + pageCount: Math.ceil(totalCount / limit), + }) + + // Handle search with debounce + const handleSearch = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value) + } + + const handleRoleFilterChange = (e: SelectChangeEvent) => { + onFilterChange(e.target.value) + } + + return ( + + + + { + if (e.key === 'Enter') { + // Trigger search when user presses Enter + const searchValue = (e.target as HTMLInputElement).value.trim() + // Pass the search term to parent component + onSearchChange(searchValue) + } + }} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: searchTerm && ( + + { + setSearchTerm('') + // Clear search + onSearchChange('') + }} + > + + + + ), + }} + size="small" + fullWidth + /> + + + + Role + + + + + {isLoading && } + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: ' 🔼', + desc: ' 🔽', + }[header.column.getIsSorted() as string] ?? null} + + ))} + + ))} + + + {data.length > 0 ? ( + data.map((row, i) => ( + + {table + .getRowModel() + .rows[i]?.getVisibleCells() + .map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + {isLoading ? ( + Loading data... + ) : ( + No users found + )} + + + )} + +
    +
    + + { + onPageChange(newPage) + }} + onRowsPerPageChange={(e) => { + const newLimit = parseInt(e.target.value, 10) + onLimitChange(newLimit) + }} + /> +
    + ) +} diff --git a/rafts/frontend/src/components/User/management/userColumns.tsx b/rafts/frontend/src/components/User/management/userColumns.tsx new file mode 100644 index 0000000..7b989e9 --- /dev/null +++ b/rafts/frontend/src/components/User/management/userColumns.tsx @@ -0,0 +1,328 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import { ColumnDef } from '@tanstack/react-table' +import { User } from '@/actions/user/getUsers' +import { + Typography, + Tooltip, + IconButton, + Box, + Chip, + Menu, + MenuItem, + ListItemIcon, + ListItemText, +} from '@mui/material' +import { + UserCog, + UserMinus, + UserCheck, + Calendar, + ChevronDown, + Shield, + User as UserIcon, +} from 'lucide-react' +import { useState } from 'react' +import { changeUserRole } from '@/actions/user/changeUserRole' +import { toggleUserStatus } from '@/actions/user/toggleUserStatus' +import dayjs from 'dayjs' + +// Define user table columns with action buttons +export const userColumns = (onActionComplete?: () => void): ColumnDef[] => { + // Role selector component + const RoleSelector = ({ userId, currentRole }: { userId: string; currentRole: string }) => { + const [anchorEl, setAnchorEl] = useState(null) + const [isLoading, setIsLoading] = useState(false) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + const handleRoleChange = async (newRole: string) => { + if (newRole === currentRole) { + handleClose() + return + } + + setIsLoading(true) + try { + await changeUserRole(userId, newRole) + if (onActionComplete) onActionComplete() + } catch (error) { + console.error('Failed to change user role:', error) + } finally { + setIsLoading(false) + handleClose() + } + } + + // These should match the UserRole enum in the backend + const roles = ['contributor', 'reviewer', 'admin'] + + return ( + <> + } + disabled={isLoading} + size="small" + /> + + {roles.map((role) => ( + handleRoleChange(role)} + selected={role === currentRole} + disabled={isLoading} + > + + {role === 'admin' ? ( + + ) : role === 'reviewer' ? ( + + ) : ( + + )} + + + + ))} + + + ) + } + + // Toggle user active status + const StatusToggle = ({ userId, isActive }: { userId: string; isActive: boolean }) => { + const [isLoading, setIsLoading] = useState(false) + + const handleToggle = async () => { + setIsLoading(true) + try { + await toggleUserStatus(userId, !isActive) + if (onActionComplete) onActionComplete() + } catch (error) { + console.error('Failed to toggle user status:', error) + } finally { + setIsLoading(false) + } + } + + return ( + + + {isActive ? : } + + + ) + } + + return [ + { + accessorKey: '_id', + header: 'ID', + cell: ({ row }) => { + const id = row.getValue('_id') as string + return ( + + {id.substring(0, 8)}... + + ) + }, + }, + { + accessorKey: 'fullName', + header: 'Name', + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + cell: ({ row }) => { + const fullName = row.getValue('fullName') as string + const email = row.original.email + + return ( + + + {fullName} + + + {email} + + + ) + }, + }, + { + accessorKey: 'affiliation', + header: 'Affiliation', + cell: ({ row }) => { + const affiliation = row.getValue('affiliation') as string + return affiliation ? ( + {affiliation} + ) : ( + + Not specified + + ) + }, + }, + { + accessorKey: 'role', + header: 'Role', + cell: ({ row }) => { + const userId = row.original._id + const role = row.getValue('role') as string + + return + }, + }, + { + accessorKey: 'isEmailVerified', + header: 'Email Verified', + cell: ({ row }) => { + const isVerified = row.getValue('isEmailVerified') as boolean + + return ( + + ) + }, + }, + { + accessorKey: 'isActive', + header: 'Status', + // Remove this incorrect accessor function + // accessorFn: (row) => !row.isLocked, + cell: ({ row }) => { + // Directly use the isActive field from the row data + const isActive = row.original.isActive + + return ( + + ) + }, + }, + { + accessorKey: 'createdAt', + header: 'Joined', + cell: ({ row }) => { + const date = new Date(row.original.createdAt) + + return ( + + + {dayjs(date).format('MMM D, YYYY')} + + ) + }, + }, + { + id: 'actions', + header: 'Actions', + cell: ({ row }) => { + const userId = row.original._id + // Use isActive directly instead of !isLocked + const isActive = row.original.isActive + + return ( + + + + ) + }, + }, + ] +} diff --git a/rafts/frontend/src/components/VersionInfo.tsx b/rafts/frontend/src/components/VersionInfo.tsx new file mode 100644 index 0000000..aad0d4c --- /dev/null +++ b/rafts/frontend/src/components/VersionInfo.tsx @@ -0,0 +1,77 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import React from 'react' +import versionInfo from '@/version.json' + +export function VersionInfo() { + return ( +
    + {versionInfo.version}@{versionInfo.date} +
    + ) +} diff --git a/rafts/frontend/src/components/common/AttachmentImage.tsx b/rafts/frontend/src/components/common/AttachmentImage.tsx new file mode 100644 index 0000000..d42d3cd --- /dev/null +++ b/rafts/frontend/src/components/common/AttachmentImage.tsx @@ -0,0 +1,284 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React, { useState, useRef, useEffect } from 'react' +import { Box, CircularProgress, IconButton, useTheme } from '@mui/material' +import { Eye } from 'lucide-react' +import ImageComponent from 'next/image' +import { + parseStoredAttachment, + isFileReference, + isBase64DataUrl, + AttachmentValue, +} from '@/types/attachments' +import AttachmentPreviewModal from './AttachmentPreviewModal' + +interface AttachmentImageProps { + /** The attachment value - can be base64 string, FileReference JSON, or direct URL */ + value: AttachmentValue | string | undefined | null + /** DOI identifier for resolving FileReference attachments */ + doiId?: string + /** Alt text for the image */ + alt?: string + /** Width of the image */ + width?: number + /** Height of the image */ + height?: number + /** Additional styles */ + style?: React.CSSProperties + /** Title for the preview modal */ + previewTitle?: string + /** Whether to show the preview button */ + showPreview?: boolean +} + +/** + * Reusable component for displaying attachment images. + * + * Handles: + * - Base64 data URLs (displayed directly) + * - FileReference objects (resolved via API route) + * - Direct URLs (displayed directly) + * + * Shows a loading spinner while images are being fetched from API. + */ +export default function AttachmentImage({ + value, + doiId, + alt = 'Attachment', + width = 100, + height = 100, + style, + previewTitle = 'Image Preview', + showPreview = true, +}: AttachmentImageProps) { + const theme = useTheme() + const [isLoading, setIsLoading] = useState(false) + const [imageUrl, setImageUrl] = useState(null) + const [previewOpen, setPreviewOpen] = useState(false) + const lastResolvedRef = useRef(null) + + // Default styles + const defaultStyle: React.CSSProperties = { + width: `${width}px`, + height: `${height}px`, + objectFit: 'cover', + borderRadius: '4px', + border: `1px solid ${theme.palette.divider}`, + ...style, + } + + // Resolve the image URL from the value + useEffect(() => { + if (!value) { + setImageUrl(null) + setIsLoading(false) + lastResolvedRef.current = null + return + } + + // Create a stable key for comparison + const valueKey = + typeof value === 'string' + ? `str:${value.substring(0, 50)}` + : isFileReference(value) + ? `ref:${value.filename}` + : null + + // Skip if we've already resolved this exact value + if (valueKey && lastResolvedRef.current === valueKey) { + return + } + + // Try to parse as FileReference + const parsed = typeof value === 'string' ? parseStoredAttachment(value) : value + + if (isFileReference(parsed) && doiId) { + // It's a FileReference - use API route + const apiUrl = `/api/attachments/${doiId}/${encodeURIComponent(parsed.filename)}` + setImageUrl(apiUrl) + setIsLoading(true) // Start loading - will be set to false by onLoad/onError + lastResolvedRef.current = valueKey + } else if (typeof value === 'string') { + // It's a base64 string or direct URL + if (isBase64DataUrl(value) || value.startsWith('http') || value.startsWith('/')) { + setImageUrl(value) + setIsLoading(false) + lastResolvedRef.current = valueKey + } + } + }, [value, doiId]) + + // No value - don't render anything + if (!value || !imageUrl) { + return null + } + + const isApiRoute = imageUrl.startsWith('/api/') + + return ( + <> + + {/* Loading spinner overlay */} + {isLoading && ( + + + + )} + + {/* Use regular img for API routes, Next.js Image for base64/external URLs */} + {isApiRoute ? ( + // eslint-disable-next-line @next/next/no-img-element + {alt} setIsLoading(false)} + onError={() => setIsLoading(false)} + onClick={showPreview ? () => setPreviewOpen(true) : undefined} + /> + ) : ( + setIsLoading(false)} + onClick={showPreview ? () => setPreviewOpen(true) : undefined} + /> + )} + + {/* Preview button */} + {showPreview && !isLoading && ( + setPreviewOpen(true)} + sx={{ + position: 'absolute', + top: -8, + right: -8, + bgcolor: 'background.paper', + border: `1px solid ${theme.palette.divider}`, + boxShadow: 1, + '&:hover': { + bgcolor: 'primary.main', + color: 'white', + }, + }} + aria-label="Preview image" + > + + + )} + + + {/* Preview Modal */} + setPreviewOpen(false)} + title={previewTitle} + type="image" + imageUrl={imageUrl || undefined} + isLoading={isLoading} + /> + + ) +} diff --git a/rafts/frontend/src/components/common/AttachmentPreviewModal.tsx b/rafts/frontend/src/components/common/AttachmentPreviewModal.tsx new file mode 100644 index 0000000..63f2391 --- /dev/null +++ b/rafts/frontend/src/components/common/AttachmentPreviewModal.tsx @@ -0,0 +1,232 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React from 'react' +import { + Dialog, + DialogTitle, + DialogContent, + IconButton, + Box, + Typography, + useTheme, + CircularProgress, +} from '@mui/material' +import { X } from 'lucide-react' + +interface AttachmentPreviewModalProps { + open: boolean + onClose: () => void + title?: string + type: 'image' | 'text' + /** Image URL for image type */ + imageUrl?: string + /** Text content for text type */ + textContent?: string + /** Loading state */ + isLoading?: boolean + /** Error message */ + error?: string +} + +/** + * Modal for previewing attachments (images or text files) in a larger view. + * Takes 80% of the viewport. + */ +export default function AttachmentPreviewModal({ + open, + onClose, + title = 'Preview', + type, + imageUrl, + textContent, + isLoading = false, + error, +}: AttachmentPreviewModalProps) { + const theme = useTheme() + + return ( + + + + {title} + + + + + + + + {isLoading ? ( + + + + Loading... + + + ) : error ? ( + + {error} + + ) : type === 'image' && imageUrl ? ( + + {/* eslint-disable-next-line @next/next/no-img-element */} + {title} + + ) : type === 'text' && textContent ? ( + +
    +              {textContent}
    +            
    +
    + ) : ( + + No content available + + )} +
    +
    + ) +} diff --git a/rafts/frontend/src/components/common/AttachmentText.tsx b/rafts/frontend/src/components/common/AttachmentText.tsx new file mode 100644 index 0000000..6981045 --- /dev/null +++ b/rafts/frontend/src/components/common/AttachmentText.tsx @@ -0,0 +1,268 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import React, { useState, useRef, useEffect } from 'react' +import { Box, CircularProgress, IconButton, Paper, Typography, useTheme } from '@mui/material' +import { Eye } from 'lucide-react' +import { parseStoredAttachment, isFileReference, AttachmentValue } from '@/types/attachments' +import AttachmentPreviewModal from './AttachmentPreviewModal' + +interface AttachmentTextProps { + /** The attachment value - can be plain text string or FileReference JSON */ + value: AttachmentValue | string | undefined | null + /** DOI identifier for resolving FileReference attachments */ + doiId?: string + /** Maximum height of the preview area */ + maxHeight?: string | number + /** Show "Preview:" label */ + showLabel?: boolean + /** Title for the preview modal */ + previewTitle?: string + /** Whether to show the preview button */ + showPreview?: boolean +} + +/** + * Reusable component for displaying text file attachments. + * + * Handles: + * - Plain text strings (displayed directly) + * - FileReference objects (fetched from API and displayed) + * + * Shows a loading spinner while text is being fetched from API. + */ +export default function AttachmentText({ + value, + doiId, + maxHeight = '200px', + showLabel = true, + previewTitle = 'Text Preview', + showPreview = true, +}: AttachmentTextProps) { + const theme = useTheme() + const [isLoading, setIsLoading] = useState(false) + const [textContent, setTextContent] = useState(null) + const [error, setError] = useState(null) + const [previewOpen, setPreviewOpen] = useState(false) + const lastResolvedRef = useRef(null) + + // Resolve the text content from the value + useEffect(() => { + if (!value) { + setTextContent(null) + setIsLoading(false) + setError(null) + lastResolvedRef.current = null + return + } + + // Create a stable key for comparison + const valueKey = + typeof value === 'string' + ? `str:${value.substring(0, 50)}` + : isFileReference(value) + ? `ref:${value.filename}` + : null + + // Skip if we've already resolved this exact value + if (valueKey && lastResolvedRef.current === valueKey) { + return + } + + // Try to parse as FileReference + const parsed = typeof value === 'string' ? parseStoredAttachment(value) : value + + if (isFileReference(parsed) && doiId) { + // It's a FileReference - fetch from API + setIsLoading(true) + setError(null) + + const apiUrl = `/api/attachments/${doiId}/${encodeURIComponent(parsed.filename)}` + + fetch(apiUrl) + .then(async (response) => { + if (!response.ok) { + throw new Error(`Failed to fetch: ${response.status}`) + } + const text = await response.text() + setTextContent(text) + lastResolvedRef.current = valueKey + }) + .catch((err) => { + console.error('[AttachmentText] Failed to fetch text:', err) + setError('Failed to load text content') + }) + .finally(() => { + setIsLoading(false) + }) + } else if (typeof value === 'string') { + // It's plain text - display directly + setTextContent(value) + setIsLoading(false) + lastResolvedRef.current = valueKey + } + }, [value, doiId]) + + // No value - don't render anything + if (!value) { + return null + } + + return ( + <> + + {/* Preview button */} + {showPreview && !isLoading && textContent && ( + setPreviewOpen(true)} + sx={{ + position: 'absolute', + top: 8, + right: 8, + bgcolor: 'background.paper', + border: `1px solid ${theme.palette.divider}`, + boxShadow: 1, + '&:hover': { + bgcolor: 'primary.main', + color: 'white', + }, + }} + aria-label="Preview text" + > + + + )} + + {showLabel && ( + + Preview: + + )} + + {isLoading ? ( + + + + ) : error ? ( + + {error} + + ) : textContent ? ( +
    +            {textContent}
    +          
    + ) : null} +
    + + {/* Preview Modal */} + setPreviewOpen(false)} + title={previewTitle} + type="text" + textContent={textContent || undefined} + isLoading={isLoading} + error={error || undefined} + /> + + ) +} diff --git a/rafts/frontend/src/components/common/StatusBadge.tsx b/rafts/frontend/src/components/common/StatusBadge.tsx new file mode 100644 index 0000000..cbb3488 --- /dev/null +++ b/rafts/frontend/src/components/common/StatusBadge.tsx @@ -0,0 +1,160 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { Chip, Tooltip } from '@mui/material' +import { BACKEND_STATUS } from '@/shared/backendStatus' +import { useTranslations } from 'next-intl' + +interface StatusBadgeProps { + status?: string +} + +const getStatusInfo = ( + status?: string, +): { bg: string; color: string; tooltipKey: string; displayKey: string } => { + const normalizedStatus = status?.toLowerCase() + + switch (normalizedStatus) { + case BACKEND_STATUS.MINTED: + case 'published': + return { + bg: 'success.main', + color: 'white', + tooltipKey: 'tooltip_published', + displayKey: 'minted', + } + + case BACKEND_STATUS.APPROVED: + return { + bg: 'info.main', + color: 'white', + tooltipKey: 'tooltip_approved', + displayKey: 'approved', + } + + case BACKEND_STATUS.REVIEW_READY: + case 'review_ready': + return { + bg: 'warning.light', + color: 'black', + tooltipKey: 'tooltip_review_ready', + displayKey: 'review ready', + } + + case BACKEND_STATUS.IN_REVIEW: + case 'under_review': + return { + bg: 'warning.main', + color: 'black', + tooltipKey: 'tooltip_in_review', + displayKey: 'in review', + } + + case BACKEND_STATUS.REJECTED: + return { + bg: 'error.main', + color: 'white', + tooltipKey: 'tooltip_rejected', + displayKey: 'rejected', + } + + case BACKEND_STATUS.IN_PROGRESS: + case 'draft': + default: + return { + bg: 'grey.400', + color: 'black', + tooltipKey: 'tooltip_draft', + displayKey: 'in progress', + } + } +} + +export default function StatusBadge({ status }: StatusBadgeProps) { + const t = useTranslations('raft_table') + + const { bg, color, tooltipKey, displayKey } = getStatusInfo(status) + + return ( + + + + ) +} diff --git a/rafts/frontend/src/config/environment.ts b/rafts/frontend/src/config/environment.ts new file mode 100644 index 0000000..17ab986 --- /dev/null +++ b/rafts/frontend/src/config/environment.ts @@ -0,0 +1,93 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// Environment configuration helper +export const isDevelopment = process.env.NODE_ENV === 'development' +export const isProduction = process.env.NODE_ENV === 'production' + +// Check if review UI is enabled (which also enables mock data in development) +export const isReviewEnabled = process.env.UI_REVIEW_ENABLED === 'true' || false + +// Check if we should use mock data +// When review is enabled, we use mock data (in both development and production for now) +export const useMockData = isReviewEnabled + +// Helper to check if API is available +export const isApiAvailable = async (): Promise => { + if (!process.env.NEXT_PUBLIC_API_BASE_URL) return false + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/health`, { + method: 'GET', + signal: AbortSignal.timeout(1000), // 1 second timeout + }).catch(() => null) + + return response?.ok || false + } catch { + return false + } +} diff --git a/rafts/frontend/src/context/RaftFormContext.tsx b/rafts/frontend/src/context/RaftFormContext.tsx new file mode 100644 index 0000000..8a56789 --- /dev/null +++ b/rafts/frontend/src/context/RaftFormContext.tsx @@ -0,0 +1,395 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { + createContext, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react' +import { clearRaftData, loadRaftData, saveRaftData } from '@/utilities/localStorage' +import { debounce } from '@/utilities/debounce' +import { + OPTION_DRAFT, + OPTION_REVIEW, + PROP_AUTHOR_INFO, + PROP_GENERAL_INFO, + PROP_MISC_INFO, + PROP_OBSERVATION_INFO, + PROP_TECHNICAL_INFO, + PROP_STATUS, +} from '@/shared/constants' +import { TRaftStatus, TRaftSubmission, TSection } from '@/shared/model' +import { VALIDATION_SCHEMAS } from '@/context/constants' +import { validateWithSchema, getValidationErrors } from '@/utilities/validation' +import { TRaftContext } from '@/context/types' +import { submitDOI } from '@/actions/submitDOI' +import { updateDOI } from '@/actions/updateDOI' +import { IResponseData } from '@/actions/types' + +// Define a type that recursively converts all leaf values to string +type RecursiveStringify = T extends object + ? { [K in keyof T]?: RecursiveStringify } + : string + +// Initial empty state structure +const initialRaftState: TRaftContext | null = null + +// Section validation state: undefined = not yet validated, true/false = validation result +type ValidationState = Partial> + +// All form sections that require validation +const ALL_SECTIONS: (keyof typeof VALIDATION_SCHEMAS)[] = [ + PROP_GENERAL_INFO, + PROP_AUTHOR_INFO, + PROP_OBSERVATION_INFO, + PROP_TECHNICAL_INFO, + PROP_MISC_INFO, +] + +// Define context type +interface RaftFormContextType { + raftData: TRaftContext | null + isLoading: boolean + updateRaftSection: (section: string, data: TSection) => void + resetForm: () => void + setFormFromFile: (data: TRaftContext) => void + submitForm: (isDraft: boolean, formData?: TRaftContext) => Promise> + isSubmitting: boolean + /** Cheap lookup from validationState - no Zod parsing */ + isSectionCompleted: (section: keyof typeof VALIDATION_SCHEMAS) => boolean + /** Derived from validationState - true only when all sections validated and passed */ + allSectionsCompleted: boolean + errors: RecursiveStringify + /** Validate a single section imperatively (runs Zod once, updates validationState + errors) */ + validateSection: (section: keyof typeof VALIDATION_SCHEMAS) => boolean + /** Validate all sections imperatively. Returns true if all pass. */ + validateAllSections: (data?: TRaftContext) => boolean + /** DOI identifier for attachment uploads (available after first save) */ + doiIdentifier: string | null +} + +// Create context +const RaftFormContext = createContext(undefined) + +interface RaftFormProviderProps { + children: ReactNode + initialRaftData?: TRaftContext | null + useLocalStorage?: boolean +} + +// Provider component +export function RaftFormProvider({ + children, + initialRaftData = null, + useLocalStorage = true, +}: RaftFormProviderProps) { + const [raftData, setRaftData] = useState(initialRaftData || initialRaftState) + const [isLoading, setIsLoading] = useState(initialRaftData === null && useLocalStorage) + const [isSubmitting, setIsSubmitting] = useState(false) + const [errors, setErrors] = useState>({}) + const [validationState, setValidationState] = useState({}) + // DOI identifier for attachment uploads - derived from raftData.id + const doiIdentifier = raftData?.id ?? null + + // Debounced localStorage save - stable ref that clears previous timeouts + const debouncedSave = useRef(debounce((data: TRaftContext) => saveRaftData(data), 500)).current + + // Load saved data on initial render if no initialRaftData was provided + useEffect(() => { + if (initialRaftData) { + setRaftData(initialRaftData) + } else if (useLocalStorage) { + const savedData = loadRaftData() + if (savedData) { + setRaftData(savedData) + } + } + setIsLoading(false) + }, [initialRaftData, useLocalStorage]) + + // Update a section of the form + const updateRaftSection = useCallback( + (section: string, data: TSection) => { + setRaftData((prevState) => { + const newState: TRaftContext = { + ...(prevState ? prevState : {}), + [section]: data, + } + + if (useLocalStorage) { + debouncedSave(newState) + } + + return newState + }) + + // Invalidate stale validation for this section — user must re-validate + if (section in VALIDATION_SCHEMAS) { + setValidationState((prev) => { + const next = { ...prev } + delete next[section as keyof typeof VALIDATION_SCHEMAS] + return next + }) + } + }, + [useLocalStorage, debouncedSave], + ) + + // Reset the entire form + const resetForm = useCallback(() => { + if (useLocalStorage) { + clearRaftData() + } + setRaftData(initialRaftData || initialRaftState) + setValidationState({}) + setErrors({}) + }, [initialRaftData, useLocalStorage]) + + // Set form from file + // Important: Strip the 'id' field to ensure importing creates a NEW RAFT + // This prevents accidentally updating an old RAFT when importing exported data + const setFormFromFile = useCallback((formData: TRaftContext) => { + if (formData) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id: _removedId, ...dataWithoutId } = formData + setRaftData(dataWithoutId) + setValidationState({}) + setErrors({}) + } + }, []) + + // Submit the form + // formData parameter allows passing synced data directly to avoid async state race condition + const submitForm = useCallback( + async (isDraft: boolean, formData?: TRaftContext) => { + try { + setIsSubmitting(true) + + // Use passed formData if provided, otherwise fall back to raftData from context + const dataToSubmit = formData || raftData + + // Determine the status to set + const newStatus = (isDraft ? OPTION_DRAFT : OPTION_REVIEW) as TRaftStatus + + // Create final submission object with status in generalInfo + const finalSubmission: TRaftContext = { + ...dataToSubmit, + [PROP_GENERAL_INFO]: { + ...(dataToSubmit?.[PROP_GENERAL_INFO] || {}), + [PROP_STATUS]: newStatus, + } as TRaftContext[typeof PROP_GENERAL_INFO], + updatedAt: new Date().toISOString(), + // Set submittedAt on every submission/resubmission for review + ...(!isDraft && { + submittedAt: new Date().toISOString(), + }), + } + + let result + + if (finalSubmission?.id) { + result = await updateDOI(finalSubmission, finalSubmission?.id) + } else { + result = await submitDOI(finalSubmission) + } + + // After successful submit, extract and store DOI identifier + if (result?.success && result.data) { + // For new submissions, result.data is the DOI URL (e.g., https://...doi/instances/25.0047) + // Extract the identifier from the URL + const identifier = typeof result.data === 'string' ? result.data.split('/').pop() : null + + if (identifier && !finalSubmission?.id) { + // Update raftData to include the id (doiIdentifier is derived from raftData.id) + setRaftData((prev) => (prev ? { ...prev, id: identifier } : prev)) + } + } + + // Clear draft after successful submission if using localStorage + if (result?.success && useLocalStorage) { + clearRaftData() + } + + return result + } catch (error) { + console.error('Error submitting RAFT:', error) + throw error + } finally { + setIsSubmitting(false) + } + }, + [raftData, useLocalStorage], + ) + + // Cheap lookup from validationState — no Zod parsing + const isSectionCompleted = useCallback( + (section: keyof typeof VALIDATION_SCHEMAS) => validationState[section] === true, + [validationState], + ) + + // Validate a single section imperatively (runs Zod once, updates validationState + errors) + const validateSection = useCallback( + (section: keyof typeof VALIDATION_SCHEMAS) => { + const sectionData = raftData?.[section] ?? {} + const isValid = validateWithSchema(VALIDATION_SCHEMAS[section], sectionData) + + setValidationState((prev) => ({ ...prev, [section]: isValid })) + setErrors((prev) => ({ + ...prev, + [section]: isValid + ? undefined + : getValidationErrors(VALIDATION_SCHEMAS[section], sectionData), + })) + + return isValid + }, + [raftData], + ) + + // Validate all sections imperatively. Optionally accepts data to validate against + // (useful for submit where we sync form refs first). + const validateAllSections = useCallback( + (data?: TRaftContext) => { + const source = data || raftData + const newValidation: ValidationState = {} + const newErrors: RecursiveStringify = {} + + for (const section of ALL_SECTIONS) { + const sectionData = source?.[section] ?? {} + const isValid = validateWithSchema(VALIDATION_SCHEMAS[section], sectionData) + newValidation[section] = isValid + if (!isValid) { + newErrors[section] = getValidationErrors(VALIDATION_SCHEMAS[section], sectionData) + } + } + + setValidationState(newValidation) + setErrors(newErrors) + + return ALL_SECTIONS.every((s) => newValidation[s] === true) + }, + [raftData], + ) + + // Derived from validationState — cheap boolean check, no Zod + const allSectionsCompleted = useMemo( + () => ALL_SECTIONS.every((section) => validationState[section] === true), + [validationState], + ) + + // Memoize context value to prevent unnecessary re-renders of all consumers + const contextValue = useMemo( + () => ({ + raftData, + isLoading, + updateRaftSection, + resetForm, + setFormFromFile, + submitForm, + isSubmitting, + isSectionCompleted, + allSectionsCompleted, + errors, + validateSection, + validateAllSections, + doiIdentifier, + }), + [ + raftData, + isLoading, + updateRaftSection, + resetForm, + setFormFromFile, + submitForm, + isSubmitting, + isSectionCompleted, + allSectionsCompleted, + errors, + validateSection, + validateAllSections, + doiIdentifier, + ], + ) + + return {children} +} + +// Custom hook to use the context +export function useRaftForm() { + const context = useContext(RaftFormContext) + if (context === undefined) { + throw new Error('useRaftForm must be used within a RaftFormProvider') + } + return context +} diff --git a/rafts/frontend/src/context/constants.ts b/rafts/frontend/src/context/constants.ts new file mode 100644 index 0000000..2412ddb --- /dev/null +++ b/rafts/frontend/src/context/constants.ts @@ -0,0 +1,94 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { + PROP_AUTHOR_INFO, + PROP_GENERAL_INFO, + PROP_MEASUREMENT_INFO, + PROP_MISC_INFO, + PROP_OBSERVATION_INFO, + PROP_TECHNICAL_INFO, +} from '@/shared/constants' +import { + authorSchema, + generalSchema, + measurementInfoSchema, + miscInfoSchema, + observationSchema, + technicalInfoSchema, +} from '@/shared/model' + +export const IS_COMPLETED = 'is_completed' + +export const VALIDATION_SCHEMAS = { + [PROP_GENERAL_INFO]: generalSchema, + [PROP_AUTHOR_INFO]: authorSchema, + [PROP_OBSERVATION_INFO]: observationSchema, + [PROP_TECHNICAL_INFO]: technicalInfoSchema, + [PROP_MEASUREMENT_INFO]: measurementInfoSchema, + [PROP_MISC_INFO]: miscInfoSchema, +} as const diff --git a/rafts/frontend/src/context/types.ts b/rafts/frontend/src/context/types.ts new file mode 100644 index 0000000..4ea12bc --- /dev/null +++ b/rafts/frontend/src/context/types.ts @@ -0,0 +1,70 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { RaftData } from '@/types/doi' + +export type TRaftContext = Partial diff --git a/rafts/frontend/src/hooks/useADESValidation.ts b/rafts/frontend/src/hooks/useADESValidation.ts new file mode 100644 index 0000000..f0293ce --- /dev/null +++ b/rafts/frontend/src/hooks/useADESValidation.ts @@ -0,0 +1,157 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState, useCallback } from 'react' +import { validateADESFile } from '@/actions/adesValidation' +import type { ADESFileKind, ADESValidationResult } from '@/actions/adesValidation.types' + +interface UseADESValidationReturn { + isValidating: boolean + validationResult: ADESValidationResult | null + validationError: string | null + validateFile: ( + file: File, + kind: ADESFileKind, + ) => Promise<{ + success: boolean + result?: ADESValidationResult + }> + resetValidation: () => void +} + +/** + * Custom hook for ADES file validation + * + * @returns Object with validation state and functions + */ +export function useADESValidation(): UseADESValidationReturn { + const [isValidating, setIsValidating] = useState(false) + const [validationResult, setValidationResult] = useState(null) + const [validationError, setValidationError] = useState(null) + + /** + * Reset validation state + */ + const resetValidation = useCallback(() => { + setValidationResult(null) + setValidationError(null) + }, []) + + /** + * Validate an ADES file + * + * @param file - File to validate + * @param kind - Type of ADES file (xml, psv, or mpc) + * @returns Object with success status and validation result + */ + const validateFile = useCallback( + async ( + file: File, + kind: ADESFileKind, + ): Promise<{ + success: boolean + result?: ADESValidationResult + }> => { + try { + setIsValidating(true) + resetValidation() + + // Create form data for the file + const formData = new FormData() + formData.append('file', file) + + // Call the server action + const { success, result, error } = await validateADESFile(formData, kind) + + if (success && result) { + setValidationResult(result) + return { success: true, result } + } else { + setValidationError(error || 'Validation failed') + return { success: false } + } + } catch (error) { + console.error('Error in validateFile:', error) + setValidationError(error instanceof Error ? error.message : 'An unexpected error occurred') + return { success: false } + } finally { + setIsValidating(false) + } + }, + [resetValidation], + ) + + return { + isValidating, + validationResult, + validationError, + validateFile, + resetValidation, + } +} diff --git a/rafts/frontend/src/hooks/useAttachmentUpload.ts b/rafts/frontend/src/hooks/useAttachmentUpload.ts new file mode 100644 index 0000000..b37f37d --- /dev/null +++ b/rafts/frontend/src/hooks/useAttachmentUpload.ts @@ -0,0 +1,434 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +/** + * React Hook for Attachment Uploads + * + * Provides a clean interface for uploading and managing file attachments + * in form components. Uses server actions to avoid CORS issues with VOSpace. + */ + +'use client' + +import { useState, useCallback } from 'react' +import { useSession } from 'next-auth/react' +import { + uploadAttachment as uploadAttachmentAction, + downloadAttachment as downloadAttachmentAction, + downloadAttachmentAsBase64 as downloadBase64Action, + deleteAttachment as deleteAttachmentAction, + UploadAttachmentResult, + DownloadAttachmentResult, +} from '@/actions/attachments' +import { + FileReference, + AttachmentValue, + isFileReference, + isBase64DataUrl, + validateAttachment, + AttachmentConfig, + blobToBase64, +} from '@/types/attachments' + +// ============================================================================ +// Types +// ============================================================================ + +export interface UseAttachmentUploadOptions { + /** DOI identifier for the RAFT (required for upload path) */ + doiIdentifier?: string + /** Attachment configuration for validation */ + config?: AttachmentConfig + /** Callback when upload completes successfully */ + onUploadComplete?: (fileReference: FileReference) => void + /** Callback when upload fails */ + onUploadError?: (error: string) => void + /** Callback when file is cleared */ + onClear?: () => void +} + +// Re-export result types for components +export type UploadResult = UploadAttachmentResult +export type DownloadResult = DownloadAttachmentResult + +export interface UseAttachmentUploadReturn { + /** Current upload state */ + isUploading: boolean + /** Upload progress (0-100) */ + progress: number + /** Error message if upload failed */ + error: string | null + /** Upload a File object */ + uploadFile: (file: File, customFilename?: string) => Promise + /** Upload content (string or Blob) */ + uploadContent: ( + content: string | Blob, + filename: string, + mimeType: string, + ) => Promise + /** Download an attachment */ + downloadFile: (filename: string, asText?: boolean) => Promise + /** Download as base64 (for images) */ + downloadAsBase64: ( + filename: string, + ) => Promise<{ success: boolean; base64?: string; error?: string }> + /** Delete an attachment */ + deleteFile: (filename: string) => Promise<{ success: boolean; error?: string }> + /** Clear the error state */ + clearError: () => void + /** Check if we have a valid session for uploads */ + canUpload: boolean + /** Resolve an attachment value to displayable content */ + resolveAttachment: (value: AttachmentValue) => Promise + /** Get the API URL for viewing an attachment (for images) */ + getAttachmentUrl: (filename: string) => string | null +} + +// ============================================================================ +// Hook Implementation +// ============================================================================ + +export function useAttachmentUpload( + options: UseAttachmentUploadOptions = {}, +): UseAttachmentUploadReturn { + const { doiIdentifier, config, onUploadComplete, onUploadError, onClear } = options + + const { data: session } = useSession() + + const [isUploading, setIsUploading] = useState(false) + const [progress, setProgress] = useState(0) + const [error, setError] = useState(null) + + // Can upload if we have a DOI identifier and are authenticated + const canUpload = Boolean(doiIdentifier && session) + + /** + * Clear error state + */ + const clearError = useCallback(() => { + setError(null) + }, []) + + /** + * Get the API URL for viewing an attachment (for images) + */ + const getAttachmentUrl = useCallback( + (filename: string): string | null => { + if (!doiIdentifier) return null + return `/api/attachments/${doiIdentifier}/${encodeURIComponent(filename)}` + }, + [doiIdentifier], + ) + + /** + * Upload a File object + */ + const uploadFile = useCallback( + async (file: File, customFilename?: string): Promise => { + if (!doiIdentifier) { + const err = 'Cannot upload: missing DOI identifier' + setError(err) + onUploadError?.(err) + return { success: false, error: err } + } + + if (!session) { + const err = 'Cannot upload: not authenticated' + setError(err) + onUploadError?.(err) + return { success: false, error: err } + } + + // Validate file if config is provided + if (config) { + const validation = validateAttachment(file, config) + if (!validation.valid) { + setError(validation.error || 'Invalid file') + onUploadError?.(validation.error || 'Invalid file') + return { success: false, error: validation.error } + } + } + + setIsUploading(true) + setProgress(0) + setError(null) + + try { + // Convert file to base64 for transport to server action + setProgress(20) + const base64 = await blobToBase64(file) + + setProgress(40) + + // Call server action + const result = await uploadAttachmentAction( + doiIdentifier, + customFilename || file.name, + base64, + file.type, + ) + + setProgress(100) + + if (result.success && result.fileReference) { + onUploadComplete?.(result.fileReference) + } else { + setError(result.error || 'Upload failed') + onUploadError?.(result.error || 'Upload failed') + } + + return result + } catch (err) { + const errorMsg = err instanceof Error ? err.message : 'Upload failed' + setError(errorMsg) + onUploadError?.(errorMsg) + return { success: false, error: errorMsg } + } finally { + setIsUploading(false) + } + }, + [doiIdentifier, session, config, onUploadComplete, onUploadError], + ) + + /** + * Upload content (string or Blob) + */ + const uploadContent = useCallback( + async (content: string | Blob, filename: string, mimeType: string): Promise => { + if (!doiIdentifier) { + const err = 'Cannot upload: missing DOI identifier' + setError(err) + onUploadError?.(err) + return { success: false, error: err } + } + + if (!session) { + const err = 'Cannot upload: not authenticated' + setError(err) + onUploadError?.(err) + return { success: false, error: err } + } + + setIsUploading(true) + setProgress(0) + setError(null) + + try { + // Convert content to base64/string for transport + setProgress(20) + let base64Content: string + + if (content instanceof Blob) { + base64Content = await blobToBase64(content) + } else { + base64Content = content + } + + setProgress(40) + + // Call server action + const result = await uploadAttachmentAction( + doiIdentifier, + filename, + base64Content, + mimeType, + ) + + setProgress(100) + + if (result.success && result.fileReference) { + onUploadComplete?.(result.fileReference) + } else { + setError(result.error || 'Upload failed') + onUploadError?.(result.error || 'Upload failed') + } + + return result + } catch (err) { + const errorMsg = err instanceof Error ? err.message : 'Upload failed' + setError(errorMsg) + onUploadError?.(errorMsg) + return { success: false, error: errorMsg } + } finally { + setIsUploading(false) + } + }, + [doiIdentifier, session, onUploadComplete, onUploadError], + ) + + /** + * Download an attachment + */ + const downloadFile = useCallback( + async (filename: string, asText: boolean = false): Promise => { + if (!doiIdentifier) { + return { + success: false, + error: 'Cannot download: missing DOI identifier', + } + } + + return downloadAttachmentAction(doiIdentifier, filename, asText) + }, + [doiIdentifier], + ) + + /** + * Download as base64 (for images) + */ + const downloadAsBase64 = useCallback( + async (filename: string): Promise<{ success: boolean; base64?: string; error?: string }> => { + if (!doiIdentifier) { + return { + success: false, + error: 'Cannot download: missing DOI identifier', + } + } + + return downloadBase64Action(doiIdentifier, filename) + }, + [doiIdentifier], + ) + + /** + * Delete an attachment + */ + const deleteFile = useCallback( + async (filename: string): Promise<{ success: boolean; error?: string }> => { + if (!doiIdentifier) { + return { success: false, error: 'Cannot delete: missing DOI identifier' } + } + + const result = await deleteAttachmentAction(doiIdentifier, filename) + if (result.success) { + onClear?.() + } + return result + }, + [doiIdentifier, onClear], + ) + + /** + * Resolve an attachment value to displayable content + * - If FileReference for image: return API URL for viewing + * - If FileReference for text: download and return content + * - If base64 string: return as-is + * - If text string: return as-is + */ + const resolveAttachment = useCallback( + async (value: AttachmentValue): Promise => { + if (!value) return null + + // If it's already a base64 data URL, return as-is + if (typeof value === 'string' && isBase64DataUrl(value)) { + return value + } + + // If it's inline text content, return as-is + if (typeof value === 'string') { + return value + } + + // If it's a FileReference, handle based on type + if (isFileReference(value)) { + if (!doiIdentifier) { + console.warn('[resolveAttachment] Cannot resolve: missing DOI identifier') + return null + } + + // For images, use the API route URL (avoids downloading to client memory) + if (value.mimeType.startsWith('image/')) { + return getAttachmentUrl(value.filename) + } + + // For text files, download via server action + const result = await downloadFile(value.filename, true) + if (result.success && result.content) { + return result.content + } + } + + return null + }, + [doiIdentifier, downloadFile, getAttachmentUrl], + ) + + return { + isUploading, + progress, + error, + uploadFile, + uploadContent, + downloadFile, + downloadAsBase64, + deleteFile, + clearError, + canUpload, + resolveAttachment, + getAttachmentUrl, + } +} diff --git a/rafts/frontend/src/hooks/useAuth.ts b/rafts/frontend/src/hooks/useAuth.ts new file mode 100644 index 0000000..37703ae --- /dev/null +++ b/rafts/frontend/src/hooks/useAuth.ts @@ -0,0 +1,228 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useSession } from 'next-auth/react' +import { useCallback, useMemo } from 'react' + +// Define role types to ensure type safety +export type UserRole = 'admin' | 'reviewer' | 'contributor' | 'user' | string + +// Define permissions as an object for future extensibility +export type Permissions = { + canCreateRaft: boolean + canEditRaft: boolean + canReviewRaft: boolean + canApproveRaft: boolean + canManageUsers: boolean +} + +interface UseAuthReturn { + // Session status + isLoading: boolean + isAuthenticated: boolean + + // User info + user: { + id?: string + name?: string | null + email?: string | null + role?: string + affiliation?: string + } | null + + // Token + accessToken?: string + + // Role utilities + role?: string + hasRole: (role: UserRole | UserRole[]) => boolean + hasAnyRole: (roles: UserRole[]) => boolean + hasAllRoles: (roles: UserRole[]) => boolean + + // Permission utilities + permissions: Permissions + hasPermission: (permission: keyof Permissions) => boolean +} + +/** + * Hook for accessing authentication state and role-based utilities + */ +export const useAuth = (): UseAuthReturn => { + const { data: session, status } = useSession() + const isLoading = status === 'loading' + const isAuthenticated = status === 'authenticated' + + // Role checking utility functions + const hasRole = useCallback( + (role: UserRole | UserRole[]): boolean => { + if (!session?.user?.role) return false + + const userRole = session.user.role + + if (Array.isArray(role)) { + return role.includes(userRole as UserRole) + } + + return userRole === role + }, + [session?.user], + ) + + const hasAnyRole = useCallback( + (roles: UserRole[]): boolean => { + if (!session?.user?.role) return false + return roles.some((role) => session.user!.role === role) + }, + [session?.user], + ) + + const hasAllRoles = useCallback( + (roles: UserRole[]): boolean => { + if (!session?.user?.role) return false + return roles.every((role) => session.user!.role === role) + }, + [session?.user], + ) + + // Calculate permissions based on the user's role + const permissions = useMemo((): Permissions => { + const role = session?.user?.role + + // Default permissions - no access + const defaultPermissions: Permissions = { + canCreateRaft: false, + canEditRaft: false, + canReviewRaft: false, + canApproveRaft: false, + canManageUsers: false, + } + + // If no role or not authenticated, return default permissions + if (!role || !isAuthenticated) return defaultPermissions + + // Role-based permissions mapping + switch (role) { + case 'admin': + return { + canCreateRaft: true, + canEditRaft: true, + canReviewRaft: true, + canApproveRaft: true, + canManageUsers: true, + } + case 'reviewer': + return { + canCreateRaft: true, + canEditRaft: true, + canReviewRaft: true, + canApproveRaft: true, + canManageUsers: false, + } + case 'contributor': + return { + canCreateRaft: true, + canEditRaft: true, + canReviewRaft: false, + canApproveRaft: false, + canManageUsers: false, + } + case 'user': + return { + canCreateRaft: false, + canEditRaft: false, + canReviewRaft: false, + canApproveRaft: false, + canManageUsers: false, + } + default: + return defaultPermissions + } + }, [session?.user?.role, isAuthenticated]) + + // Permission check utility + const hasPermission = useCallback( + (permission: keyof Permissions): boolean => { + return permissions[permission] === true + }, + [permissions], + ) + + return { + isLoading, + isAuthenticated, + user: session?.user || null, + accessToken: session?.accessToken, + role: session?.user?.role, + hasRole, + hasAnyRole, + hasAllRoles, + permissions, + hasPermission, + } +} diff --git a/rafts/frontend/src/hooks/useFormTutorial.ts b/rafts/frontend/src/hooks/useFormTutorial.ts new file mode 100644 index 0000000..013f79f --- /dev/null +++ b/rafts/frontend/src/hooks/useFormTutorial.ts @@ -0,0 +1,124 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState, useEffect } from 'react' +import { ACTIONS, CallBackProps, STATUS } from 'react-joyride' + +const TUTORIAL_KEY = 'raft-form-tutorial-completed' + +export const useFormTutorial = () => { + const [run, setRun] = useState(false) + const [stepIndex, setStepIndex] = useState(0) + + // Check if tutorial has been completed before + useEffect(() => { + const hasCompletedTutorial = localStorage.getItem(TUTORIAL_KEY) + if (!hasCompletedTutorial) { + // Start tutorial after a short delay for better UX + setTimeout(() => setRun(true), 1000) + } + }, []) + + const handleJoyrideCallback = (data: CallBackProps) => { + const { status, type, index, action } = data + const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED] + + if (finishedStatuses.includes(status)) { + // Mark tutorial as completed + localStorage.setItem(TUTORIAL_KEY, 'true') + setRun(false) + } else if (type === 'step:after') { + // Move forward or backward depending on the action + if (action === ACTIONS.PREV) { + setStepIndex(index - 1) + } else { + setStepIndex(index + 1) + } + } + } + + const startTutorial = () => { + setStepIndex(0) + setRun(true) + } + + const resetTutorial = () => { + localStorage.removeItem(TUTORIAL_KEY) + setStepIndex(0) + setRun(true) + } + + return { + run, + stepIndex, + handleJoyrideCallback, + startTutorial, + resetTutorial, + } +} diff --git a/rafts/frontend/src/hooks/useSectionTutorial.ts b/rafts/frontend/src/hooks/useSectionTutorial.ts new file mode 100644 index 0000000..af98572 --- /dev/null +++ b/rafts/frontend/src/hooks/useSectionTutorial.ts @@ -0,0 +1,136 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useState, useEffect } from 'react' +import { ACTIONS, CallBackProps, STATUS } from 'react-joyride' + +interface UseSectionTutorialProps { + sectionName: string + autoStart?: boolean +} + +export const useSectionTutorial = ({ sectionName, autoStart = false }: UseSectionTutorialProps) => { + const [run, setRun] = useState(false) + const [stepIndex, setStepIndex] = useState(0) + + const TUTORIAL_KEY = `raft-${sectionName}-tutorial-completed` + + // Check if tutorial has been completed before + useEffect(() => { + if (autoStart) { + const hasCompletedTutorial = localStorage.getItem(TUTORIAL_KEY) + if (!hasCompletedTutorial) { + // Start tutorial after a short delay for better UX + setTimeout(() => setRun(true), 1500) + } + } + }, [autoStart, TUTORIAL_KEY]) + + const handleJoyrideCallback = (data: CallBackProps) => { + const { status, type, index, action } = data + const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED] + + if (finishedStatuses.includes(status)) { + // Mark tutorial as completed + localStorage.setItem(TUTORIAL_KEY, 'true') + setRun(false) + } else if (type === 'step:after') { + // Move forward or backward depending on the action + if (action === ACTIONS.PREV) { + setStepIndex(index - 1) + } else { + setStepIndex(index + 1) + } + } + } + + const startTutorial = () => { + setStepIndex(0) + setRun(true) + } + + const resetTutorial = () => { + localStorage.removeItem(TUTORIAL_KEY) + setStepIndex(0) + setRun(true) + } + + const hasCompletedTutorial = () => { + return localStorage.getItem(TUTORIAL_KEY) === 'true' + } + + return { + run, + stepIndex, + handleJoyrideCallback, + startTutorial, + resetTutorial, + hasCompletedTutorial, + } +} diff --git a/rafts/frontend/src/hooks/useSetCookies.ts b/rafts/frontend/src/hooks/useSetCookies.ts new file mode 100644 index 0000000..bd13bbb --- /dev/null +++ b/rafts/frontend/src/hooks/useSetCookies.ts @@ -0,0 +1,138 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' + +import { useEffect, useState, useCallback } from 'react' +import { CADC_COOKIE_DOMAIN_URL, CANFAR_COOKIE_DOMAIN_URL } from '@/auth/cadc-auth/constants' +import { Session } from 'next-auth' + +export function useSetCADCCookies(session: Session) { + const [isSettingUp, setIsSettingUp] = useState(false) + const [isSetupComplete, setIsSetupComplete] = useState(false) + + // Function to set up cookies using iframes + const setupCookiesWithIframe = useCallback(async () => { + if (!session?.accessToken || isSetupComplete) return false + + setIsSettingUp(true) + + try { + // Create a promise that resolves when iframe loads or times out + const loadInIframe = (url: string, token: string): Promise => { + return new Promise((resolve) => { + const iframe = document.createElement('iframe') + iframe.style.cssText = 'position:absolute;width:1px;height:1px;opacity:0;' + iframe.src = `${url}${token}` + + // Set timeout to resolve after 5 seconds + const timeout = setTimeout(() => { + if (document.body.contains(iframe)) { + document.body.removeChild(iframe) + } + resolve(true) // Assume success after timeout + }, 5000) + + // Resolve when loaded + iframe.onload = () => { + clearTimeout(timeout) + document.body.removeChild(iframe) + resolve(true) + } + + document.body.appendChild(iframe) + }) + } + + // Load both endpoints sequentially + await loadInIframe(CANFAR_COOKIE_DOMAIN_URL, session.accessToken) + await loadInIframe(CADC_COOKIE_DOMAIN_URL, session.accessToken) + + setIsSetupComplete(true) + return true + } catch (error) { + console.error('Error setting CADC cookies:', error) + return false + } finally { + setIsSettingUp(false) + } + }, [session?.accessToken, isSetupComplete]) + + // Automatic setup + useEffect(() => { + if (session?.accessToken && !isSetupComplete && !isSettingUp) { + setupCookiesWithIframe() + } + }, [session, isSetupComplete, isSettingUp, setupCookiesWithIframe]) + + // Return function for manual triggering + return { + setupCookies: setupCookiesWithIframe, + isSettingUp, + isSetupComplete, + } +} diff --git a/rafts/frontend/src/i18n/request.ts b/rafts/frontend/src/i18n/request.ts new file mode 100644 index 0000000..d1bd846 --- /dev/null +++ b/rafts/frontend/src/i18n/request.ts @@ -0,0 +1,81 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { getRequestConfig } from 'next-intl/server' +import { routing } from './routing' +import { Lang } from '@/types/common' + +export default getRequestConfig(async ({ requestLocale }) => { + let locale = (await requestLocale) as Lang + if (!locale || !routing.locales.includes(locale)) { + locale = routing.defaultLocale + } + return { + locale, + messages: (await import(`../../messages/${locale}.json`)).default, + } +}) diff --git a/rafts/frontend/src/i18n/routing.ts b/rafts/frontend/src/i18n/routing.ts new file mode 100644 index 0000000..bf13fe9 --- /dev/null +++ b/rafts/frontend/src/i18n/routing.ts @@ -0,0 +1,76 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { defineRouting } from 'next-intl/routing' +import { createNavigation } from 'next-intl/navigation' + +export const routing = defineRouting({ + locales: ['en', 'fr'], + defaultLocale: 'en', +}) + +export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing) diff --git a/rafts/frontend/src/middleware.ts b/rafts/frontend/src/middleware.ts new file mode 100644 index 0000000..2f9097b --- /dev/null +++ b/rafts/frontend/src/middleware.ts @@ -0,0 +1,175 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { auth } from '@/auth/cadc-auth/credentials' +import createIntlMiddleware from 'next-intl/middleware' +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' +import { routing } from '@/i18n/routing' + +// Create the internationalization middleware +const intlMiddleware = createIntlMiddleware(routing) + +// Define routes and their allowed roles +const routePermissions = { + '/form/create': ['contributor', 'reviewer', 'admin'], + '/review': ['reviewer', 'admin'], + '/admin': ['admin'], +} + +// Check if a user with the given role can access a specific path +const canAccessRoute = (path: string, role?: string): boolean => { + // Check each route pattern + for (const [route, allowedRoles] of Object.entries(routePermissions)) { + if (path.startsWith(route)) { + return role ? allowedRoles.includes(role) : false + } + } + + // If no specific restrictions found, allow access by default + return true +} + +// Strip locale prefix from pathname (e.g., /en/view/rafts -> /view/rafts) +const stripLocale = (pathname: string): string => { + const localePattern = /^\/(en|fr)(\/|$)/ + return pathname.replace(localePattern, '/') +} + +const middleware = async (request: NextRequest) => { + const publicPaths = ['/login', '/login-required', '/api/auth', '/unauthorized', '/public-view'] + const pathnameWithoutLocale = stripLocale(request.nextUrl.pathname) + + // Check if it's a public path (or the home page) + const isPublicPath = + pathnameWithoutLocale === '/' || + publicPaths.some((path) => pathnameWithoutLocale.startsWith(path)) + + // Get session + const session = await auth() + const userRole = session?.user?.role + + // Skip locale handling for API routes + const pathname = request.nextUrl.pathname + const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' + const pathWithoutBase = pathname.replace(basePath, '') + + if (pathWithoutBase.startsWith('/api/')) { + // For API routes, just return without locale handling + const response = NextResponse.next() + response.headers.set('Cache-Control', 'no-store, max-age=0') + response.headers.set('Pragma', 'no-cache') + return response + } + + // Handle locale for non-API routes + const response = await intlMiddleware(request) + + // Set cache control headers to prevent caching of authenticated pages + response.headers.set('Cache-Control', 'no-store, max-age=0') + response.headers.set('Pragma', 'no-cache') + + // If it's a public path, just handle the locale + if (isPublicPath) { + return response + } + + // If not authenticated (no session), redirect to login-required page + if (!session) { + const loginRequiredUrl = new URL('/login-required', request.url) + // Preserve the current path as returnUrl - strip basePath to avoid double prepending + const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '' + const returnPath = request.nextUrl.pathname.replace(basePath, '') || '/' + loginRequiredUrl.searchParams.set('returnUrl', returnPath) + return NextResponse.redirect(loginRequiredUrl) + } + + // Check role-based access (use pathname without locale prefix) + const hasAccess = canAccessRoute(pathnameWithoutLocale, userRole) + + if (!hasAccess) { + return NextResponse.redirect(new URL('/unauthorized', request.url)) + } + + // Continue with the locale-handled response + return response +} + +export default middleware + +// Fix 5: Make matcher basePath-aware +export const config = { + matcher: [ + /* + * Match all request paths except: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ + '/((?!_next/static|_next/image|favicon.ico).*)', + ], +} diff --git a/rafts/frontend/src/services/attachmentService.ts b/rafts/frontend/src/services/attachmentService.ts new file mode 100644 index 0000000..96c8e23 --- /dev/null +++ b/rafts/frontend/src/services/attachmentService.ts @@ -0,0 +1,549 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +/** + * Attachment Service for VOSpace + * + * Provides functions to upload, download, and manage file attachments + * stored in VOSpace. Uses the VOSpace Transfer API for reliable uploads. + */ + +import { + FileReference, + createFileReference, + sanitizeFilename, + isTextMimeType, + blobToBase64, +} from '@/types/attachments' +import { + VAULT_SYNCTRANS_ENDPOINT, + VOSPACE_AUTHORITY, + VAULT_BASE_ENDPOINT, +} from '@/services/constants' +import { getCurrentPath } from '@/services/utils' + +// ============================================================================ +// Constants +// ============================================================================ + +const VAULT_NODES_ENDPOINT = + process.env.NEXT_VAULT_NODES_ENDPOINT || 'https://ws-cadc.canfar.net/vault/nodes' + +// ============================================================================ +// Path Utilities +// ============================================================================ + +/** + * Get the VOSpace path for a specific attachment (goes directly into data folder) + */ +export function getAttachmentPath(doiIdentifier: string, filename: string): string { + const basePath = getCurrentPath(doiIdentifier) + return `${basePath}/${sanitizeFilename(filename)}` +} + +/** + * Get the download URL for an attachment + */ +export function getAttachmentDownloadUrl(doiIdentifier: string, filename: string): string { + const path = getAttachmentPath(doiIdentifier, filename) + return `${VAULT_BASE_ENDPOINT}/${path}` +} + +// ============================================================================ +// VOSpace Transfer Protocol +// ============================================================================ + +/** + * Build VOSpace transfer XML for pushing a file + */ +function buildTransferXml(vosPath: string): string { + const vosUri = `vos://${VOSPACE_AUTHORITY}/${vosPath}` + return ` + + ${vosUri} + pushToVoSpace + +` +} + +/** + * Build DataNode XML for creating a file node + */ +function buildDataNodeXml(vosPath: string): string { + const vosUri = `vos://${VOSPACE_AUTHORITY}/${vosPath}` + return ` + +` +} + +/** + * Parse UWS job XML to extract phase and transferDetails URL + */ +function parseUwsJobXml(xml: string): { + phase: string + transferDetailsUrl: string | null + error: string | null +} { + const phaseMatch = xml.match(/([^<]+)<\/uws:phase>/) + const phase = phaseMatch ? phaseMatch[1] : 'UNKNOWN' + + const transferDetailsMatch = xml.match(/id="transferDetails"[^>]*xlink:href="([^"]+)"/) + const transferDetailsUrl = transferDetailsMatch ? transferDetailsMatch[1] : null + + const errorMatch = xml.match(/([^<]+)<\/uws:message>/) + const error = errorMatch ? errorMatch[1] : null + + return { phase, transferDetailsUrl, error } +} + +/** + * Parse VOSpace transfer XML to extract the actual upload endpoint + */ +function parseTransferDetailsXml(xml: string): string | null { + const endpointMatch = xml.match(/([^<]+)<\/vos:endpoint>/) + return endpointMatch ? endpointMatch[1] : null +} + +// ============================================================================ +// Node Management +// ============================================================================ + +/** + * Ensure a DataNode (file placeholder) exists in VOSpace + */ +async function ensureDataNodeExists( + vosPath: string, + accessToken: string, +): Promise<{ exists: boolean; created: boolean }> { + const nodeUrl = `${VAULT_NODES_ENDPOINT}/${vosPath}` + const authHeaders = { Cookie: `CADC_SSO=${accessToken}` } + + // Check if node exists + const getResponse = await fetch(nodeUrl, { + method: 'GET', + headers: { ...authHeaders, Accept: 'text/xml' }, + }) + + if (getResponse.ok) { + return { exists: true, created: false } + } + + // Create the data node + const nodeXml = buildDataNodeXml(vosPath) + const createResponse = await fetch(nodeUrl, { + method: 'PUT', + headers: { ...authHeaders, 'Content-Type': 'text/xml' }, + body: nodeXml, + }) + + if (createResponse.ok || createResponse.status === 201) { + return { exists: true, created: true } + } + + if (createResponse.status === 409) { + return { exists: true, created: false } + } + + return { exists: false, created: false } +} + +// ============================================================================ +// Transfer Negotiation +// ============================================================================ + +/** + * Negotiate a VOSpace transfer to get the actual upload endpoint + */ +async function negotiateTransfer(vosPath: string, accessToken: string): Promise { + const authHeaders = { Cookie: `CADC_SSO=${accessToken}` } + const transferXml = buildTransferXml(vosPath) + + // Step 1: POST transfer request to synctrans + const synctransResponse = await fetch(VAULT_SYNCTRANS_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'text/xml', ...authHeaders }, + body: transferXml, + redirect: 'manual', + }) + + if (synctransResponse.status !== 303) { + const errorText = await synctransResponse.text().catch(() => '') + throw new Error(`Transfer negotiation failed: ${synctransResponse.status} ${errorText}`) + } + + const locationUrl = synctransResponse.headers.get('Location') + if (!locationUrl) { + throw new Error('No Location header in synctrans response') + } + + // Step 2: GET the job XML to check status + const jobUrl = locationUrl.replace('/results/transferDetails', '') + const jobResponse = await fetch(jobUrl, { + method: 'GET', + headers: { ...authHeaders, Accept: 'text/xml' }, + }) + + if (!jobResponse.ok) { + throw new Error(`Failed to get transfer job: ${jobResponse.status}`) + } + + const jobXml = await jobResponse.text() + const { phase, transferDetailsUrl, error } = parseUwsJobXml(jobXml) + + if (phase === 'ERROR') { + throw new Error(`Transfer job failed: ${error || 'Unknown error'}`) + } + + if (!transferDetailsUrl) { + throw new Error('No transferDetails URL in job response') + } + + // Step 3: GET the transferDetails URL to get the actual endpoint + const transferDetailsResponse = await fetch(transferDetailsUrl, { + method: 'GET', + headers: { ...authHeaders, Accept: 'text/xml' }, + }) + + if (!transferDetailsResponse.ok) { + throw new Error(`Failed to get transfer details: ${transferDetailsResponse.status}`) + } + + const transferDetailsXml = await transferDetailsResponse.text() + const uploadEndpoint = parseTransferDetailsXml(transferDetailsXml) + + if (!uploadEndpoint) { + throw new Error('No endpoint URL in transfer details') + } + + return uploadEndpoint +} + +// ============================================================================ +// Public API: Upload +// ============================================================================ + +export interface UploadResult { + success: boolean + fileReference?: FileReference + error?: string +} + +/** + * Upload an attachment file to VOSpace + * + * @param doiIdentifier - The DOI identifier (e.g., "RAFTS-xxx") + * @param filename - The filename to use for storage + * @param content - File content as Blob or string + * @param mimeType - MIME type of the file + * @param accessToken - CADC SSO access token + * @returns Upload result with FileReference on success + */ +export async function uploadAttachment( + doiIdentifier: string, + filename: string, + content: Blob | string, + mimeType: string, + accessToken: string, +): Promise { + try { + const sanitizedFilename = sanitizeFilename(filename) + const filePath = getAttachmentPath(doiIdentifier, sanitizedFilename) + + // Step 1: Ensure DataNode exists (data folder is created by DOI service) + const nodeResult = await ensureDataNodeExists(filePath, accessToken) + + if (!nodeResult.exists) { + throw new Error(`Failed to create data node: ${filePath}`) + } + + // Step 2: Negotiate transfer and get upload endpoint + const uploadEndpoint = await negotiateTransfer(filePath, accessToken) + + // Step 3: Prepare content for upload + let body: Blob | string + let contentLength: number + + if (content instanceof Blob) { + body = content + contentLength = content.size + } else { + body = content + contentLength = new Blob([content]).size + } + + // Step 4: Upload the file + const response = await fetch(uploadEndpoint, { + method: 'PUT', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + 'Content-Type': mimeType, + }, + body, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + throw new Error(`Upload failed: ${response.status} ${errorText}`) + } + + // Create and return FileReference + const fileReference = createFileReference(sanitizedFilename, mimeType, contentLength) + + return { success: true, fileReference } + } catch (error) { + console.error('[uploadAttachment] Error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + } + } +} + +/** + * Upload a File object to VOSpace + * Convenience wrapper around uploadAttachment + */ +export async function uploadFile( + doiIdentifier: string, + file: File, + accessToken: string, + customFilename?: string, +): Promise { + const filename = customFilename || file.name + return uploadAttachment(doiIdentifier, filename, file, file.type, accessToken) +} + +// ============================================================================ +// Public API: Download +// ============================================================================ + +export interface DownloadResult { + success: boolean + content?: Blob | string + mimeType?: string + error?: string +} + +/** + * Download an attachment from VOSpace + * + * @param doiIdentifier - The DOI identifier + * @param filename - The filename to download + * @param accessToken - CADC SSO access token + * @param asText - If true, return content as text string + * @returns Download result with content on success + */ +export async function downloadAttachment( + doiIdentifier: string, + filename: string, + accessToken: string, + asText: boolean = false, +): Promise { + try { + const url = getAttachmentDownloadUrl(doiIdentifier, filename) + + const response = await fetch(url, { + method: 'GET', + headers: { Cookie: `CADC_SSO=${accessToken}` }, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + throw new Error(`Download failed: ${response.status} ${errorText}`) + } + + const mimeType = response.headers.get('Content-Type') || 'application/octet-stream' + let content: Blob | string + + if (asText || isTextMimeType(mimeType)) { + content = await response.text() + } else { + content = await response.blob() + } + + return { success: true, content, mimeType } + } catch (error) { + console.error('[downloadAttachment] Error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + } + } +} + +/** + * Download an attachment and convert to base64 data URL + * Useful for displaying images in the browser + */ +export async function downloadAttachmentAsBase64( + doiIdentifier: string, + filename: string, + accessToken: string, +): Promise<{ success: boolean; base64?: string; error?: string }> { + const result = await downloadAttachment(doiIdentifier, filename, accessToken, false) + + if (!result.success || !result.content) { + return { success: false, error: result.error } + } + + try { + const blob = result.content instanceof Blob ? result.content : new Blob([result.content]) + const base64 = await blobToBase64(blob) + return { success: true, base64 } + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to convert to base64', + } + } +} + +// ============================================================================ +// Public API: Delete +// ============================================================================ + +/** + * Delete an attachment from VOSpace + * + * @param doiIdentifier - The DOI identifier + * @param filename - The filename to delete + * @param accessToken - CADC SSO access token + * @returns Success status + */ +export async function deleteAttachment( + doiIdentifier: string, + filename: string, + accessToken: string, +): Promise<{ success: boolean; error?: string }> { + try { + const path = getAttachmentPath(doiIdentifier, filename) + const nodeUrl = `${VAULT_NODES_ENDPOINT}/${path}` + + const response = await fetch(nodeUrl, { + method: 'DELETE', + headers: { Cookie: `CADC_SSO=${accessToken}` }, + }) + + if (!response.ok && response.status !== 404) { + const errorText = await response.text().catch(() => '') + throw new Error(`Delete failed: ${response.status} ${errorText}`) + } + + return { success: true } + } catch (error) { + console.error('[deleteAttachment] Error:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + } + } +} + +// ============================================================================ +// Public API: Batch Operations +// ============================================================================ + +export interface AttachmentInfo { + fieldName: string + value: string | Blob + filename: string + mimeType: string +} + +/** + * Upload multiple attachments + * Returns a map of fieldName -> FileReference + */ +export async function uploadMultipleAttachments( + doiIdentifier: string, + attachments: AttachmentInfo[], + accessToken: string, +): Promise<{ results: Record; errors: Record }> { + const results: Record = {} + const errors: Record = {} + + for (const attachment of attachments) { + const result = await uploadAttachment( + doiIdentifier, + attachment.filename, + attachment.value, + attachment.mimeType, + accessToken, + ) + + if (result.success && result.fileReference) { + results[attachment.fieldName] = result.fileReference + } else { + errors[attachment.fieldName] = result.error || 'Upload failed' + } + } + + return { results, errors } +} diff --git a/rafts/frontend/src/services/canfarStorage.ts b/rafts/frontend/src/services/canfarStorage.ts new file mode 100644 index 0000000..f9542ce --- /dev/null +++ b/rafts/frontend/src/services/canfarStorage.ts @@ -0,0 +1,443 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { + DEFAULT_RAFT_NAME, + VAULT_BASE_ENDPOINT, + VAULT_SYNCTRANS_ENDPOINT, + VOSPACE_AUTHORITY, +} from '@/services/constants' + +// VOSpace nodes endpoint for creating/managing nodes +const VAULT_NODES_ENDPOINT = + process.env.NEXT_VAULT_NODES_ENDPOINT || 'https://ws-cadc.canfar.net/vault/nodes' +import { StorageResponse } from '@/services/types' +import { getCurrentPath } from '@/services/utils' +import { TRaftContext } from '@/context/types' +import { IResponseData } from '@/actions/types' + +/** + * Build a VOSpace transfer XML document for pushing a file + * @param vosPath - The VOSpace path (e.g., rafts-test/RAFTS-xxx/data/RAFT.json) + * @returns XML string for the transfer request + */ +function buildTransferXml(vosPath: string): string { + const vosUri = `vos://${VOSPACE_AUTHORITY}/${vosPath}` + return ` + + ${vosUri} + pushToVoSpace + +` +} + +/** + * Build VOSpace DataNode XML for creating a file node + */ +function buildDataNodeXml(vosPath: string): string { + const vosUri = `vos://${VOSPACE_AUTHORITY}/${vosPath}` + return ` + +` +} + +/** + * Create or update a DataNode in VOSpace + * This is required before uploading file content + */ +async function ensureDataNodeExists( + vosPath: string, + accessToken: string, +): Promise<{ exists: boolean; created: boolean }> { + const nodeUrl = `${VAULT_NODES_ENDPOINT}/${vosPath}` + + const authHeaders = { + Cookie: `CADC_SSO=${accessToken}`, + } + + // First, check if node already exists + const getResponse = await fetch(nodeUrl, { + method: 'GET', + headers: { + ...authHeaders, + Accept: 'text/xml', + }, + }) + + if (getResponse.ok) { + return { exists: true, created: false } + } + + if (getResponse.status !== 404) { + // Continue to try creating the node anyway + } + + // Node doesn't exist, create it + const nodeXml = buildDataNodeXml(vosPath) + + const createResponse = await fetch(nodeUrl, { + method: 'PUT', + headers: { + ...authHeaders, + 'Content-Type': 'text/xml', + }, + body: nodeXml, + }) + + if (createResponse.ok || createResponse.status === 201) { + return { exists: true, created: true } + } + + // If 409 conflict, node already exists + if (createResponse.status === 409) { + return { exists: true, created: false } + } + + // Return what we have - transfer might still work + return { exists: false, created: false } +} + +/** + * Parse UWS job XML to extract phase and transferDetails URL + */ +function parseUwsJobXml(xml: string): { + phase: string + transferDetailsUrl: string | null + error: string | null +} { + // Extract phase + const phaseMatch = xml.match(/([^<]+)<\/uws:phase>/) + const phase = phaseMatch ? phaseMatch[1] : 'UNKNOWN' + + // Extract transferDetails URL from results (id="transferDetails") + const transferDetailsMatch = xml.match(/id="transferDetails"[^>]*xlink:href="([^"]+)"/) + const transferDetailsUrl = transferDetailsMatch ? transferDetailsMatch[1] : null + + // Extract error message if present + const errorMatch = xml.match(/([^<]+)<\/uws:message>/) + const error = errorMatch ? errorMatch[1] : null + + return { phase, transferDetailsUrl, error } +} + +/** + * Parse VOSpace transfer XML to extract the actual upload endpoint + */ +function parseTransferDetailsXml(xml: string): string | null { + // Look for endpoint inside protocol element + // URL + const endpointMatch = xml.match(/([^<]+)<\/vos:endpoint>/) + return endpointMatch ? endpointMatch[1] : null +} + +/** + * Negotiate a VOSpace transfer to get the actual upload endpoint + * Uses UWS (Universal Worker Service) job pattern + */ +async function negotiateTransfer(transferXml: string, accessToken: string): Promise { + // Use Cookie auth (CADC_SSO) for consistency with DOI API + const authHeaders = { + Cookie: `CADC_SSO=${accessToken}`, + } + + // Step 1: POST transfer request to synctrans - don't follow redirects + const synctransResponse = await fetch(VAULT_SYNCTRANS_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'text/xml', + ...authHeaders, + }, + body: transferXml, + redirect: 'manual', // Don't follow redirects automatically + }) + + // Expect 303 redirect with Location header + if (synctransResponse.status !== 303) { + const errorText = await synctransResponse.text().catch(() => '') + throw new Error( + `Expected 303 redirect from synctrans, got ${synctransResponse.status}: ${errorText}`, + ) + } + + // Get the transfer job URL from Location header (strip /results/transferDetails) + const locationUrl = synctransResponse.headers.get('Location') + if (!locationUrl) { + throw new Error('No Location header in synctrans response') + } + + // Extract the job URL (without /results/transferDetails) + const jobUrl = locationUrl.replace('/results/transferDetails', '') + + // Step 2: GET the job XML to check status + const jobResponse = await fetch(jobUrl, { + method: 'GET', + headers: { + ...authHeaders, + Accept: 'text/xml', + }, + }) + + if (!jobResponse.ok) { + const errorText = await jobResponse.text().catch(() => '') + throw new Error(`Failed to get transfer job: ${jobResponse.status}: ${errorText}`) + } + + const jobXml = await jobResponse.text() + + // Parse the UWS job response + const { phase, transferDetailsUrl, error } = parseUwsJobXml(jobXml) + + if (phase === 'ERROR') { + throw new Error(`Transfer job failed: ${error || 'Unknown error'}`) + } + + if (!transferDetailsUrl) { + throw new Error('No transferDetails URL in transfer job response') + } + + // Step 3: GET the transferDetails URL to get the actual endpoint + const transferDetailsResponse = await fetch(transferDetailsUrl, { + method: 'GET', + headers: { + ...authHeaders, + Accept: 'text/xml', + }, + }) + + if (!transferDetailsResponse.ok) { + const errorText = await transferDetailsResponse.text().catch(() => '') + throw new Error( + `Failed to get transfer details: ${transferDetailsResponse.status}: ${errorText}`, + ) + } + + const transferDetailsXml = await transferDetailsResponse.text() + + // Parse the actual upload endpoint from transfer details + const uploadEndpoint = parseTransferDetailsXml(transferDetailsXml) + + if (!uploadEndpoint) { + throw new Error('No endpoint URL in transfer details response') + } + + return uploadEndpoint +} + +export const uploadFile = async ( + doiIdentifier: string, + fileJson: TRaftContext, + accessToken: string, +): Promise> => { + try { + const currentPath = getCurrentPath(doiIdentifier) + const filePath = `${currentPath}/${DEFAULT_RAFT_NAME}` + + // Step 1: Ensure the DataNode exists (required before upload) + await ensureDataNodeExists(filePath, accessToken) + + // Step 2: Build transfer XML and negotiate upload endpoint + const transferXml = buildTransferXml(filePath) + + const uploadEndpoint = await negotiateTransfer(transferXml, accessToken) + + // Convert JSON to string for upload + const jsonContent = JSON.stringify(fileJson, null, 2) + + // Step 3: PUT the file content to the negotiated endpoint + const response = await fetch(uploadEndpoint, { + method: 'PUT', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + 'Content-Type': 'application/json', + }, + body: jsonContent, + }) + + if (!response.ok) { + const errorText = (await response.text()) || 'Failed to upload file' + console.error('[uploadFile] Error:', response.status, errorText) + return { + error: { + status: response.status, + message: errorText, + }, + } + } + + return { data: { uploaded: true } } + } catch (error) { + console.error('[uploadFile] Exception:', error) + return { + error: { + status: 500, + message: error instanceof Error ? error.message : 'Unknown error occurred', + }, + } + } +} + +/** + * Update metadata fields in an existing RAFT.json file. + * Downloads the current file, merges metadata, and re-uploads. + */ +export const updateRaftMetadata = async ( + dataDirectory: string, + metadataUpdate: Partial, + accessToken: string, +): Promise> => { + try { + // Step 1: Download current RAFT.json + const downloadResult = await downloadRaftFile(dataDirectory, accessToken) + if (!downloadResult.success || !downloadResult.data) { + return { success: false, message: 'Failed to download RAFT.json for metadata update' } + } + + // Step 2: Merge metadata fields (append to statusHistory instead of replacing) + const existingHistory = + (downloadResult.data as TRaftContext & { statusHistory?: unknown[] }).statusHistory || [] + const newHistory = + (metadataUpdate as TRaftContext & { statusHistory?: unknown[] }).statusHistory || [] + + // Deduplicate: skip new entries whose toStatus matches the last recorded toStatus + // This prevents redundant entries when an action fires for an already-completed transition + const lastExisting = existingHistory[existingHistory.length - 1] as + | { toStatus?: string } + | undefined + const filteredNewHistory = newHistory.filter((entry) => { + const typed = entry as { toStatus?: string } + if (lastExisting && lastExisting.toStatus === typed.toStatus) { + return false + } + return true + }) + + const updatedData: TRaftContext = { + ...downloadResult.data, + ...metadataUpdate, + statusHistory: [...existingHistory, ...filteredNewHistory], + } + + // Step 3: Re-upload via the existing uploadFile function + // Extract the DOI identifier from the dataDirectory path + // dataDirectory is like "/rafts-test/RAFTS-xxx/data" — we need the RAFTS-xxx part + const parts = dataDirectory.split('/') + const doiIndex = parts.findIndex((p) => p.startsWith('RAFTS-')) + const doiIdentifier = doiIndex >= 0 ? parts[doiIndex] : parts[parts.length - 2] + + const uploadResult = await uploadFile(doiIdentifier, updatedData, accessToken) + if (uploadResult.error) { + return { success: false, message: uploadResult.error.message } + } + + return { success: true, data: true } + } catch (error) { + console.error('[updateRaftMetadata] Error:', error) + return { + success: false, + message: error instanceof Error ? error.message : 'Unknown error', + } + } +} + +export const downloadRaftFile = async ( + dataDirectory: string, + accessToken: string, +): Promise> => { + try { + // Remove leading slash from dataDirectory to avoid double slashes + const cleanedDataDirectory = dataDirectory.startsWith('/') + ? dataDirectory.slice(1) + : dataDirectory + const url = `${VAULT_BASE_ENDPOINT}/${cleanedDataDirectory}/${DEFAULT_RAFT_NAME}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + }, + }) + if (!response.ok) { + const errorText = (await response.text()) || 'Failed to download a RAFT file' + console.error('[downloadRaftFile] Error:', response.status, errorText) + return { + success: false, + message: errorText, + } + } + const data = await response.json() + return { success: true, data } + } catch (error) { + console.error('[downloadRaftFile] Exception:', error) + return { + success: false, + message: error instanceof Error ? error.message : 'Unknown error occurred', + } + } +} diff --git a/rafts/frontend/src/services/constants.ts b/rafts/frontend/src/services/constants.ts new file mode 100644 index 0000000..36060c2 --- /dev/null +++ b/rafts/frontend/src/services/constants.ts @@ -0,0 +1,89 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +/** + * Module for interacting with the CANFAR storage service + */ + +// Base URL for CANFAR storage vault API +// Configurable via environment variables for local development +// Default: CANFAR production storage +// Local dev: https://haproxy.cadc.dao.nrc.ca/cavern/files +export const CANFAR_STORAGE_BASE_URL = + process.env.NEXT_CANFAR_STORAGE_BASE_URL || 'https://www.canfar.net/storage/vault/file' +export const VAULT_BASE_ENDPOINT = + process.env.NEXT_VAULT_BASE_ENDPOINT || 'https://ws-cadc.canfar.net/vault/files' +// Synctrans endpoint for file uploads (PUT) +export const VAULT_SYNCTRANS_ENDPOINT = + process.env.NEXT_VAULT_SYNCTRANS_ENDPOINT || 'https://ws-cadc.canfar.net/vault/synctrans' +export const DEFAULT_RAFT_NAME = 'RAFTS.json' +// VOSpace authority for constructing vos:// URIs +export const VOSPACE_AUTHORITY = process.env.NEXT_VOSPACE_AUTHORITY || 'cadc.nrc.ca~vault' +// https://ws-cadc.canfar.net/vault/files/AstroDataCitationDOI/CISTI.CANFAR/25.0047/data/5397631_7799701.pdf +// partial url - configurable for local development (e.g., 'rafts-test') +export const CITE_ULR = process.env.NEXT_CITE_URL || 'AstroDataCitationDOI/CISTI.CANFAR' +console.log('[constants] NEXT_CITE_URL resolved to:', CITE_ULR) diff --git a/rafts/frontend/src/services/processAttachments.ts b/rafts/frontend/src/services/processAttachments.ts new file mode 100644 index 0000000..4ce4099 --- /dev/null +++ b/rafts/frontend/src/services/processAttachments.ts @@ -0,0 +1,318 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +/** + * Process Attachments for Save + * + * This module handles converting inline attachments (base64 images, text content) + * to FileReferences by uploading them to VOSpace during form save. + * + * This is a server-side only module - called from server actions. + */ + +import { TRaftContext } from '@/context/types' +import { + PROP_OBSERVATION_INFO, + PROP_TECHNICAL_INFO, + PROP_FIGURE, + PROP_EPHEMERIS, + PROP_ORBITAL_ELEMENTS, + PROP_SPECTROSCOPY, + PROP_ASTROMETRY, +} from '@/shared/constants' +import { + FileReference, + isFileReference, + isBase64DataUrl, + getMimeTypeFromBase64, + createFileReference, +} from '@/types/attachments' +import { uploadToVOSpaceFolder } from '@/services/vospaceTransfer' +import { getCurrentPath } from '@/services/utils' + +const ATTACHMENTS_FOLDER = 'attachments' + +/** + * Attachment field configuration + */ +interface AttachmentFieldConfig { + section: string + field: string + filename: string + mimeType: string + isBinary: boolean +} + +/** + * All attachment fields in the form + */ +const ATTACHMENT_FIELDS: AttachmentFieldConfig[] = [ + { + section: PROP_OBSERVATION_INFO, + field: PROP_FIGURE, + filename: 'figure.png', + mimeType: 'image/png', + isBinary: true, + }, + { + section: PROP_TECHNICAL_INFO, + field: PROP_EPHEMERIS, + filename: 'ephemeris.txt', + mimeType: 'text/plain', + isBinary: false, + }, + { + section: PROP_TECHNICAL_INFO, + field: PROP_ORBITAL_ELEMENTS, + filename: 'orbital.txt', + mimeType: 'text/plain', + isBinary: false, + }, + { + section: PROP_TECHNICAL_INFO, + field: PROP_SPECTROSCOPY, + filename: 'spectrum.txt', + mimeType: 'text/plain', + isBinary: false, + }, + { + section: PROP_TECHNICAL_INFO, + field: PROP_ASTROMETRY, + filename: 'astrometry.xml', + mimeType: 'text/xml', + isBinary: false, + }, +] + +/** + * Check if a value is already a FileReference (either object or JSON string) + */ +function isAlreadyFileReference(value: unknown): boolean { + if (!value) return false + if (isFileReference(value)) return true + + // Check if it's a JSON string representing a FileReference + if (typeof value === 'string' && value.startsWith('{')) { + try { + const parsed = JSON.parse(value) + return isFileReference(parsed) + } catch { + return false + } + } + + return false +} + +/** + * Parse a FileReference from a value (could be object or JSON string) + */ +function parseFileReference(value: unknown): FileReference | null { + if (isFileReference(value)) return value + + if (typeof value === 'string' && value.startsWith('{')) { + try { + const parsed = JSON.parse(value) + if (isFileReference(parsed)) return parsed + } catch { + // Not valid JSON + } + } + + return null +} + +/** + * Convert base64 data URL to Buffer for server-side upload + */ +function base64ToBuffer(base64: string): Buffer { + // Remove the data URL prefix (e.g., "data:image/png;base64,") + const base64Data = base64.split(',')[1] + return Buffer.from(base64Data, 'base64') +} + +/** + * Get the attachments folder path for a DOI + */ +function getAttachmentsFolderPath(doiIdentifier: string): string { + const basePath = getCurrentPath(doiIdentifier) + return `${basePath}/${ATTACHMENTS_FOLDER}` +} + +/** + * Process all attachments in form data, uploading them to VOSpace + * and replacing inline content with FileReferences. + * + * @param formData - The form data to process + * @param doiIdentifier - The DOI identifier for the RAFT + * @param accessToken - The CADC SSO access token + * @returns The processed form data with FileReferences instead of inline content + */ +export async function processAttachmentsForSave( + formData: TRaftContext, + doiIdentifier: string, + accessToken: string, +): Promise { + // Create a deep copy of the form data + const processedData: TRaftContext = JSON.parse(JSON.stringify(formData)) + const folderPath = getAttachmentsFolderPath(doiIdentifier) + + for (const config of ATTACHMENT_FIELDS) { + const section = processedData[config.section as keyof TRaftContext] as Record + if (!section) continue + + const value = section[config.field] + if (!value) continue + + // Skip if already a FileReference + if (isAlreadyFileReference(value)) { + // Ensure it's stored as JSON string for consistency + const fileRef = parseFileReference(value) + if (fileRef) { + section[config.field] = JSON.stringify(fileRef) + } + continue + } + + // Skip if value is empty string + if (typeof value === 'string' && value.trim() === '') { + continue + } + + try { + let content: string | Buffer + let mimeType = config.mimeType + let filename = config.filename + let contentSize = 0 + + if (config.isBinary && typeof value === 'string' && isBase64DataUrl(value)) { + // Binary content (e.g., image) - convert from base64 + mimeType = getMimeTypeFromBase64(value) + content = base64ToBuffer(value) + contentSize = content.length + + // Update filename extension based on actual mime type + if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') { + filename = 'figure.jpg' + } else if (mimeType === 'image/png') { + filename = 'figure.png' + } + } else if (typeof value === 'string') { + // Text content + content = value + contentSize = Buffer.byteLength(content, 'utf8') + } else { + continue + } + + // Upload to VOSpace + const result = await uploadToVOSpaceFolder( + folderPath, + filename, + content, + mimeType, + accessToken, + ) + + if (result.success) { + // Create FileReference and store as JSON string + const fileReference = createFileReference(filename, mimeType, contentSize) + section[config.field] = JSON.stringify(fileReference) + } else { + console.error(`[processAttachments] ${config.field}: Upload failed:`, result.error) + // Keep original value on failure - don't lose user data + } + } catch (error) { + console.error(`[processAttachments] ${config.field}: Exception:`, error) + // Keep original value on failure + } + } + + return processedData +} + +/** + * Get the count of inline attachments that need to be uploaded + */ +export function getInlineAttachmentCount(formData: TRaftContext): number { + let count = 0 + + for (const config of ATTACHMENT_FIELDS) { + const section = formData[config.section as keyof TRaftContext] as Record + if (!section) continue + + const value = section[config.field] + if (!value) continue + + // Count if it's inline content (not already a FileReference) + if (!isAlreadyFileReference(value) && typeof value === 'string' && value.trim() !== '') { + count++ + } + } + + return count +} diff --git a/rafts/frontend/src/services/types.ts b/rafts/frontend/src/services/types.ts new file mode 100644 index 0000000..f67b8df --- /dev/null +++ b/rafts/frontend/src/services/types.ts @@ -0,0 +1,81 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// Types +export interface CANFARUploadResponse { + code: number +} + +export interface StorageError { + status: number + message: string +} + +export interface StorageResponse { + data?: T + error?: StorageError +} diff --git a/rafts/frontend/src/services/utils.ts b/rafts/frontend/src/services/utils.ts new file mode 100644 index 0000000..73a9f16 --- /dev/null +++ b/rafts/frontend/src/services/utils.ts @@ -0,0 +1,78 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { CITE_ULR } from '@/services/constants' + +export const getCurrentPath = (doiIdentifier: string) => { + return `${CITE_ULR}/${doiIdentifier}/data` +} + +export const jsonToFile = (jsonData: object, fileName: string): File => { + const jsonString = JSON.stringify(jsonData, null, 2) + const blob = new Blob([jsonString], { type: 'application/json' }) + return new File([blob], fileName, { type: 'application/json' }) +} diff --git a/rafts/frontend/src/services/vospaceTransfer.ts b/rafts/frontend/src/services/vospaceTransfer.ts new file mode 100644 index 0000000..066a93a --- /dev/null +++ b/rafts/frontend/src/services/vospaceTransfer.ts @@ -0,0 +1,403 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +/** + * VOSpace Transfer Utilities + * + * Shared server-side utilities for uploading files to VOSpace using + * the VOSpace Transfer API (UWS job pattern). + * + * This module is server-side only - do not import from client components. + */ + +import { VAULT_SYNCTRANS_ENDPOINT, VOSPACE_AUTHORITY } from '@/services/constants' + +// VOSpace nodes endpoint for creating/managing nodes +const VAULT_NODES_ENDPOINT = + process.env.NEXT_VAULT_NODES_ENDPOINT || 'https://ws-cadc.canfar.net/vault/nodes' + +// ============================================================================ +// XML Builders +// ============================================================================ + +/** + * Build a VOSpace transfer XML document for pushing a file + */ +export function buildTransferXml(vosPath: string): string { + const vosUri = `vos://${VOSPACE_AUTHORITY}/${vosPath}` + return ` + + ${vosUri} + pushToVoSpace + +` +} + +/** + * Build VOSpace DataNode XML for creating a file node + */ +export function buildDataNodeXml(vosPath: string): string { + const vosUri = `vos://${VOSPACE_AUTHORITY}/${vosPath}` + return ` + +` +} + +/** + * Build ContainerNode XML for creating a folder + */ +export function buildContainerNodeXml(vosPath: string): string { + const vosUri = `vos://${VOSPACE_AUTHORITY}/${vosPath}` + return ` + +` +} + +// ============================================================================ +// XML Parsers +// ============================================================================ + +/** + * Parse UWS job XML to extract phase and transferDetails URL + */ +export function parseUwsJobXml(xml: string): { + phase: string + transferDetailsUrl: string | null + error: string | null +} { + const phaseMatch = xml.match(/([^<]+)<\/uws:phase>/) + const phase = phaseMatch ? phaseMatch[1] : 'UNKNOWN' + + const transferDetailsMatch = xml.match(/id="transferDetails"[^>]*xlink:href="([^"]+)"/) + const transferDetailsUrl = transferDetailsMatch ? transferDetailsMatch[1] : null + + const errorMatch = xml.match(/([^<]+)<\/uws:message>/) + const error = errorMatch ? errorMatch[1] : null + + return { phase, transferDetailsUrl, error } +} + +/** + * Parse VOSpace transfer XML to extract the actual upload endpoint + */ +export function parseTransferDetailsXml(xml: string): string | null { + const endpointMatch = xml.match(/([^<]+)<\/vos:endpoint>/) + return endpointMatch ? endpointMatch[1] : null +} + +// ============================================================================ +// Node Management +// ============================================================================ + +/** + * Ensure a ContainerNode (folder) exists in VOSpace + */ +export async function ensureContainerNodeExists( + vosPath: string, + accessToken: string, +): Promise<{ exists: boolean; created: boolean }> { + const nodeUrl = `${VAULT_NODES_ENDPOINT}/${vosPath}` + const authHeaders = { Cookie: `CADC_SSO=${accessToken}` } + + // Check if node exists + const getResponse = await fetch(nodeUrl, { + method: 'GET', + headers: { ...authHeaders, Accept: 'text/xml' }, + }) + + if (getResponse.ok) { + return { exists: true, created: false } + } + + // Create the container node + const nodeXml = buildContainerNodeXml(vosPath) + const createResponse = await fetch(nodeUrl, { + method: 'PUT', + headers: { ...authHeaders, 'Content-Type': 'text/xml' }, + body: nodeXml, + }) + + if (createResponse.ok || createResponse.status === 201) { + return { exists: true, created: true } + } + + if (createResponse.status === 409) { + // Conflict - already exists + return { exists: true, created: false } + } + + return { exists: false, created: false } +} + +/** + * Ensure a DataNode (file placeholder) exists in VOSpace + */ +export async function ensureDataNodeExists( + vosPath: string, + accessToken: string, +): Promise<{ exists: boolean; created: boolean }> { + const nodeUrl = `${VAULT_NODES_ENDPOINT}/${vosPath}` + const authHeaders = { Cookie: `CADC_SSO=${accessToken}` } + + // Check if node exists + const getResponse = await fetch(nodeUrl, { + method: 'GET', + headers: { ...authHeaders, Accept: 'text/xml' }, + }) + + if (getResponse.ok) { + return { exists: true, created: false } + } + + // Create the data node + const nodeXml = buildDataNodeXml(vosPath) + const createResponse = await fetch(nodeUrl, { + method: 'PUT', + headers: { ...authHeaders, 'Content-Type': 'text/xml' }, + body: nodeXml, + }) + + if (createResponse.ok || createResponse.status === 201) { + return { exists: true, created: true } + } + + if (createResponse.status === 409) { + return { exists: true, created: false } + } + + return { exists: false, created: false } +} + +// ============================================================================ +// Transfer Negotiation +// ============================================================================ + +/** + * Negotiate a VOSpace transfer to get the actual upload endpoint + * Uses UWS (Universal Worker Service) job pattern + */ +export async function negotiateTransfer(vosPath: string, accessToken: string): Promise { + const transferXml = buildTransferXml(vosPath) + const authHeaders = { Cookie: `CADC_SSO=${accessToken}` } + + // Step 1: POST transfer request to synctrans + const synctransResponse = await fetch(VAULT_SYNCTRANS_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'text/xml', ...authHeaders }, + body: transferXml, + redirect: 'manual', + }) + + if (synctransResponse.status !== 303) { + const errorText = await synctransResponse.text().catch(() => '') + throw new Error(`Expected 303 redirect, got ${synctransResponse.status}: ${errorText}`) + } + + const locationUrl = synctransResponse.headers.get('Location') + if (!locationUrl) { + throw new Error('No Location header in synctrans response') + } + + // Step 2: GET the job XML + const jobUrl = locationUrl.replace('/results/transferDetails', '') + + const jobResponse = await fetch(jobUrl, { + method: 'GET', + headers: { ...authHeaders, Accept: 'text/xml' }, + }) + + if (!jobResponse.ok) { + const errorText = await jobResponse.text().catch(() => '') + throw new Error(`Failed to get transfer job: ${jobResponse.status}: ${errorText}`) + } + + const jobXml = await jobResponse.text() + const { phase, transferDetailsUrl, error } = parseUwsJobXml(jobXml) + + if (phase === 'ERROR') { + throw new Error(`Transfer job failed: ${error || 'Unknown error'}`) + } + + if (!transferDetailsUrl) { + throw new Error('No transferDetails URL in job response') + } + + // Step 3: GET the transfer details + const transferDetailsResponse = await fetch(transferDetailsUrl, { + method: 'GET', + headers: { ...authHeaders, Accept: 'text/xml' }, + }) + + if (!transferDetailsResponse.ok) { + const errorText = await transferDetailsResponse.text().catch(() => '') + throw new Error( + `Failed to get transfer details: ${transferDetailsResponse.status}: ${errorText}`, + ) + } + + const transferDetailsXml = await transferDetailsResponse.text() + const uploadEndpoint = parseTransferDetailsXml(transferDetailsXml) + + if (!uploadEndpoint) { + throw new Error('No endpoint URL in transfer details') + } + + return uploadEndpoint +} + +// ============================================================================ +// High-Level Upload Functions +// ============================================================================ + +export interface UploadResult { + success: boolean + error?: string +} + +/** + * Upload content to VOSpace + * + * @param vosPath - Full VOSpace path (e.g., "rafts-test/RAFTS-xxx/data/file.txt") + * @param content - Content to upload (string or Buffer) + * @param mimeType - MIME type of the content + * @param accessToken - CADC SSO access token + */ +export async function uploadToVOSpace( + vosPath: string, + content: string | Buffer, + mimeType: string, + accessToken: string, +): Promise { + try { + // Step 1: Ensure DataNode exists + await ensureDataNodeExists(vosPath, accessToken) + + // Step 2: Negotiate transfer + const uploadEndpoint = await negotiateTransfer(vosPath, accessToken) + + // Step 3: PUT the content + const response = await fetch(uploadEndpoint, { + method: 'PUT', + headers: { + Cookie: `CADC_SSO=${accessToken}`, + 'Content-Type': mimeType, + }, + body: content, + }) + + if (!response.ok) { + const errorText = await response.text().catch(() => '') + return { success: false, error: `Upload failed: ${response.status} ${errorText}` } + } + + return { success: true } + } catch (error) { + console.error('[uploadToVOSpace] Exception:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } +} + +/** + * Upload content to a folder in VOSpace (ensures folder exists first) + * + * @param folderPath - VOSpace folder path (e.g., "rafts-test/RAFTS-xxx/data/attachments") + * @param filename - Filename to upload as + * @param content - Content to upload + * @param mimeType - MIME type + * @param accessToken - CADC SSO access token + */ +export async function uploadToVOSpaceFolder( + folderPath: string, + filename: string, + content: string | Buffer, + mimeType: string, + accessToken: string, +): Promise { + try { + // Ensure the folder exists + await ensureContainerNodeExists(folderPath, accessToken) + + // Upload the file + const filePath = `${folderPath}/${filename}` + return await uploadToVOSpace(filePath, content, mimeType, accessToken) + } catch (error) { + console.error('[uploadToVOSpaceFolder] Exception:', error) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + } + } +} diff --git a/rafts/frontend/src/shared/backendStatus.ts b/rafts/frontend/src/shared/backendStatus.ts new file mode 100644 index 0000000..3bf9aa5 --- /dev/null +++ b/rafts/frontend/src/shared/backendStatus.ts @@ -0,0 +1,109 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// Backend status values from DOI service +// These are the actual status values used by the DOI backend API + +export const BACKEND_STATUS = { + IN_PROGRESS: 'in progress', + REVIEW_READY: 'review ready', + IN_REVIEW: 'in review', + APPROVED: 'approved', + REJECTED: 'rejected', + MINTED: 'minted', +} as const + +export type BackendStatusType = (typeof BACKEND_STATUS)[keyof typeof BACKEND_STATUS] + +// Maps backend status values to UI display names +// Matches the raft_table translations in messages/en.json +const STATUS_DISPLAY_NAMES: Record = { + 'in progress': 'Draft', + 'review ready': 'Review Ready', + 'in review': 'In Review', + approved: 'Approved', + rejected: 'Rejected', + minted: 'Published', +} + +export const getStatusDisplayName = (status?: string): string => { + if (!status) return 'Unknown' + return STATUS_DISPLAY_NAMES[status.toLowerCase()] || status +} + +/** + * Status workflow: + * + * in progress (Draft) → author submits → review ready + * review ready → publisher claims → in review (+ assigns reviewer) + * review ready → author cancels → in progress + * in review → publisher approves → approved + * in review → publisher rejects → rejected + * in review → request revision → in progress + * approved → in progress (revision) + * rejected → in progress (revision) + */ diff --git a/rafts/frontend/src/shared/constants.ts b/rafts/frontend/src/shared/constants.ts new file mode 100644 index 0000000..3772db8 --- /dev/null +++ b/rafts/frontend/src/shared/constants.ts @@ -0,0 +1,207 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// Properties +export const PROP_TITLE = 'title' +export const PROP_CORRESPONDING_AUTHOR = 'correspondingAuthor' +export const PROP_CONTRIBUTING_AUTHORS = 'contributingAuthors' +export const PROP_COLLABORATIONS = 'collaborations' +export const PROP_GENERAL_INFO = 'generalInfo' +export const PROP_AUTHOR_INFO = 'authorInfo' +export const PROP_OBSERVATION_INFO = 'observationInfo' +export const PROP_TOPIC = 'topic' +export const PROP_OBJECT_NAME = 'objectName' +export const PROP_ABSTRACT = 'abstract' +export const PROP_ACKNOWLEDGEMENTS = 'acknowledgements' +export const PROP_PREVIOUS_RAFTS = 'relatedPublishedRafts' +export const PROP_FIGURE = 'figure' + +// Author Properties +export const PROP_AUTHOR_FIRST_NAME = 'firstName' +export const PROP_AUTHOR_LAST_NAME = 'lastName' +export const PROP_AUTHOR_ORCID = 'authorORCID' +export const PROP_AUTHOR_AFFILIATION = 'affiliation' +export const PROP_AUTHOR_EMAIL = 'email' + +// Machine Readable Table Properties +export const PROP_TECHNICAL_INFO = 'technical' +export const PROP_MPC_ID = 'mpcId' +export const PROP_ALERT_ID = 'alertId' +export const PROP_INSTRUMENT = 'instrument' +export const PROP_MJD = 'mjd' +export const PROP_TIME_OBSERVED = 'timeObserved' +export const PROP_TELESCOPE = 'telescope' + +// Measurement Properties +export const PROP_MEASUREMENT_INFO = 'measurementInfo' +export const PROP_POSITION = 'position' +export const PROP_FLUX = 'flux' +export const PROP_PHOTOMETRY = 'photometry' +export const PROP_WAVELENGTH = 'wavelength' +export const PROP_BRIGHTNESS = 'brightness' +export const PROP_ERRORS = 'errors' + +// Optional Properties +export const PROP_EPHEMERIS = 'ephemeris' +export const PROP_ORBITAL_ELEMENTS = 'orbitalElements' +export const PROP_SPECTROSCOPY = 'spectroscopy' +export const PROP_ASTROMETRY = 'astrometry' +export const PROP_MISC_INFO = 'miscInfo' +export const PROP_MISC = 'misc' +export const PROP_MISC_KEY = 'miscKey' +export const PROP_MISC_VALUE = 'miscValue' +export const PROP_MISC_TYPE = 'miscType' + +// Review +export const PROP_STATUS = 'status' +export const PROP_POST_OPT_OUT = 'postOptOut' +export const OPTION_DRAFT = 'draft' +export const OPTION_REVIEW = 'review_ready' +export const OPTION_UNDER_REVIEW = 'under_review' +export const OPTION_APPROVED = 'approved' +export const OPTION_REJECTED = 'rejected' +export const OPTION_PUBLISHED = 'published' +// Backend status values — re-exported from single source of truth +// Use BACKEND_STATUS from '@/shared/backendStatus' for new code +export { BACKEND_STATUS } from './backendStatus' +export const OPTION_IN_PROGRESS = 'in progress' as const +export const OPTION_IN_REVIEW = 'in review' as const +export const OPTION_MINTED = 'minted' as const +export const STATUS_OPTIONS = [ + OPTION_DRAFT, + OPTION_REVIEW, + OPTION_UNDER_REVIEW, + OPTION_APPROVED, + OPTION_REJECTED, + OPTION_PUBLISHED, + OPTION_IN_PROGRESS, + OPTION_IN_REVIEW, + OPTION_MINTED, +] as const +// Topic Options +export const OPTION_COMET = 'comet' +export const OPTION_NEA = 'near_earth_object' +export const OPTION_TNO = 'trans_neptunian_object' +export const OPTION_ASTEROID = 'asteroid' +export const OPTION_PHA = 'potentially_hazardous_asteroid' +export const OPTION_ISO = 'interstellar_object' +export const OPTION_TCEO = 'temporarily_captured_earth_orbiter' +export const OPTION_ACTIVE = 'active_object' +export const OPTION_OUTBURST = 'outburst' +export const OPTION_MULTI_COMPONENT = 'multi_component_system' +export const OPTION_UNUSUAL_ROTATION = 'unusual_rotation_properties' +export const OPTION_UNUSUAL_SPECTRA = 'unusual_colour_spectra' +export const OPTION_NON_DETECTION = 'non_detection' +export const OPTION_NON_GRAVITATIONAL = 'non_gravitational_perturbations' +export const OPTION_ERRATA = 'errata' +export const OPTION_RETRACTION = 'retraction' +export const OPTION_OTHER = 'other' +export const OPTION_TROJANS = 'trojans' +export const OPTION_CENTAURS = 'centaurs' +export const OPTION_SATELLITES = 'satellites' + +export const TOPIC_OPTIONS = [ + OPTION_COMET, + OPTION_NEA, + OPTION_TNO, + OPTION_ASTEROID, + OPTION_PHA, + OPTION_ISO, + OPTION_TCEO, + OPTION_ACTIVE, + OPTION_OUTBURST, + OPTION_MULTI_COMPONENT, + OPTION_UNUSUAL_ROTATION, + OPTION_UNUSUAL_SPECTRA, + OPTION_NON_DETECTION, + OPTION_NON_GRAVITATIONAL, + OPTION_TROJANS, + OPTION_CENTAURS, + OPTION_SATELLITES, + OPTION_ERRATA, + OPTION_RETRACTION, + OPTION_OTHER, +] as const + +export const FORM_SECTIONS = [ + PROP_AUTHOR_INFO, + PROP_OBSERVATION_INFO, + PROP_TECHNICAL_INFO, + PROP_MISC_INFO, +] as const +// Brightness Unit Options +export const OPTION_BRIGHTNESS_AB_MAG = 'AB mag' +export const OPTION_BRIGHTNESS_VEGA_MAG = 'Vega mag' +export const OPTION_BRIGHTNESS_MJY = 'mJy' +export const OPTION_BRIGHTNESS_ERGS = 'ergs/s/cm^2/Å' + +export const ORCID_REGEX = /^(\d{4}-){3}\d{3}[\dX]$/ + +// User +export const ROLE_REVIEWER = 'reviewer' +export const ROLE_CONTRIBUTOR = 'contributor' +export const USER_ROLES = [ROLE_REVIEWER, ROLE_CONTRIBUTOR] as const diff --git a/rafts/frontend/src/shared/index.ts b/rafts/frontend/src/shared/index.ts new file mode 100644 index 0000000..dd6ba5b --- /dev/null +++ b/rafts/frontend/src/shared/index.ts @@ -0,0 +1,69 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export * from './constants' +export * from './model' diff --git a/rafts/frontend/src/shared/model.ts b/rafts/frontend/src/shared/model.ts new file mode 100644 index 0000000..28518e6 --- /dev/null +++ b/rafts/frontend/src/shared/model.ts @@ -0,0 +1,261 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +// model.ts +import { z } from 'zod' +import { + OPTION_DRAFT, + ORCID_REGEX, + PROP_ABSTRACT, + PROP_ACKNOWLEDGEMENTS, + PROP_ALERT_ID, + PROP_ASTROMETRY, + PROP_AUTHOR_AFFILIATION, + PROP_AUTHOR_EMAIL, + PROP_AUTHOR_FIRST_NAME, + PROP_AUTHOR_INFO, + PROP_AUTHOR_LAST_NAME, + PROP_AUTHOR_ORCID, + PROP_BRIGHTNESS, + PROP_CONTRIBUTING_AUTHORS, + PROP_COLLABORATIONS, + PROP_CORRESPONDING_AUTHOR, + PROP_EPHEMERIS, + PROP_ERRORS, + PROP_FIGURE, + PROP_FLUX, + PROP_GENERAL_INFO, + PROP_MEASUREMENT_INFO, + PROP_MISC, + PROP_MISC_INFO, + PROP_MISC_KEY, + PROP_MISC_TYPE, + PROP_MISC_VALUE, + PROP_MJD, + PROP_MPC_ID, + PROP_OBJECT_NAME, + PROP_OBSERVATION_INFO, + PROP_ORBITAL_ELEMENTS, + PROP_PHOTOMETRY, + PROP_POSITION, + PROP_POST_OPT_OUT, + PROP_PREVIOUS_RAFTS, + PROP_SPECTROSCOPY, + PROP_TECHNICAL_INFO, + PROP_TELESCOPE, + PROP_TIME_OBSERVED, + PROP_TITLE, + PROP_TOPIC, + PROP_WAVELENGTH, + PROP_STATUS, + ROLE_CONTRIBUTOR, + STATUS_OPTIONS, + TOPIC_OPTIONS, + USER_ROLES, +} from './constants' + +export const miscInfoSchema = z.object({ + [PROP_MISC]: z + .array( + z.object({ + [PROP_MISC_KEY]: z.string().optional(), + [PROP_MISC_VALUE]: z.string().optional(), + [PROP_MISC_TYPE]: z.enum(['text', 'file']).default('text'), + }), + ) + .optional(), +}) + +const spectroscopySchema = z + .object({ + [PROP_WAVELENGTH]: z.string(), + [PROP_FLUX]: z.string().optional(), + [PROP_ERRORS]: z.string().optional(), + }) + .optional() + +const astrometrySchema = z + .object({ + [PROP_POSITION]: z.string().optional(), + [PROP_TIME_OBSERVED]: z.string().optional(), + }) + .optional() + +const photometrySchema = z + .object({ + [PROP_WAVELENGTH]: z.string().optional(), + [PROP_BRIGHTNESS]: z.string().optional(), + [PROP_ERRORS]: z.string().optional(), + }) + .optional() + +export const measurementInfoSchema = z.object({ + [PROP_PHOTOMETRY]: photometrySchema, + [PROP_SPECTROSCOPY]: spectroscopySchema, + [PROP_ASTROMETRY]: astrometrySchema, +}) + +export const pearsonSchema = z.object({ + [PROP_AUTHOR_FIRST_NAME]: z.string().min(1, { message: 'is_required' }), + [PROP_AUTHOR_LAST_NAME]: z.string().min(1, { message: 'is_required' }), + [PROP_AUTHOR_AFFILIATION]: z.string().min(1, { message: 'is_required' }), + [PROP_AUTHOR_ORCID]: z + .string() + .trim() + .optional() + .refine((val) => !val || ORCID_REGEX.test(val), { message: 'invalid_orcid' }), + [PROP_AUTHOR_EMAIL]: z.string().email({ message: 'valid_email_required' }), +}) + +export const technicalInfoSchema = z + .object({ + [PROP_PHOTOMETRY]: photometrySchema, + [PROP_SPECTROSCOPY]: z.string().optional(), + [PROP_ASTROMETRY]: z.string().optional(), + [PROP_EPHEMERIS]: z.string().optional(), + [PROP_ORBITAL_ELEMENTS]: z.string().optional(), + [PROP_MPC_ID]: z.string().optional(), + [PROP_ALERT_ID]: z.string().optional(), + [PROP_MJD]: z + .string() + .refine((val) => !val || /^\d{5}(\.\d+)?$/.test(val), { message: 'invalid_mjd_format' }), + [PROP_TELESCOPE]: z.string().optional(), + }) + .superRefine((data, ctx) => { + // Check if at least one identifier is provided + if (!data[PROP_MPC_ID] && !data[PROP_ORBITAL_ELEMENTS]) { + // Add error for each field + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'at_least_one_identifier_required', + path: [PROP_MPC_ID], + }) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'at_least_one_identifier_required', + path: [PROP_ORBITAL_ELEMENTS], + }) + } + }) + +export const generalSchema = z.object({ + [PROP_TITLE]: z.string().min(1, { message: 'is_required' }), + [PROP_POST_OPT_OUT]: z.boolean().default(false), + [PROP_STATUS]: z.enum(STATUS_OPTIONS).default(OPTION_DRAFT), +}) + +export const authorSchema = z.object({ + [PROP_CORRESPONDING_AUTHOR]: pearsonSchema, + [PROP_CONTRIBUTING_AUTHORS]: z.array(pearsonSchema).optional(), + [PROP_COLLABORATIONS]: z.array(z.string()).optional(), +}) + +export const observationSchema = z.object({ + [PROP_TOPIC]: z + .array(z.enum(TOPIC_OPTIONS)) + .min(1, { message: 'is_required' }) + .nonempty({ message: 'is_required' }), + [PROP_OBJECT_NAME]: z.string({ message: 'is_required' }), + [PROP_ABSTRACT]: z.string().min(1, { message: 'is_required' }).max(2000, { + message: 'character_limit', + }), + [PROP_FIGURE]: z.string().optional(), + [PROP_ACKNOWLEDGEMENTS]: z.string().optional(), + [PROP_PREVIOUS_RAFTS]: z.string().optional(), +}) + +export const raftSchema = z.object({ + [PROP_GENERAL_INFO]: generalSchema, + [PROP_AUTHOR_INFO]: authorSchema, + [PROP_OBSERVATION_INFO]: observationSchema, + [PROP_TECHNICAL_INFO]: technicalInfoSchema, + [PROP_MEASUREMENT_INFO]: measurementInfoSchema, + [PROP_MISC_INFO]: miscInfoSchema, +}) + +// Export types +export type TRaftSubmission = z.infer +export type TAuthor = z.infer +export type TGeneral = z.infer +export type TPerson = z.infer +export type TObservation = z.infer +export type TTechInfo = z.infer +export type TMeasurementInfo = z.infer +export type TSpectroscopy = z.infer +export type TPhotometry = z.infer +export type TAstrometry = z.infer +export type TMiscInfo = z.infer +export type TMeasurement = TSpectroscopy | TPhotometry | TAstrometry +export type TSection = TGeneral | TAuthor | TObservation | TTechInfo | TMeasurementInfo | TMiscInfo +export type TRaftStatus = (typeof STATUS_OPTIONS)[number] + +// user model +export const userRolesSchema = z.object({ + role: z.enum(USER_ROLES).default(ROLE_CONTRIBUTOR), +}) +export type TUserRole = z.infer +export type TRoles = (typeof USER_ROLES)[number] diff --git a/rafts/frontend/src/styles/ThemeProvider.tsx b/rafts/frontend/src/styles/ThemeProvider.tsx new file mode 100644 index 0000000..70df48c --- /dev/null +++ b/rafts/frontend/src/styles/ThemeProvider.tsx @@ -0,0 +1,98 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' +import { ThemeProvider as MUIThemeProvider } from '@mui/material/styles' +import CssBaseline from '@mui/material/CssBaseline' +import { useAppTheme } from './theme' +import { ReactNode, useEffect, useState } from 'react' + +export default function ThemeProvider({ children }: { children: ReactNode }) { + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + // Get theme only after mounting to ensure correct dark/light mode + const theme = useAppTheme() + + // Don't render MUI components until after hydration to prevent flash + // next-themes sets the class on before hydration via blocking script, + // but MUI needs to wait for client-side JS to determine the correct theme + if (!mounted) { + // Return minimal shell that respects CSS variables set by next-themes + return
    {children}
    + } + + return ( + + + {children} + + ) +} diff --git a/rafts/frontend/src/styles/theme.ts b/rafts/frontend/src/styles/theme.ts new file mode 100644 index 0000000..4c9c2e1 --- /dev/null +++ b/rafts/frontend/src/styles/theme.ts @@ -0,0 +1,504 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +'use client' +import { createTheme, ThemeOptions, alpha } from '@mui/material/styles' +import { useTheme } from 'next-themes' +import { useMemo } from 'react' + +// Function to create the theme options for a specific mode +const getDesignTokens = (mode: 'light' | 'dark'): ThemeOptions => ({ + typography: { + fontFamily: 'var(--font-roboto), "Inter", "Roboto", "Helvetica", "Arial", sans-serif', + h1: { + fontWeight: 300, + fontSize: '6rem', + lineHeight: 1.167, + letterSpacing: '-0.01562em', + }, + h2: { + fontWeight: 300, + fontSize: '3.75rem', + lineHeight: 1.2, + letterSpacing: '-0.00833em', + }, + h3: { + fontWeight: 400, + fontSize: '3rem', + lineHeight: 1.167, + letterSpacing: '0em', + }, + h4: { + fontWeight: 400, + fontSize: '2.125rem', + lineHeight: 1.235, + letterSpacing: '0.00735em', + }, + h5: { + fontWeight: 400, + fontSize: '1.5rem', + lineHeight: 1.334, + letterSpacing: '0em', + }, + h6: { + fontWeight: 500, + fontSize: '1.25rem', + lineHeight: 1.6, + letterSpacing: '0.0075em', + }, + subtitle1: { + fontWeight: 400, + fontSize: '1rem', + lineHeight: 1.75, + letterSpacing: '0.00938em', + }, + subtitle2: { + fontWeight: 500, + fontSize: '0.875rem', + lineHeight: 1.57, + letterSpacing: '0.00714em', + }, + body1: { + fontWeight: 400, + fontSize: '1rem', + lineHeight: 1.5, + letterSpacing: '0.00938em', + }, + body2: { + fontWeight: 400, + fontSize: '0.875rem', + lineHeight: 1.43, + letterSpacing: '0.01071em', + }, + button: { + fontWeight: 400, + fontSize: '0.875rem', + lineHeight: 1.75, + letterSpacing: '0.02857em', + textTransform: 'uppercase', + }, + caption: { + fontWeight: 400, + fontSize: '0.75rem', + lineHeight: 1.66, + letterSpacing: '0.03333em', + }, + overline: { + fontWeight: 400, + fontSize: '0.75rem', + lineHeight: 2.66, + letterSpacing: '0.08333em', + textTransform: 'uppercase', + }, + }, + palette: { + mode, + ...(mode === 'light' + ? { + // + // ------------------------------------------- + // LIGHT MODE PALETTE + // ------------------------------------------- + // + primary: { + main: '#00796B', // A deep, elegant teal + light: '#48a999', + dark: '#004c40', + contrastText: '#ffffff', + }, + secondary: { + main: '#C2185B', // A vibrant, engaging magenta + light: '#f6598c', + dark: '#8c0032', + contrastText: '#ffffff', + }, + error: { + main: '#D32F2F', // Standard Material Red + light: '#EF5350', + dark: '#C62828', + contrastText: '#ffffff', + }, + warning: { + main: '#F57C00', // A rich, noticeable amber + light: '#FFA726', + dark: '#E65100', + contrastText: '#000000', // Black text for high contrast + }, + info: { + main: '#0288D1', // A clear, friendly blue + light: '#29B6F6', + dark: '#01579B', + contrastText: '#ffffff', + }, + success: { + main: '#2E7D32', // A reassuring, deep green + light: '#4CAF50', + dark: '#1B5E20', + contrastText: '#ffffff', + }, + background: { + default: '#F7F9FC', // A very light, clean grey + paper: '#ffffff', // Pure white for cards and surfaces + }, + text: { + primary: '#121212', + secondary: '#5f6368', + disabled: '#9e9e9e', + }, + divider: 'rgba(0, 0, 0, 0.12)', + action: { + active: 'rgba(0, 0, 0, 0.54)', + hover: 'rgba(0, 0, 0, 0.06)', // "Dimming" effect - slightly darker overlay + selected: 'rgba(0, 0, 0, 0.08)', + disabled: 'rgba(0, 0, 0, 0.26)', + disabledBackground: 'rgba(0, 0, 0, 0.12)', + }, + } + : { + // + // ------------------------------------------- + // DARK MODE PALETTE (REVISED FOR ACCESSIBILITY & AESTHETICS) + // ------------------------------------------- + // + primary: { + main: '#26A69A', // A vibrant teal that passes contrast checks with black text + light: '#64d8cb', + dark: '#00766c', + contrastText: '#000000', + }, + secondary: { + main: '#C2185B', // Using the richer light-mode main for better contrast with white text + light: '#F06292', + dark: '#8c0032', + contrastText: '#ffffff', + }, + error: { + main: '#D32F2F', // Using the richer light-mode main for better contrast with white text + light: '#EF5350', + dark: '#C62828', + contrastText: '#ffffff', + }, + warning: { + main: '#FF9800', // This orange has great contrast with black and works well + light: '#FFB74D', + dark: '#F57C00', + contrastText: '#000000', + }, + info: { + main: '#0288D1', // Using the richer light-mode main for better contrast with white text + light: '#29B6F6', + dark: '#01579B', + contrastText: '#ffffff', + }, + success: { + main: '#2E7D32', // Using the richer light-mode main for better contrast with white text + light: '#4CAF50', + dark: '#1B5E20', + contrastText: '#ffffff', + }, + background: { + default: '#121212', // MUI standard dark background + paper: '#1E1E1E', // A slightly lighter surface for elevation + }, + text: { + primary: '#E0E0E0', // Off-white for comfortable reading + secondary: '#A4A4A4', + disabled: '#616161', + }, + divider: 'rgba(255, 255, 255, 0.12)', + action: { + active: '#ffffff', + hover: 'rgba(255, 255, 255, 0.1)', // "Glowing" effect - slightly brighter overlay + selected: 'rgba(255, 255, 255, 0.16)', + disabled: 'rgba(255, 255, 255, 0.3)', + disabledBackground: 'rgba(255, 255, 255, 0.12)', + }, + }), + }, + components: { + // --- Global Styles --- + MuiCssBaseline: { + styleOverrides: (theme) => ` + body { + background-color: ${theme.palette.background.default}; + color: ${theme.palette.text.primary}; + // Improve text rendering + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + /* Ensure links are clearly visible */ + a { + color: ${mode === 'dark' ? theme.palette.primary.light : theme.palette.primary.main}; + text-decoration: underline; + } + a:hover { + color: ${mode === 'dark' ? alpha(theme.palette.primary.light, 0.8) : theme.palette.primary.dark}; + } + `, + }, + // --- Form Input Styling (Outlined is common) --- + MuiOutlinedInput: { + styleOverrides: { + root: ({ theme, ownerState }) => ({ + // Use paper background for contrast against page background + backgroundColor: mode === 'dark' ? '#101223' : theme.palette.background.default, + // Ensure text color is correct + color: theme.palette.text.primary, + ...(mode === 'dark' && { + // Dark mode specific border/focus styles + '& .MuiOutlinedInput-notchedOutline': { + // Use divider color for a subtle but visible border + borderColor: theme.palette.divider, // More consistent than arbitrary rgba + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + // Slightly lighter border on hover + borderColor: alpha(theme.palette.text.primary, 0.4), + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + // Use primary color for focus, make it thicker + borderColor: theme.palette.primary.main, + borderWidth: 2, + }, + // Error state + ...(ownerState.error && { + '& .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.error.main, + }, + '&:hover .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.error.light, // Lighter red on hover + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.error.main, // Keep red when focused + error + }, + }), + // Ensure input text color is primary + '& input': { + color: theme.palette.text.primary, + }, + // Style placeholder text + '& ::placeholder': { + color: theme.palette.text.secondary, + opacity: 0.8, // Don't make placeholder too faint + }, + }), + }), + }, + }, + // --- Label Styling --- + MuiInputLabel: { + styleOverrides: { + root: ({ theme, ownerState }) => ({ + // Default label color + color: theme.palette.text.secondary, + ...(mode === 'dark' && { + // Slightly brighter secondary text color is good for labels + color: theme.palette.text.secondary, // Uses the refined #b0b0b0 + // Focused state + '&.Mui-focused': { + // Don't override error color if focused AND error + color: ownerState.error ? theme.palette.error.main : theme.palette.primary.main, + }, + // Error state + ...(ownerState.error && { + color: theme.palette.error.main, + }), + }), + }), + }, + }, + // --- Helper Text Styling --- + MuiFormHelperText: { + styleOverrides: { + root: ({ theme, ownerState }) => ({ + // Default helper text color (often same as secondary) + color: theme.palette.text.secondary, + ...(mode === 'dark' && { + // Slightly lighter than default secondary might be good here + color: alpha(theme.palette.text.secondary, 0.9), // Subtly different if needed, or keep same as label + // Error state + ...(ownerState.error && { + color: theme.palette.error.main, + }), + }), + }), + }, + }, + // --- Button Styling --- + MuiButton: { + styleOverrides: { + root: () => ({ + fontSize: '1rem', // Ensure buttons aren't too small + textTransform: 'none', // Often preferred over ALL CAPS + fontWeight: 600, // Make button text bold for clarity + // Add specific dark mode styles if needed beyond palette colors + ...(mode === 'dark' && + { + // Example: Ensure outlined buttons have sufficient contrast + // if (ownerState.variant === 'outlined') { + // borderColor: alpha(theme.palette.text.primary, 0.5), + // } + }), + }), + // Ensure contained buttons have good contrast and maybe a subtle shadow + contained: () => ({ + ...(mode === 'dark' && { + boxShadow: '0 1px 3px rgba(0,0,0,0.4)', // Subtle shadow can help lift elements + }), + }), + }, + // Define default props if desired + // defaultProps: { + // disableElevation: true, // Example: Flat buttons by default + // } + }, + // --- Fieldset/FormControl Spacing --- + MuiFormControl: { + styleOverrides: { + root: { + // Add consistent spacing below form elements + marginBottom: '1.25rem', // ~20px + }, + }, + }, + // --- Legend specific styling (often used with Fieldset) --- + MuiFormLabel: { + // Targets when used in FormControl/Fieldset context + styleOverrides: { + root: ({ theme }) => ({ + ...(mode === 'dark' && { + color: theme.palette.text.primary, // Make legends prominent like titles + fontWeight: 600, + marginBottom: '0.5rem', // Space below legend inside fieldset + fontSize: '1.1rem', + }), + }), + }, + }, + // --- Paper/Card styling --- + MuiPaper: { + styleOverrides: { + root: ({ theme }) => ({ + // Use paper background color + backgroundColor: theme.palette.background.paper, + // Remove MUI's default subtle background image in dark mode if present + backgroundImage: mode === 'dark' ? 'none' : undefined, + // Add a subtle border to Paper elements in dark mode to help define edges + ...(mode === 'dark' && { + border: `1px solid ${theme.palette.divider}`, + }), + }), + }, + // Optionally default elevation + // defaultProps: { + // elevation: 2, // Slight elevation + // } + }, + // --- Optional: App Bar --- + MuiAppBar: { + styleOverrides: { + root: ({ theme }) => ({ + ...(mode === 'dark' && { + // Use paper background for AppBar, remove default gradient/image + backgroundColor: theme.palette.background.paper, + backgroundImage: 'none', + boxShadow: `0 1px 2px ${alpha(theme.palette.common.black, 0.5)}`, // Subtle bottom shadow + }), + }), + }, + // defaultProps: { + // elevation: 0, // Control shadow via styleOverrides if needed + // } + }, + }, +}) + +// Static theme for server-side rendering (usually defaults to light) +export const staticTheme = createTheme(getDesignTokens('light')) + +// Helper to detect initial theme from DOM (next-themes sets class before hydration) +function getInitialTheme(): 'light' | 'dark' { + if (typeof window !== 'undefined') { + // next-themes sets the class on before React hydration + return document.documentElement.classList.contains('dark') ? 'dark' : 'light' + } + return 'light' // Server-side fallback +} + +// Theme hook for client-side rendering +export function useAppTheme() { + // Use 'system' if you want next-themes to handle OS preference initially + const { resolvedTheme } = useTheme() + // Create theme based on the current resolved theme ('light' or 'dark') + return useMemo(() => { + // Use resolvedTheme if available, otherwise detect from DOM class + const currentMode = (resolvedTheme as 'light' | 'dark') || getInitialTheme() + return createTheme(getDesignTokens(currentMode)) + }, [resolvedTheme]) +} + +// Export the hook as default or alongside staticTheme as needed +export default useAppTheme // Or export both: export { useAppTheme, staticTheme } diff --git a/rafts/frontend/src/tests/README-mock-data.md b/rafts/frontend/src/tests/README-mock-data.md new file mode 100644 index 0000000..140f8ee --- /dev/null +++ b/rafts/frontend/src/tests/README-mock-data.md @@ -0,0 +1,55 @@ +# Mock Review Data for RAFTS + +This directory contains mock data for testing the RAFTS review functionality. + +## Files + +- `raft-2025-07-23.json` - Sample RAFT submission structure +- `mock-review-data.json` - Multiple RAFT submissions in different review states + +## Review States + +The mock data includes RAFT submissions in the following states: + +1. **review_ready** - Ready for Review (2 submissions) + - Discovery of Asteroid 2025 XY1 + - Discovery of a Potential Supernova in M81 + +2. **under_review** - Under Review (1 submission) + - Spectroscopic Analysis of Comet C/2025 Q2 + +3. **approved** - Approved (1 submission) + - Optical Variability in the Active Galaxy NGC 4151 (includes DOI) + +4. **rejected** - Rejected (1 submission) + - Possible Detection of Exoplanet Transit (rejected due to insufficient data) + +## Usage + +To use this mock data: + +1. Enable the review feature by setting `UI_REVIEW_ENABLED=true` in your `.env.local` file +2. Modify the `getReviewReadyRafts()` action to return this mock data during development +3. The review page will display these submissions filtered by status + +## Data Structure + +Each RAFT submission includes: + +- Basic metadata (\_id, createdBy, timestamps) +- General information (title, status) +- Author information (corresponding author, contributing authors, collaborations) +- Observation details (topic, object name, abstract) +- Technical information (telescope, timing, identifiers) +- Measurement data (photometry, spectroscopy, astrometry) +- Miscellaneous information + +## Testing Scenarios + +This mock data allows testing of: + +- Status filtering functionality +- Review workflow transitions +- Display of different observation types (asteroid, comet, AGN, supernova, exoplanet) +- Handling of approved submissions with DOIs +- Display of rejected submissions with reasons diff --git a/rafts/frontend/src/tests/mock-data-loader.ts b/rafts/frontend/src/tests/mock-data-loader.ts new file mode 100644 index 0000000..4fbb330 --- /dev/null +++ b/rafts/frontend/src/tests/mock-data-loader.ts @@ -0,0 +1,178 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { RaftData } from '@/types/doi' +import { TRaftStatus } from '@/shared/model' +import mockReviewData from './mock-review-data.json' +import detailedRaftData from './raft-2025-07-23.json' + +// In-memory storage for mock data modifications +let mockDataStore: RaftData[] | null = null + +/** + * Initialize or get the mock data store + */ +function getMockDataStore(): RaftData[] { + if (!mockDataStore) { + mockDataStore = JSON.parse(JSON.stringify(mockReviewData.rafts)) as RaftData[] + } + return mockDataStore +} + +/** + * Load mock RAFT data for development/testing + * @param status - Filter by status (optional) + * @returns Array of mock RAFT data + */ +export function loadMockRaftData(status?: string): RaftData[] { + const allRafts = getMockDataStore() + + if (!status) { + return allRafts + } + + return allRafts.filter((raft) => raft.generalInfo.status === status) +} + +/** + * Get count of RAFTs by status + * @returns Object with status counts + */ +export function getMockRaftCounts(): Record { + const allRafts = getMockDataStore() + const counts: Record = {} + + allRafts.forEach((raft) => { + const status = raft.generalInfo.status + counts[status] = (counts[status] || 0) + 1 + }) + + return counts +} + +/** + * Get a single mock RAFT by ID + * @param id - The RAFT ID + * @returns Single RAFT data or null + */ +export function getMockRaftById(id: string): RaftData | null { + // Always return the detailed RAFT data for any valid ID from the mock review data + const allRafts = getMockDataStore() + const raftExists = allRafts.find((raft) => raft._id === id || raft.id === id) + + if (raftExists) { + // Return the detailed RAFT data but preserve the ID and status from the original + const detailedRaft = detailedRaftData as RaftData + return { + ...detailedRaft, + _id: raftExists._id, + id: raftExists.id, + generalInfo: { + ...detailedRaft.generalInfo, + status: raftExists.generalInfo.status, + title: raftExists.generalInfo.title, // Keep the original title for consistency + }, + createdAt: raftExists.createdAt, + updatedAt: raftExists.updatedAt, + } + } + + return null +} + +/** + * Update the status of a mock RAFT + * @param id - The RAFT ID + * @param newStatus - The new status to set + * @returns Success status and updated RAFT data + */ +export function updateMockRaftStatus( + id: string, + newStatus: string, +): { success: boolean; data?: RaftData; error?: string } { + const allRafts = getMockDataStore() + const raftIndex = allRafts.findIndex((raft) => raft._id === id || raft.id === id) + + if (raftIndex === -1) { + return { success: false, error: 'RAFT not found' } + } + + // Update the status + allRafts[raftIndex].generalInfo.status = newStatus as TRaftStatus + allRafts[raftIndex].updatedAt = new Date().toISOString() + + // Return the detailed RAFT with updated status + const updatedRaft = getMockRaftById(id) + return { success: true, data: updatedRaft || allRafts[raftIndex] } +} + +/** + * Reset mock data to original state + */ +export function resetMockData(): void { + mockDataStore = null +} diff --git a/rafts/frontend/src/tests/mock-review-data.json b/rafts/frontend/src/tests/mock-review-data.json new file mode 100644 index 0000000..1ea69aa --- /dev/null +++ b/rafts/frontend/src/tests/mock-review-data.json @@ -0,0 +1,615 @@ +{ + "rafts": [ + { + "_id": "mock-raft-review-1", + "id": "mock-raft-review-1", + "createdBy": "john.doe@example.com", + "createdAt": "2025-07-20T10:00:00Z", + "updatedAt": "2025-07-20T10:00:00Z", + "relatedRafts": [], + "generateForumPost": true, + "generalInfo": { + "title": "Discovery of Asteroid 2025 XY1 - Ready for Review", + "postOptOut": false, + "status": "review_ready" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "John", + "lastName": "Doe", + "affiliation": "Space Research Institute", + "authorORCID": "0000-0001-2345-6789", + "email": "john.doe@space-research.org" + }, + "contributingAuthors": [ + { + "firstName": "Jane", + "lastName": "Smith", + "affiliation": "National Observatory", + "authorORCID": "0000-0002-3456-7890", + "email": "jane.smith@natobs.org" + } + ], + "collaborations": ["Asteroid Watch Group"] + }, + "observationInfo": { + "topic": ["asteroid"], + "objectName": "2025 XY1", + "abstract": "We report the discovery of a new main-belt asteroid designated 2025 XY1. The object was detected during a routine sky survey on July 19, 2025. Initial orbital analysis suggests a semi-major axis of 2.8 AU with an eccentricity of 0.15. Photometric observations indicate an absolute magnitude of 18.2, suggesting a diameter of approximately 1.5 km.", + "figure": null, + "previousRafts": [], + "alertId": null + }, + "technicalInfo": { + "ephemeris": "JPL Small-Body Database", + "orbitalElements": "a = 2.8 AU, e = 0.15, i = 5.2°", + "telescope": "2.5m Automated Sky Survey Telescope", + "mjd": "60502.345", + "mpcId": "2025 XY1" + }, + "measurementInfo": { + "photometry": { + "wavelength": "V-band (550nm)", + "brightness": "18.2 ± 0.1 mag", + "errors": "0.1 mag" + }, + "astrometry": { + "position": "RA: 12h 34m 56.78s, Dec: +23° 45' 67.89\"", + "timeObserved": "2025-07-19T23:45:00Z" + }, + "spectroscopy": null + }, + "miscInfo": { + "misc": [ + { + "miscKey": "Discovery Method", + "miscValue": "Automated detection algorithm" + }, + { + "miscKey": "Follow-up Observations", + "miscValue": "Scheduled for next week" + } + ] + } + }, + { + "_id": "mock-raft-review-2", + "id": "mock-raft-review-2", + "createdBy": "alice.johnson@example.com", + "createdAt": "2025-07-18T14:30:00Z", + "updatedAt": "2025-07-21T09:15:00Z", + "relatedRafts": ["mock-raft-review-1"], + "generateForumPost": true, + "generalInfo": { + "title": "Spectroscopic Analysis of Comet C/2025 Q2 - Under Review", + "postOptOut": false, + "status": "under_review" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "Alice", + "lastName": "Johnson", + "affiliation": "Planetary Science Lab", + "authorORCID": "0000-0003-4567-8901", + "email": "alice.johnson@planetsci.edu" + }, + "contributingAuthors": [ + { + "firstName": "Bob", + "lastName": "Williams", + "affiliation": "University Observatory", + "authorORCID": "", + "email": "bob.williams@uniobs.edu" + }, + { + "firstName": "Carol", + "lastName": "Davis", + "affiliation": "Institute of Astronomy", + "authorORCID": "0000-0004-5678-9012", + "email": "carol.davis@astro.org" + } + ], + "collaborations": ["Comet Research Network", "SPECTRA Collaboration"] + }, + "observationInfo": { + "topic": ["comet"], + "objectName": "C/2025 Q2", + "abstract": "We present high-resolution spectroscopic observations of the recently discovered long-period comet C/2025 Q2. Spectra obtained on July 17-18, 2025 reveal strong emission lines of CN, C2, and C3, indicating active outgassing. The relative abundances suggest a typical composition for an Oort cloud comet. Notable is the detection of several organic molecules in the coma.", + "figure": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "previousRafts": [], + "alertId": "ZTF25aaa" + }, + "technicalInfo": { + "ephemeris": "MPC Ephemeris Service", + "orbitalElements": "q = 1.2 AU, e = 0.98, i = 89.5°", + "telescope": "8.2m Gemini North + GMOS", + "mjd": "60500.678", + "mpcId": "C/2025 Q2" + }, + "measurementInfo": { + "photometry": { + "wavelength": "R-band (650nm)", + "brightness": "12.5 ± 0.05 mag", + "errors": "0.05 mag" + }, + "spectroscopy": { + "wavelength": "3800-7000 Angstroms", + "flux": "Calibrated flux spectra available", + "errors": "~5% flux calibration uncertainty" + }, + "astrometry": null + }, + "miscInfo": { + "misc": [ + { + "miscKey": "Heliocentric Distance", + "miscValue": "1.5 AU" + }, + { + "miscKey": "Gas Production Rate", + "miscValue": "Q(H2O) = 1.2e28 mol/s" + } + ] + } + }, + { + "_id": "mock-raft-approved-1", + "id": "mock-raft-approved-1", + "createdBy": "michael.brown@example.com", + "createdAt": "2025-07-10T08:00:00Z", + "updatedAt": "2025-07-22T16:00:00Z", + "relatedRafts": [], + "generateForumPost": true, + "doi": "10.12345/cadc.raft.2025.001", + "generalInfo": { + "title": "Optical Variability in the Active Galaxy NGC 4151 - Approved", + "postOptOut": false, + "status": "approved" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "Michael", + "lastName": "Brown", + "affiliation": "Galactic Research Center", + "authorORCID": "0000-0005-6789-0123", + "email": "michael.brown@grc.org" + }, + "contributingAuthors": [ + { + "firstName": "Sarah", + "lastName": "Lee", + "affiliation": "High Energy Astrophysics Lab", + "authorORCID": "0000-0006-7890-1234", + "email": "sarah.lee@heal.edu" + } + ], + "collaborations": ["AGN Watch"] + }, + "observationInfo": { + "topic": ["agn", "variability"], + "objectName": "NGC 4151", + "abstract": "We report significant optical variability in the Seyfert 1 galaxy NGC 4151 observed over a period of 30 days. The V-band magnitude varied by 0.8 magnitudes, with the most rapid change occurring over a 48-hour period. This variability is correlated with changes in the broad emission line profiles, suggesting changes in the inner accretion disk structure.", + "figure": null, + "previousRafts": ["2025-001", "2025-002"], + "alertId": null + }, + "technicalInfo": { + "ephemeris": null, + "orbitalElements": null, + "telescope": "4.0m SOAR Telescope + Goodman Spectrograph", + "mjd": "60495.123 - 60525.456", + "mpcId": null + }, + "measurementInfo": { + "photometry": { + "wavelength": "UBVRI bands", + "brightness": "V = 11.2-12.0 mag", + "errors": "0.02 mag" + }, + "spectroscopy": { + "wavelength": "4000-8000 Angstroms", + "flux": "Flux-calibrated spectra", + "errors": "3% relative flux accuracy" + }, + "astrometry": null + }, + "miscInfo": { + "misc": [ + { + "miscKey": "Monitoring Cadence", + "miscValue": "Daily observations" + }, + { + "miscKey": "Redshift", + "miscValue": "z = 0.00332" + } + ] + } + }, + { + "_id": "mock-raft-rejected-1", + "id": "mock-raft-rejected-1", + "createdBy": "david.wilson@example.com", + "createdAt": "2025-07-15T11:00:00Z", + "updatedAt": "2025-07-23T10:00:00Z", + "relatedRafts": [], + "generateForumPost": false, + "generalInfo": { + "title": "Possible Detection of Exoplanet Transit - Rejected (Insufficient Data)", + "postOptOut": false, + "status": "rejected" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "David", + "lastName": "Wilson", + "affiliation": "Amateur Astronomy Association", + "authorORCID": "", + "email": "david.wilson@amateur-astro.org" + }, + "contributingAuthors": [], + "collaborations": [] + }, + "observationInfo": { + "topic": ["exoplanet"], + "objectName": "TYC 1234-567-1", + "abstract": "We report a possible transit detection in the star TYC 1234-567-1. A single dimming event of approximately 1% was observed on July 14, 2025. The duration was consistent with a planetary transit, but no follow-up observations were possible due to weather.", + "figure": null, + "previousRafts": [], + "alertId": null + }, + "technicalInfo": { + "ephemeris": null, + "orbitalElements": null, + "telescope": "0.4m Amateur Telescope + CCD", + "mjd": "60499.789", + "mpcId": null + }, + "measurementInfo": { + "photometry": { + "wavelength": "Clear filter", + "brightness": "10.5 mag (nominal)", + "errors": "0.1 mag" + }, + "spectroscopy": null, + "astrometry": null + }, + "miscInfo": { + "misc": [ + { + "miscKey": "Rejection Reason", + "miscValue": "Single observation insufficient for confirmation" + } + ] + } + }, + { + "_id": "mock-raft-review-3", + "id": "mock-raft-review-3", + "createdBy": "emma.thompson@example.com", + "createdAt": "2025-07-23T07:00:00Z", + "updatedAt": "2025-07-23T07:00:00Z", + "relatedRafts": [], + "generateForumPost": true, + "generalInfo": { + "title": "Discovery of a Potential Supernova in M81 - Ready for Review", + "postOptOut": false, + "status": "review_ready" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "Emma", + "lastName": "Thompson", + "affiliation": "Supernova Search Team", + "authorORCID": "0000-0007-8901-2345", + "email": "emma.thompson@snsearch.org" + }, + "contributingAuthors": [ + { + "firstName": "Frank", + "lastName": "Garcia", + "affiliation": "Transient Astronomy Group", + "authorORCID": "0000-0008-9012-3456", + "email": "frank.garcia@tag.edu" + }, + { + "firstName": "Grace", + "lastName": "Martinez", + "affiliation": "Observatorio Nacional", + "authorORCID": "", + "email": "grace.martinez@obsnac.mx" + } + ], + "collaborations": ["Global Supernova Network"] + }, + "observationInfo": { + "topic": ["supernova", "transient"], + "objectName": "SN 2025abc in M81", + "abstract": "We report the discovery of a bright transient in the galaxy M81, designated SN 2025abc. The object was discovered at magnitude 15.2 on July 22, 2025, and has brightened to magnitude 14.8 within 12 hours. The location is consistent with a spiral arm of M81. Spectroscopic follow-up is urgently needed to confirm the supernova nature and determine its type.", + "figure": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "previousRafts": [], + "alertId": "ATLAS25xyz" + }, + "technicalInfo": { + "ephemeris": null, + "orbitalElements": null, + "telescope": "1.5m Robotic Telescope Network", + "mjd": "60504.291", + "mpcId": null + }, + "measurementInfo": { + "photometry": { + "wavelength": "gri bands", + "brightness": "g=15.0, r=14.8, i=14.9 mag", + "errors": "0.03 mag" + }, + "spectroscopy": null, + "astrometry": { + "position": "RA: 09h 55m 33.17s, Dec: +69° 03' 55.1\"", + "timeObserved": "2025-07-22T19:00:00Z" + } + }, + "miscInfo": { + "misc": [ + { + "miscKey": "Host Galaxy Distance", + "miscValue": "3.63 Mpc" + }, + { + "miscKey": "Offset from Galaxy Center", + "miscValue": "45.2 arcsec West, 23.1 arcsec North" + }, + { + "miscKey": "Rising Rate", + "miscValue": "0.4 mag/day" + } + ] + } + }, + { + "_id": "mock-raft-under-review-2", + "id": "mock-raft-under-review-2", + "createdBy": "robert.chen@example.com", + "createdAt": "2025-07-19T11:30:00Z", + "updatedAt": "2025-07-24T08:00:00Z", + "relatedRafts": [], + "generateForumPost": true, + "generalInfo": { + "title": "Unusual Activity in Binary Star System HD 123456", + "postOptOut": false, + "status": "under_review" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "Robert", + "lastName": "Chen", + "affiliation": "Stellar Observatory", + "authorORCID": "0000-0009-0123-4567", + "email": "robert.chen@stellar.edu" + }, + "contributingAuthors": [], + "collaborations": ["Binary Star Research Group"] + }, + "observationInfo": { + "topic": ["binary_star", "variability"], + "objectName": "HD 123456", + "abstract": "We report unusual periodic variations in the binary star system HD 123456. The system shows unexpected eclipse timing variations suggesting a possible third body.", + "figure": null, + "previousRafts": [], + "alertId": null + }, + "technicalInfo": { + "telescope": "3.5m Apache Point Observatory", + "mjd": "60503.456", + "mpcId": null + }, + "measurementInfo": { + "photometry": { + "wavelength": "V-band", + "brightness": "8.5 ± 0.02 mag", + "errors": "0.02 mag" + } + }, + "miscInfo": { + "misc": [] + } + }, + { + "_id": "mock-raft-approved-2", + "id": "mock-raft-approved-2", + "createdBy": "lisa.martinez@example.com", + "createdAt": "2025-07-05T15:45:00Z", + "updatedAt": "2025-07-20T14:30:00Z", + "relatedRafts": [], + "generateForumPost": true, + "generalInfo": { + "title": "New Trans-Neptunian Object 2025 TN1 Discovery", + "postOptOut": false, + "status": "approved" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "Lisa", + "lastName": "Martinez", + "affiliation": "Outer Solar System Survey", + "authorORCID": "0000-0010-1234-5678", + "email": "lisa.martinez@osss.org" + }, + "contributingAuthors": [], + "collaborations": ["OSSS Team"] + }, + "observationInfo": { + "topic": ["trans_neptunian_object"], + "objectName": "2025 TN1", + "abstract": "Discovery of a new trans-Neptunian object with semi-major axis of 45 AU. Initial orbit determination suggests a stable, circular orbit.", + "figure": null, + "previousRafts": [], + "alertId": null + }, + "technicalInfo": { + "telescope": "Subaru Telescope", + "mjd": "60490.789", + "mpcId": "2025 TN1" + }, + "measurementInfo": { + "photometry": { + "wavelength": "r-band", + "brightness": "22.3 ± 0.1 mag", + "errors": "0.1 mag" + } + }, + "miscInfo": { + "misc": [] + } + }, + { + "_id": "mock-raft-review-4", + "id": "mock-raft-review-4", + "createdBy": "paul.anderson@example.com", + "createdAt": "2025-07-24T10:00:00Z", + "updatedAt": "2025-07-24T10:00:00Z", + "relatedRafts": [], + "generateForumPost": true, + "generalInfo": { + "title": "Outburst Detection in Dwarf Nova SS Cygni", + "postOptOut": false, + "status": "review_ready" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "Paul", + "lastName": "Anderson", + "affiliation": "Variable Star Network", + "authorORCID": "", + "email": "paul.anderson@vsnet.org" + }, + "contributingAuthors": [], + "collaborations": [] + }, + "observationInfo": { + "topic": ["outburst"], + "objectName": "SS Cygni", + "abstract": "SS Cygni entered a new outburst phase on July 23, 2025. The star brightened from magnitude 12.0 to 8.5 within 24 hours.", + "figure": null, + "previousRafts": [], + "alertId": "ASASSN-25xyz" + }, + "technicalInfo": { + "telescope": "0.5m Remote Observatory", + "mjd": "60505.123", + "mpcId": null + }, + "measurementInfo": { + "photometry": { + "wavelength": "Clear filter", + "brightness": "8.5 mag", + "errors": "0.05 mag" + } + }, + "miscInfo": { + "misc": [] + } + }, + { + "_id": "mock-raft-rejected-2", + "id": "mock-raft-rejected-2", + "createdBy": "mark.jones@example.com", + "createdAt": "2025-07-12T09:15:00Z", + "updatedAt": "2025-07-18T11:00:00Z", + "relatedRafts": [], + "generateForumPost": false, + "generalInfo": { + "title": "Possible New Comet Detection - Insufficient Evidence", + "postOptOut": false, + "status": "rejected" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "Mark", + "lastName": "Jones", + "affiliation": "Independent Observer", + "authorORCID": "", + "email": "mark.jones@email.com" + }, + "contributingAuthors": [], + "collaborations": [] + }, + "observationInfo": { + "topic": ["comet"], + "objectName": "JONES 2025a", + "abstract": "Fuzzy object detected near constellation Perseus. Possible new comet based on appearance.", + "figure": null, + "previousRafts": [], + "alertId": null + }, + "technicalInfo": { + "telescope": "0.25m Amateur Telescope", + "mjd": "60496.456", + "mpcId": null + }, + "measurementInfo": { + "photometry": { + "wavelength": "Unfiltered", + "brightness": "15 mag (estimated)", + "errors": "unknown" + } + }, + "miscInfo": { + "misc": [ + { + "miscKey": "Rejection Reason", + "miscValue": "No proper astrometry provided, single observation insufficient" + } + ] + } + }, + { + "_id": "mock-raft-under-review-3", + "id": "mock-raft-under-review-3", + "createdBy": "nina.patel@example.com", + "createdAt": "2025-07-22T14:20:00Z", + "updatedAt": "2025-07-24T09:00:00Z", + "relatedRafts": [], + "generateForumPost": true, + "generalInfo": { + "title": "Potentially Hazardous Asteroid 2025 PH3 Recovery", + "postOptOut": false, + "status": "under_review" + }, + "authorInfo": { + "correspondingAuthor": { + "firstName": "Nina", + "lastName": "Patel", + "affiliation": "Near Earth Object Center", + "authorORCID": "0000-0011-2345-6789", + "email": "nina.patel@neoc.org" + }, + "contributingAuthors": [], + "collaborations": ["NEO Survey Team"] + }, + "observationInfo": { + "topic": ["potentially_hazardous_asteroid"], + "objectName": "2025 PH3", + "abstract": "Recovery observations of potentially hazardous asteroid 2025 PH3. Updated orbit shows closest approach of 0.05 AU in 2027.", + "figure": null, + "previousRafts": ["2025-100"], + "alertId": null + }, + "technicalInfo": { + "telescope": "2.0m Faulkes Telescope North", + "mjd": "60504.678", + "mpcId": "2025 PH3" + }, + "measurementInfo": { + "photometry": { + "wavelength": "R-band", + "brightness": "18.7 ± 0.05 mag", + "errors": "0.05 mag" + } + }, + "miscInfo": { + "misc": [] + } + } + ] +} diff --git a/rafts/frontend/src/tests/mocks/handlers.ts b/rafts/frontend/src/tests/mocks/handlers.ts new file mode 100644 index 0000000..55e10da --- /dev/null +++ b/rafts/frontend/src/tests/mocks/handlers.ts @@ -0,0 +1,125 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { http, HttpResponse } from 'msw' + +export const handlers = [ + // Health check endpoint + http.get('*/api/health', () => { + return HttpResponse.json({ status: 'ok' }) + }), + + // DOI status mock + http.get('*/doi/instances', () => { + return HttpResponse.xml(` + + + + 25.0047 + Test RAFT + draft + + + `) + }), + + // CANFAR auth mock + http.post('*/ac/login', () => { + return new HttpResponse('mock-token', { + headers: { 'Content-Type': 'text/plain' }, + }) + }), + + // CANFAR whoami mock + http.get('*/ac/whoami', () => { + return HttpResponse.json({ + userid: 'testuser', + groups: ['platform-users'], + }) + }), + + // VOSpace file upload mock + http.post('*/vault/synctrans', () => { + return new HttpResponse(null, { + status: 303, + headers: { + Location: 'https://mock-vault/jobs/123/results/transferDetails', + }, + }) + }), + + // VOSpace file list mock + http.get('*/vault/files/*', () => { + return HttpResponse.xml(` + + + + + + `) + }), +] diff --git a/rafts/frontend/src/tests/mocks/server.ts b/rafts/frontend/src/tests/mocks/server.ts new file mode 100644 index 0000000..be4e271 --- /dev/null +++ b/rafts/frontend/src/tests/mocks/server.ts @@ -0,0 +1,72 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { setupServer } from 'msw/node' +import { handlers } from './handlers' + +// Setup MSW server with the defined handlers +export const server = setupServer(...handlers) diff --git a/rafts/frontend/src/tests/raft-2025-07-23.json b/rafts/frontend/src/tests/raft-2025-07-23.json new file mode 100644 index 0000000..88188b2 --- /dev/null +++ b/rafts/frontend/src/tests/raft-2025-07-23.json @@ -0,0 +1,57 @@ +{ + "authorInfo": { + "correspondingAuthor": { + "firstName": "Serhii", + "lastName": "Zautkin", + "affiliation": "NRC", + "authorORCID": "7897-8978-9789-7897", + "email": "szautkin@gmail.com" + }, + "contributingAuthors": [ + { + "firstName": "Anna-Marie", + "lastName": "Yarosh", + "affiliation": "JPL", + "authorORCID": "", + "email": "yaroshna@gmail.com" + } + ], + "collaborations": ["SOLaR1 "] + }, + "generalInfo": { + "title": "Test Object N1", + "postOptOut": false, + "status": "draft" + }, + "observationInfo": { + "topic": ["asteroid"], + "objectName": "2023 RN3", + "abstract": "We report the discovery of a new Near-Earth Object (NEO) using ground-based telescopic observations. The object, provisionally designated 2025 AB1, was detected on July 21, 2025, and subsequently tracked over several nights to determine its orbital parameters. Preliminary analysis indicates that the NEO follows an elliptical orbit with a perihelion distance that brings it within 0.05 astronomical units of Earth’s orbit. Initial size and reflectivity estimates suggest a diameter of approximately 200 meters. Continued monitoring and analysis will refine its trajectory and assess any potential impact risk. This discovery contributes to the ongoing efforts to catalog and characterize NEOs for planetary defense and scientific study.", + "figure": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/wAARCAPAAtADAREAAhEBAxEB/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDwGtxC0CCgAoAKBi0WASgQtACUwFxSAVl29e9DAbQAoHagAP1pgGKACmAopAHemAUASPIXREx93pQAwDNAgIoABQAUAHagBeaAFxQAnNMQcetAC0DEoELQAoGKADHNMAHWgBTwaAACgAIoAcMY45NMQuMGgBe9AC4oAXGKYg70ALj2oAXmgAANMAIoAXA79aAFINAC44yaBC4IpDEHU4piA9aBinoKQB/OgAAOTTAQ8k4oADSAXGKYDSKQCn7oPemITFAwoAMUgE70ABoGKQMZoATGTSAQ0AJQAuKAGnrQAYPXtQMTFIBMc0wEIoAD1oAMUANNAB+NIBCMDFIAoAKAGnrQAuOM55oGJ+NIAIxQAmDQAmMmgBlSMKBhjimIKQBSAlhlERbMavkYw1MCM8k9qACgBKYC8igB8ezd+8JxjjFADPWgDUZNHGhBhJN/ahk5XHyBaAMs+/WgAoAUrimAUhBTQCmgAoGA60CCgAxTANtAC4pASmJfswkDEtuwVoAiAoAMelMQ7Cjr1pjEzuoAAKQgxQAv40wFwPWgAx81MAPJoELigYoHB/xpCNG8vre6sbOGOyWGSFcSSqf9Z7mmBQxyKAAj5qYhenWkA/Ee0ncc9uKoBBx1pAL16UxCc+tAxxFDELjI56UAS7YPsobeROD07YoGR4wOepoAXHyUAHpmgQoU7hjvQMTGWoEKOMjvSAkeICFZA4Yn+GmMixx70ALtyhbPI7UAJwelACEUhARTACOBTGGKTAMfnSEGOeaYwIxyelACEUhguSDxQAp5/h/WgQ3j05pAOVtqkbMk/pTGR8+tAAQMGkAEkjaRQA3GRxQAYxQA0g0DBhkg0AJzmgAwKADGOv50gJI4fPkKh1Q9cuaAImXDEcHB6560gE/GgBO9ACY5oAfG8ao4ZfmPSgZHg0AFIAwKAI6kYUALkYoGJTAKQhaAEpgLmgAoAO9AC9O9ACCgA70AL+FABTAQnOPSgBcUgA00IKACgBRQAnemA4LmkAHjOKoAHIpAGAe+PagAIzQAo9O9ABjmmJBjNIY8Rt5RkzwDjFADPegQvGKYBjpTAcRgfWgBAMUAL0oExetAIcegFAAOD70wDGTmgBw+lAhQPamAACgBaBC4FAwFAhcE+4pgKOewoAUe5oACOmKQxxXuMUxASS2fQUDEPPbmkA+QxMF8tSpx83vQIZgHnFAxCOaBBgA9KBjgATTAaR81IAAoEDUAHbrQMVwBjY2Se1ADMDpigBT2FAC4BwKADHOAeKBjcEfWgBAcZyaBARyaQxAKAEIpCAj5qYxM4JpAIRmgBMYoAG6UAIeKBhQIaeTSACB6UhiEdqYCYoATFIBaAE5osAZxQMTPtQAUARVAx3GPegBMUAFMYHikIc5GRj0oASmAUAHagAFABTEFACikAfjTGFABikBYRLY2bszsJwflXHGKAK5piFFIBDTQC0AHegLC0AIMVQC5ApCDIzQMXuKAD+I0wEFAC0hAeOM8UAKuCeTgUwHkIPutkj1oAZ796YC9etAC0CDnvTAKAHnqKADHNAhRigRJCIPM/f7gmOMetCGN7nB+XPFMBeKADrRYkcRge9A7jlVDnceaYClvlVcYHtQAgANAC4pCACgAxTKF6mgQp9qQCc5pgKRjmgYmc0hAfamgEAyTzQMD1pCFynO7OaAG59utAWA0AGOnHNAxT0460AJ9aADGG6cUABPzE9KQxCOM5oEJx26+lAADnJxQMbigA6UgEJ+emAhHNIBOnegAIHrQA1hzxQMX2HJoEIe4oGIABmkA3uaAF49aAE7GgBBSsAECmAlAAetIBRyvPWgBlAEdQULQAUAFMAoAKQBTAWgAxxSABTAKYgpDCmAYpALTEL1FACde1IYfzpgSRojB9zBSBkUCGZwKADqKAHkp5AAHz5oAbigBKoQ4UDEP0oABwaQC5zQAYpgAPtQAuKQmKRTAMZpgKOKBBQMOe9AhTx2oELTGL1NMQpFAAM0WEO5oAWgYAe9MB2B260CDnNADhzQAd6LgKaBi44GKQC8DqM0xC7T1CigBOB1FACgY7HHrQAnXnFABg4pDFKnbux8v1oATk4IH60wDBJ7e4oEI3J4oAKQBg/hTGJmkIP4hSGIByaoBcccUAJz6cUhAwz26UDDHy0gAetMBuDSYCEZGO9AxXXawU85oENP0xQMD834UgGZ9RQAmMdqAA4FACjqCOKYDT1pAHvSATr2oAbj2pjEzmkAAe1IAxjtTAMe1ACY9qQCHjigAoAj6VBQZoAKADNAwpiCkAueOlMAyaAFyRketACUAFMBaQgxnNAxx2bBj7xoAb3piDvQAdKQDyq7QQ3PcUwGcUABoAXtQA5jnaPSmAnegAzQIO9MYvY96AJJImiVC2PnGRQIZzQAnOaAHDBJBOKAEP14oAXgcUwAfeoAAOaBC8gUgJCi+WHR8k/w0xjAKBDqLkgBVDHc/hQAvWi4gFACmgBwGO1ACgkcY4qgBe+aQB0oAdxikA4/dNMCeeBIYoGSZZTKuSo/hPpQBCRxx+NMA2+9Awxxx1pCHMGCKzdD2oAaRke1AB14pWEKCcYB4plDce9ACYx3piFx+dIYhHNIQEdOMmgYuM84OD7cUAIRtI/vUAA70AIetMAx60CA9OtIYmPkoATHagBCAOCaQBxjjrTBBgH3NAxuOxpAJjFAB9aAEOetIA4/GgBMck0xjRSATvQIOaQCHJoGPSJpFdhjCjmgCPr9KQCGmAYOaAG80DCkAUgI8VJQUCCgBWXaeaAEoAU0IYdqYhKACgB34UAJQAUxC9qQxcEruoAPrTEHfNABnNFgCgApgHXpQAvIpAJ/OmA7tnvQIOTzTAB70AGM59KBi5JwCc0AAFAgoAcg3PzgDvmgYh6nigQvJUEYpoQgIz0oGB60AKCTkCgQpHXrmgB4RhH5nbpQAg5HSmIcCfQUwDH1zRcB34Nn0PFAWDpQAp6UEjhytMAI/OmMcuPTmgQY7N0pDHNjdlBTAB6kUgFGM9KAAADvQApwRxTAMdu9IBeoALZApgJ1oAAPmoAQDmkA7txikAz6imAEZ7UDF4oEGB3pWGiyCW00r5yHD8REcmgCsRycA7s880AGCO1ACDJzkUwG85oYBj8qQCGmAEY6UgEJ9qAsJgCgAOO1AxMjpSEJj1oAbihgBz0qUMV124xyTVAJ1PpSAbmgBCPyoGJuPpQAnvUiAD0yP60xidsUABFAAe1ACDNJjF8tyhkCnYDjd70AM70AR1AwxQA4Ef3eaAEzQA+NUdsO+z3oAawCsQCD6GgYlAgqhBSAKEAtMBKAFpAAJAx2oAKYCUwFpABpgKO1IBy4Dc9KAG470wFyKBBjmmMUc0hBigA7Y6UwFHNAB3pgA60hCke1AxMUAKBTAUYPTrQIUigBwjY5IIFMBCO350hC5JULn5fSmhi45piFoEOHGDnkHIpATXFw91KssioCox8oxn3pjGwwvLKI0xk+tMQjKVZkPUHFAABgmgBSvNMBQKAFGeaQh2M0DF2Y5zkUwAAmgAHPUUhC8DtQMXbkZGM+lMBMEc4oATqaQAThqADjNAAQPmx09KADHFACEdzQMOM+9AgPXpxQAcZ6ZpDFI/DNMBOvegBMA96GAYFIBDwxApgNIxSAWgQnPPNAxpJ/CgA44pAGKYDDxmkAEYAoACMdaQxMc+tACdKAExzQAjfe46UDDjHWgBDyDSAsTWbQ2cM5dWEmcKDyKAKvHakAhFMAxjHrQA4gdTyfY0gLK3040xtPGzyWkDk4Gc0DKpRgOnHagCCoGFAAKYwFAgpAL26UxiCgQpFAAOKADvQAUAFMQuBQAnegB1ABQAmaYC9RSAKYB+FACigAx7UCAHNMYtKwhc7TnFMAPzEHHNMYcCkIXtmgBKYCjOaAFzigBD06UhErMjRgBcPTAjxQIXFMBQKAFxQA7HApgLimJikdKAQ5epoAUcEEE5HfNACjJPPJ9aADb+dIQ7600Av0pjAUmApAxQhDuSMZ49KoYAc4oAd+NIBMZzmgBAOaBCge2fxoGKcAEED8KBDccZ7Uhi8elMBpGWoATtSAXHFJAOYqYwMYamMbQAuMjigBOpoACMdBQITndQMRiTkEUgA4I460ABGV96AGD9aBC/Q0DE5waAAA7MjFADevWgBh69KQC9qAGkZ60DAjFACcCgBMc5oAQ0rjDocd6QhDn8KADPTkkDtmgYe+KAG546UWAO1AhBQMMY7Uhic+9AEeF2991QUJTAUcZpAJTEFAC9qACgApAPdUULtfdkc+1MBgoAU8mmIQigYoHpQIMHnNABQAZz3pgLSAOgoAWmIKAAZz70AHOaAFwKYAaYBjNIBcUAH4ZoEIe1MY/txQIME0DEAPpQId060AANAC4HUUwDmmA4cikIMce9AhewFUMXbQIVcHFADgOTQAuPQ0ASbQYt28E/wB2gBq/SgQuOelAC7RTGO4PTr70hC9uaaAcBzTGNA+Y0gF24PvQA7j0oAAMGgAxiiwCYzmgQgyvFAwOD3oANuDQA0daBsOaQh3Ue9MYmOeaAFA5akA3GM0CHHlRzQMaM4oAQY59aQhOme1AxO3HegBp4PSgA4BoAD0oAQjjAoATAz1oYCHAPvSGNxzQApxSEJj3pjE20AIfSgAPTmpAdmNYWXyj5hPDZ6UxkRyOKQgoGNOKABuMUAKckDilcBMUAJTAOlICH+dQULQA8IvlF9/zZ6UAMpgFACUALQAUCCgYuKYBigQUAO5XNACEk9aBh9elAgxQAH6UAObaxG0YpgNxRcBSP1pgKeCaQgFACAUwHdKQCqqlSS2GFMBvbmgBw9BQAYApoAA5oAXvQIXB9aAE6980wFxSEKBTQAQKYx2PlFIQYpiY/j8aEAoHtmmFy/b6XNcaZc36TwCOAgNGz4c59B3pAUhjAwP/AK1MBQM8GgQpHquMUxjsY5xigBfwpCHr8hOVznpTAQDIJ7k0AAHHNACkY70xjiMY96QCqYxkMm4npQAqBDnOQR90UANxnrQMDQIOlACMO9ABtxRcBD1oACDjjrSGOMbCPzcDb0PrTAZg9fXvSC4YHegQpBH0oGICOlAhNvAoGKQMUAIB1xSAaxO3kc0wAjCDigBOoPHNIQnXg0AIQR24oGD/ADEEDHtQA3PtQApPQHp2xQAsZiWQmZCVxgY9aQyPH60gGgc07gGBQAmBzQAnHSkAUANoACPegYmMdKGIQ9OakY90URxspBZutMZGfagQpGMetACc0gIakoKLALSASmAuOKACgA4oQheKYxKQC/SgQdKYAfpQAUwA0gHN0GKYCCgA5pAKKYCUhC5xj1pgB5pgB4oAdQAgNAgNAxT0oAkdkMSBR8/egBuOOBzTEGfWkAmQOaYDjt9OaYAPYUAKKQg5piF6djQMU9hTAXigB4piAZ7GgQpAzkjj60DH9T05oAng8gRytIfmx8v1pgQgcfWkIlaRniSMgbVNADRnPTimABcE8UwFwB9aBjuCDjrSEH/AaYC5Axx+tAwxigBwHcD86ADqOaTATj0oEJj1pjHYyPakA0c55oAMdSKAE65HWgBSSFIyQvpSGOlWIbPKcnI+YelAhmAfegAwDxQAcdjTGIDk880mIaMgGkApBxzTARgCMUDBhxQAzHU0AAHHBoAQjIzQAgHc0mAEHPFCAaMknIoAQ5z7UgG5pMYL1oQCHqeKYCA5oAQ4FJAIOtMAHepYCEc0gHohcldpIP8AF6U7jSFmhMblVBJHUmkVYiwPfPvTENx2oELxjFIQhH50xkNZlCnGODzTAM0AFABRYApgFAhaACkA5Qu1s/hTAbQAo96AEzQAHrTAWkAHFAC7aYDmQKoIYEntQA2gQGgEKBkUAFMBc9MUAHNMBRk84oENJ56UDHUgDFMAH0oJFpgKOtFxiqOe+M9aAA/e4HA/WgAB/OmIcMkc0AKvJ5oFcdtI7cUWAXB60wFHT3oAWmA7rigB3tigAIoEOx0oGOxQIBxmmAvfNAw5J7YoEx/PTFACDg8igYE/lSAUgk5poBec0ABzQAgHtQApOBjHX3oAZtpCFIHamMTkgYoBh9RzSAXpyaBgQBzSAQDBpiFx7UDEwetIQ0c54oAbtAoAMCgaEPJ9hSATK85oENPU9hQMDjHBoAG4x70CGkDHvTGLjjPcUhjcnr0HpSAaTnOKBCAYzQAYOaYDe+BSGHNIQhyKYxCMdKQw+q59s0WA6nwNq+k6Pr8Fzq1n9otwSGVudvvjvUMaLXxC1nQ9a1rz9GtRDBtwcDGT70IZxRGD6knqKZImKAExTAMe9AEVQUJQAUAAoAWgAoAKYhT0oAQUgFNABTAcWyMYoAQH2pgB56UALtBUtnmkAAHGadgEFAhaBh0piCkApGFpgJQA7tTATFAC4xQAvH1oAUYBG8HGecelIRZvvsRuv+JeJBBtHEp5z3pjK2c0xCg80AL/ABUhDi7GPy8jbmmMaD0B7elAh2D1HSgAwevamAq/SmIUA0wFA5oAf2FAhcHvQMfswMj8qYCj9aQDuSOlAhUXLbc4pjFKkMeOlAgz7UAOyOvf0pAPXy/Ib+/u4pgNwQBQA7HpQAmMj1OaBks0EtuVWZdrMMrz2NAEZXB560AAzk0gDJNNAMwcmhgKABSEIRQMU8/hTGIuaQhG9COaQCnrjtTGLgfhQIbzk+lAxPvH0pAGOKAEwKRIhHpTKGnk8igQmOvFIBMkggjj2pgIcfhQMUnkccUAMxzQICADx17UhiEHd70gBdu/LdMUAMxjP1oACBQAlADeaBicUhAQPWhDE2gg+tAC5Ycjj+tILhks3T/69Fhhu4xsFAWGc/jQAmcUAGfWgCGoKFoAWgQlMYVIheBTGJTAWgQZoAOtABQAtMBKAFFABQAuKYAOlIBKBDh9KAE/CmAuaAFxxQFxDTAdxSYhCKY7jlUl8DvTACMMR6UhBQAopgKB83sKAE7k0CHCgBefbFMBAPzoAcSSAMdKYhetAC9vegB5+XqOTTAMUCH0AKBxQA8AnpQMcFXy2bd82elAhMcfWgBcDFMY7jvQKwDg4xTAcPunPWkABcimMUDmkINoP9aAQ95JHKmRi+BgZ7UDGkcetAxSpxu7elIQ3aSaYB069aQCYzn0osAcbulADSOT9aLgOK96AD8BQAmfUUAhOv0oGB7ccUgEwBQIOeeKQDSQOooAD0yOlMY04xx1oAQ5OPWhAJ+NAhp5JzQAh6+1MYHipEHHWgY3HOaBCHnrSGJgUDG4HehCDtxxQMTIBpAISO5x7UAIevSkAEDuMDvimAsuxn/d8KB0oGNNIQhAHNAxOR0FAC0AJmgViGoLEoAWgApgLSEFMYlIQ4bf4s/hQMTjPHSmIWgBKAFpoAoAO9ABjmmAoHWkAE4oAUdaBAc5pgKM0AIaYC9qQBjmgBzpsbHXPemAnegQcA8cGmNC8+tAC5FITDGTQhC561QwHSkAAc0CHAEdBTAUHmmAmOaBD+1AhccDHWmA45bHtTGOHXmkIcBnpQAq0AOxQIAAe1Mod3xigBTQIdjjjmgB3IBBJ/CgB7xxqF8t95P3hjpTAbkE+1IYAAGmAAdTSEKR0zQMUjAPPFICS4ijTyhG4fK5OOxpgQkYOT1oEGQD0/GgBpBAFAxT2oAAOuakAI9aaAQjFMApAGKAEPTnrSAlkaFoI0SApKD8zg/eoGQsrbd5HyE4BFADe3SgQ3nPJoAVAp3Z44oQyM9Bk0AGMdAc0gE56mhABFUA3NIQ1uWx2pDCgANIBoHvyaBiYPXqfagQ8QuIWm/hzgc0DIsfmaQh0bBXyybh3FAyM4ySO5oAODQAnBoGJzk0AIfekIcQARTGNPWgAxSAhqChKAFoACKEAUCCgBcUwCgBcGgA5oAMZQnvTAAOKAACgA70ALjOaANeHRoJfD02p/2lAs8b7fsrHDkeooAyDgdetABQIXHHrQMOlMQuBQIXseKQyxOtp9mh8gsZz/rAelMCt3pgKRii4Bx0xQIdjjNACdOQKYCjmgLABTAd0oEBGBmlYQ45OKYABTGL3oEKOtMQuMGgB+OAelMYo57ZpCHn5gOMUwFH0pAKOOtNCHjpxTGABNACgY60hDwDgkHimhiAUAOUc9BigBQOuaAFPQ9hSAVlUkbOh60AGOxFABjHYe1ACAA/WgYnTigQpxjI60AJgmkwDrkfyoGhWVAi7D8560ANIz1oAT8KADgdqAA8jgUAIKADoPXFIYM7+UIt2UB3YoAjOc8cCmADOTSEN4zz60DE6A5HGetIC1qCaeJYhpzuVKDzA/8Ae70gKTDOMcCmAEUwEx7UAIRkk+lIBpOelACdaQCZG4GgBW+/lcj2oGNJJXaGO0fwmkAzPSgQGgBDzQMTgdaABuBx0oAVhz1waBiDk4xQAZ55oEIRgGkMcFjMZO/5z0GKAK1QUHagCTfiIx7Byc570AMPb0pgFFhBQAZpgJQA4UgDFMBKAFzQAcUALjFAC0CDj0oGOBxkMuRQA3Oev5UCJJmjd0Ma7QFGfrQMj60xC0CFyKLAH86BhxmmApyKBD41BbB7kcntQB1ur+B5dJ8M22tNqFtPHcYxGr/MOO4pDOSdecAYA9aYmIMZphcXByeKEAg96BCn3HFMBSMnPamIcAD35oAcFPPamALz16UCHD6cUgFIIHv2qgJXjeMgOpUsMikA3bTESYwBjrQMUcHpTACOaAH4OBigBeAcEc0IBQM8YxTAUjikA7Hy0DQfWgQ4jApAGPTrTGBFACcCkIX6UDE79KBCc5OfwoGGMdaAYZ4460AJx6UgDABpgKeRQAnJ4PSkA0jmgAxQMbjr6UgEPWgBDg96YCc/SgGJ6jFIQuDtHKnaeBQMZnPHPPegBp7A9aQCEfNQIbz0zTGNzz6UhCtnjFMY0GpYAVJ4PSgBfJcp5gT5R1INAyPPHHSkAY46imIbxnBNACHg470gHBgG5XNAxueRxxQA0nv1NADmJIDdDQA0frQMMmkAmPWgCIKD3qChKYC0gA0wDFMQCkAmKYC9KQxevSgQgBpgKaAFpAJx9aYCnAOKADvTAMDigQ4jmgBBSAXpTAU8UWEGaYByKYwpCF/CmADmgTHDp6+1IaJDNK0SIzuUU8KW4pAMz82WBz2FUApOeduPagQgOc0wFHPTH40gFKnHPbp71QEssMsO0SRGMuNylh1FICPaQTx+NMQoHvTAcBnrQIUDigQ/AxxzTsNDyzOQXYtgYGe1AC4xQIXHGaYDsUxijr7VIhxHIz0pookySpAABHcDrQITb9c0ABHFADsYxzSuMXAPb86AFx7ce9IQmMnNUMdwe3NAhSD6UANJPpzQMUYz9etIBCG6EcUAMHuaQCntjmmAHp05pAIR60IAK0DE+nWgBD1570hCYNMYhyfagQnHpSATaefl4pgMySTQAnSkAmAKAEOCPekFxM4JG3mmMQ/rQAY9OvpSAZ3II5oEN470DQoxQDE56HpSEAdgCquQp7CkMaSCAO9ADMUwDIA5oADyc+vSgYgFIBMYNAAR2HegAxnPtQMDntQAnXrQAdeQTSEQVJQtACUAKKAF70AIOlMAxQAuOKQFm6uYZ44FjtUhaNcMyn759aBlYimIKYC0hBjPbigCVJFWJ0aPcW6H0pjIunFIBcZ7UxCt1GBTAKQBnNAgNMBT0ouMMGgBRz060CFGTTABQACgBQPmoEKQTTGGBQIMAGmA7BzjtSEKpxjrkHigDR1LVrnVPs4ughFugjXaAOBQMoAZ759qYhQATTAAMUCHgAU0IcowTTGOUUgFxkUIQ8D1pgL09aYxwyc8UrCHKM9gSPWgDS0j+yftj/2wk/kFCB5OMhscfhSGUm25bZkruO3PXFMBvGSKQDtue1AE0AtzN/pSt5ZHJXqDTGRkZLbTlc8fSgAPFACkUCDGKYBjFSMTo3NACcbiCefWgBD9KQgPQ8baBiyIq7SjZ3dfamMYcZpCDABINAAqsxIH4UDEbGcd/rQA3HNAhD1FACMWz2xQAwjPNMBMce9IYZyKQhvfmgBOhxQkAhBPNADTikAD5cnH0pjEyeopCEIJ6Hn0oGKI8xtJvAIP3fWkBEcYyRyaAAkeuKQAflJ9+lAxnc0CExgGmAh5oGBwKQCZ55pgA+8DigAAwxpMAxkHJpAIccetMYY96AIKgYtAwzQIM0wAUgAUwAUALQADNABigAoAX6UwAUCF5oABRcCRUHkl93zZ4GaYxmOh70hC8mmIAD6frQMTrQA5euKQgKnJB/CmMnFpcNaPdCI/ZwQpYdjQBB16dKYhRigQUAA4NADvemAcHp1oAXp1oQDsYTI6mmAg/n1pCFA5pgKB+ApgOwBmgBVHBGMmgQ8dOaBDscdMY70wJNgCbgwJ9KADrwKAHD6U0A5c5oGLg80AOC5AoEOHHSgY/HsTmmBajuxFZy2v2aF95yJG+8v0qQK2OABx7UwF6dKAHY4460DG46+tIB7Kmxdp+bvTAaRmhiA+3WkApXBIIznpQMlWfy7SW3MCOXIIc9V+lAFcggD09aAE/CgBMbccc0AIeOnX0oGHHpyaBDQOg75pAIc5OVwaAE4FAxp60xCEc0BcbjmpAQjqe3rTARhkdaAEHY9aB3EIyxzQISkAYI6Dj60AMbng0DEIxQIQDIz6UgEIyc7cCkMTrx6UAIRtJAH40AKQw4OD7igYbE2FvMy/pQIiHTNAxPWkSKegx1pjEzQMQHmkAZwenWgBCKADgCgY4qBgAZoArVmMU8GmMMUxABQAUgAUALgZ5NMAIHGDxQAAcUASiEtC0gZcA/dzzSAizTAc2CBjrTAMUxC9KQB/KiwhSACOmKYw5zikAd6Yg696YC/jSAQnPQ07AOxx/SkA9ZZViaFZGETHJQHjNADM+3NMBcY/hpCA4PbFNAOxxwM0AJ9RTAXBzxxQAAZzTAcAdg4OwHrSAPwpiF5oAeqlgxzjH60xCD6UATGNFt1YNmVm5X0FMBMdKBDwOlMACj0oAeeBQBJ/CPX0oAXae9MYo6UhDtoAGKYx2KBD1XvSABxQMUY6CgA20gFIpgLjjIoATHtQMXiiwBjkHtQAmDk+9ACYANIAxjgUAGCetADOc80AJwO1IAx83rQA3J3ZHY0ADszNuOPfFMBnHpQAjfSgBHCnAHXvQA3ueKQDcnoQMUAIRgUhATgbdvU5pjQ3oaAGkUWADkqPQUgE789aAE/iwaQDOnFMBSM855pDEwWHHFADRnB70gsHJ4PIFADe/SgBpGKBh26UhDdopjAAetJgFCAQZzzTAXIFIAA60AIPY0AQZqChaAAigAPFMAFIAoAWmAY4pgApCFxQMT2oAdjj3pgIaBDhj8aYCdTQId2oAB70WGGOaBCY5oAUYoAXAHNO4xSMDPc0CACgQY4NADz5Yj776BidAOM0xByfp6UDFGfSgQEc570xDufpQA8Sv5Jh6xk5oGN9u1MQox3NFiRce360DFFAh6jPb9aYxw460wH4oEOXNMBwHXNIBQuRTAfzikBIybdpyGz+lABQA4YNMYoH4UAKBx0pCJXYSIihMMvUjvQMYPekAEUAKRxxTGLQAmO2KAEPNAAaQCEd6AEK/LuHX3pjExz70AL7AYNIQ0HI5GaABWaJsqBk9PagZG3PsTSAD7UwEPIoEMxmkA0imAhGO+KAE28njPqaQDSM85zSEBGQMVQxuPSgYjHFIQmMqfWkAzjtyaADp25pjE6n3pANx60gAgAA9TQArDOTgjA/OkAwD8zTAAe3ekA0jvQMRuOCMGkITvQAfxUDEOaEADn3oYARjqKEApUgbiDtoAQEHnHFAFeoLF6UCCgA7UASo0YidWjJc/dbPSgZHjjrQIXp2pgJk0wFoAOaBC9uelIZNOLf5PILZx826mBD17UCF70wDGDQAUCDGO9AxR1oAMc0CDvQAvXjHFACsOfamAUCYo6H1osIXnv1plAMjNAhcd80AKAnlls/PnpQAYyKYB0oJHYzimMAOaBDue3WmIXJxQMUCgRIFzTGOAHv70CHYPpxSAftPXiqAM8dqQEgGBTAUDPagB4UdhQA8A9xxQAoB6AUDDnGCKAHqO1ILAvPHegYuOcbefrQIXGOo4pgJjLZpDAdTxSELyOaAGgDBPemMX8KAY05ANIQhAbaO1MYYw3FACHkk96QDcE96YCD0NIBOAcYP1oAYRk89KAEIzTAb0qQDGeaYDBkg8ZpCFDvGHAA+YY6UDI8cYoExCOlACEL2NMYmMHkcUgG+uB0pABJxkcUAIQSfSmAnHbk1IxueTmkIeiK7hZG2ITyaYxkiqHZd+UzwaAGt2PWkAsiPHjIxu6UAMIJJwPrQAsjtIwYgDAxxQMaRjhsc+9IRa0/UH0+SR44IZd6FMSjOM9xQMpsdxJxgk5oAe8bRqC+NrenWgCPgZHJ96AHFmKLGTxmgAZSmNynHb3oAh8z93s2jk5z3qCxgoAX8KBCDrTAWkAh60ALkkVQCj3oAQ0gHfLs/wBrNMBwAKnn5hQIaMn8aBhjmgQoxmgAPJzimAvI7UhC49cUDHxwPNIsaYZ2OB2pgOubeS1uHglAEinkA0CI/wBKaAQZzg0AL65oAMigQYpgOx3oGGc8UCFAIzQIOOO9MYp/WgA60CFxjBzTAUDk0CFUcmmAooAePSkIkA596aAljMZk/eKSpHNMAOCTjoTxQA7AzQhDlAP8P60xjwD6UASRGNd+8ckcH3oAQKQKAH7fQc0IBw3Hg9PrTGA9MUgHYx35oAkRAYnfzMMP4fWgYwAkDmkIXHNABjDD0oAUg5OBkUDG45x19qBC9OMUhiFTmmA32xQAhAJoAXHNADCuCaQA3tQAhyPTNMBpycg0AMbhqQCEGgBvf2oENP3jTGNOaQCZx0FACNxQAhI75pAJggn3pgNyQeTUgSQwGYuAwAUbs56+1AEON3+779aAECk8jmgY8QyNG8vlt5KkBmHQGgCLnBI6UhBjIztHvQCE6cYHJ9KBsbgDK4/HNAIUszEZOQo7ikBGAe3TNAE0dtPJHLIkRaNOXYdqBlfGRkDjPFAhec0AIQM4I4pDEzyMk/nQA7bjjacUAJjBwetAxxdzjJ3YHAoEVgSKgsPXFAhOe9MA70gHfhTAQ0ALjjIoABmmIMnoRQAd6BhxQIce2KADjqaQgYg9KoYYoEL2oAMc0AKMjBHBHQigBWYsxLEsT1JNADcU0A4AYJoAAMigBQpoEGfXoKAFz6UwFAyc0AIM5pgO6jgUhDmClF2/e75pjE69KYhT74xSFcUj1Ix2qgHHg8c5oAPegB6qxBcKdvdqAJFBPP5U0Ik5+gpgKOD0yKAHKOaAJNppgKopWEPA/KgY4CkA4qPxpoYoXPPSgBwHHvQAAYFADsDqRk0DFIzzQAAA0AJjmkINpOevtQMl8ljbmcOMZwVzg0gISOOBz3oACBxxzTAQj24oAQg0AA69KAGHqeKQhpGB0oAWTBC460xkZFACEflSAawPFMBMAdaAGN+lIAIGOM5oACjBdxI47UAR5zyRSAOjDP50AMJwcFu9DAOOcDPrmkBKTaiBlCMLjPDg8YoGJNHAI4jBMXkYfOpGMf40CIDkjj8R60ASC5mS2a3WYiB2yUHPNIZEw6DtQA3r0oAQjJoEIAMkFqBiHI4zmkA3PUHpQIcssiRtGkjqjdR2NIYwjoaYByKAEIJHuaQEr3BkgSIxr8p+8OpoGQjgHufegAHQ5oAQDng0AQVBQAUwDGKBgOKBC0AGKBC5NAxOtMQvSgYoosIMUAFAC0IQuOOtMYZoEIeRQMce3FAhO9MBcGkMPrTEKRtBBFAwxigkU7fxpoBRtJz09qAADDdOKAFBz7UwA+1AD8N5e/8Ah6UCEAPUdaAJY7eWVXdANqDJ5oAj9flpiD8KoBy4oAco5I7UiSZJZRC0Kt+7Y5INBQ4Dp/KmIfz0xTAfz6YpgOAB6DmkA4DsaBDsCmCHgUmMcB60hDtvHFMoe0bIAzLgN0OetIBCoApgOwBxQwFX3pAIR0pjFK4oAQ0AxSM4z0pCHiAm3ecBSoYDg80DIyC3I/GgBNtACdevSgBMCgQm2mMawyaQCAUANNADSM49aQgkRkbY4wfagBhAHfmgBuVpgNOOjfdpALIIzIRET5fvQBGRjPGKBiE8ZxQAnUcjJ9KQiSKcwrKvkq4kXHP8P0oGV8AcEZzQIB3ANIdxOM+/rQA3G00AAHyn3oGNx0oACCOaAE4/GgQhGM5pAKBlc78sBwKBjAcdRQAHnIJ6HpSGNI5oEIQOtO4AeRSAMYJpAJigBSB65NAwIwBwM0AVqgoDTAM8UgF6ACmMKBBQIKYC0gAjIzmmAuMUxhn1pCFxxkCmADmgQuMGgYvNAgPb0pASg2/2UghhPuyD2xTGR7enf3oEJzn6UCNTRNGm17U47C3miikkztMrbV47UxkGpWT6dfz2cuGkicruQ5BpAVCeelMQGmA7GCcD8aBiDOeTTELgdqBC5GKQDvm2hc5UVQAOTwaBCjg4GQD1wetABjNMBQfxoAXHPPemIdjBoAkUdKYEq47daAJACR70wHAHPWkIeuBnI5oGKB6imDH7RSQkOC96YxyikA7aPTNAIfliBliQOxoGJj9aAH4znHWgBMZ7/hQMXHKjtSEBwGIIpjDaMelABtJHWkITAPsO/NAwIzzQAhAoATrxQA0igQhoGNI9OtIBCMdOtMBpxz60hMYeenWgBGz3JP1pjGnigA9cDJ+tIQw/XigBCM0xjeOh6UgGsc4oAToxzQxCEcjNIYjADnjFAC84yNv50BYZ15IoAbjJNIBAB0zSATIHGD+NADcCmApWmAg/WpYxuOfegAHpgmgByeSok84Nux8mPWkMi4Ax1oEKRkZGBQAhUfU0DBwQQpHNIBBzwRxQIQkdMUIYDcBxTAr1mUB+lMBeMUAHWkAYpgHSmAuKAEoEOB4INAxBQIcNoB557UwEySMHoKAF70AL39aBBimAp6CkAo4oC4c9M8UwDPzcdaBD0do2BUkOOQynmgBWdiTuJJJySeuaBjAOKBDgOOn40AauqWmkW9vYtpl69xNJHmdXXARvQUxmVnJ44FAhcAnrTEL6jHNMAAPPFABj2oAXGD70xC49+aQEioSjSBhwemaYCLjjrzTEOx3FMRMv0zQBIo9aYx4GP4qAHr14NADv484oAfgDOaQC4oQh5A2e9MY8ryPekFxVBA+tMYo4zmkA4fSkAmOT70wuKQMUDHH+H1oAUjJoAbtGeaQAFGTgZNMAI4570gGkdKAA4/GgAI496AG/WgQxhl6QDcc80wEYdKAEZefU0AN9eOaAGdaGAhxjpSAWTyzEgQYccHI60DImPbFAhGHA7UxjTgZ3ce9IBD9OKAFwnkH+/npQBGB8oGevWgQnGT8o460hjdpycL+tABg7eRyKQEtxaTW0UEkwwsy7lwc5FICAcN6etACHJJOQaYxmAM560mIQj3oQC8Z460MY0juetIQpycc0wEA7YoGN6UhhwT04oEN/3RgimArEk5PWkMTA6kHBpABHtgdqAGjNAEFQUFMAoAWmAUgDHtTAKQC9qAJZLaWGNJZYmVJOVY96YyKgkcOV96YCjHf0oAAMZyKAEFADhTEIe1AC0ALihALj5ScUwAHnOOaBBjJ96Bi4OfagkOB9KAFz3wB+HWgYp+hx2pgAB57UCFwTzTAM880CFAGQRmmAfxdaAHAdeKQhQAf4fyqhjl9CKBDx9KYiVBleBQBIo5GeaYyTbtOTQA/6c0AOAzSESksV24GKAEBPTFMY78M0DJCOFOeaBDscCkA5Plb7uaBhtIyR37UAKPegAwOnb1oAG69KBgRzQAuPTk0hD4ovNnCNII89z0oGRsNrkHkA9fWgQ09OlACuN2OOnWgZH1zQAjUANPWgBueTQIa3UelAASQeKYDTk9aAGEjNIGNPB/lSAT6jn60xjTnv1pCG7S3AG454FADnwjhDGySdG3GgCMgnAPI7UAN657UDGgenUUhFi2tGumlImjiKLu+c43fSkMqspOSeo4znrTEB5XHQAcj1pAISWwpYkKOAe1AxuCCc96AG4xznvQA8jCg7wDnkd6QyIkchuaBD3EXlApneeoPb6UDIyMYzQAEd6AExjp1pAIw79TmgYMCnUEA9PegQ0jA45oADjvwaQDpJnljjQhSq9MUDGEkj2oAUICm4MM9ApoAq1BQo/GgAxQAlMB3egBOaAFNMA/nSESvcTTIiSSs6J90N2oGRUxDiO3Q0AAGKYB3oEOxmgYYx2FMQZzQAo+lAmH16UALnIxjimMAfyoELgmkA5VUqSXwR0pgNHvTAXntSESPIzqisBhaYDTnHtTAU8gAGgQuO4BpgHPQ9KAHBcvjoCQM+lICSaIRXBiDiQf3hTAYATzjFMQ8A9h+dAhVJ5yOaYE6DKEnrQBIgwBTAmAwCRg/jQMUZ49KAHrj0pCF24poY/bkUgHAccdaYDsZxQMdjNADqQDtvpQAbRQMUqPwpiExmkMcAM470ANCk5pCE49KBgBjgCgBpzQAbcdKAGkD60AMwefQUwBjnp0FADMDsDSAb3G6mAhHPt60gGYpiEIpANyR2/WgBuCMkd6BjTlup6UMQmTuBXhh0OaQIWRmkcs5DMe+aYEQHrSGI3HSgBp+nNIQEA4+UUANIHQjii4xCoXHIpABDFvQetADQeuASKQxOhwRkUEiYG0n1oYye2ltkinWeDdIwwjA9D60hkB4HPLE0AIqEnb+poAe8BV9u9WPoKYEHU8cEUhC5DdBk96BivJI+zdyE4GR0oAYSCMAf/XpAPlaJ1jEcZjI+8eu40DEypBOAHXpQAzhu/HpQA3Hp1oAr1BRKsrLE0YAwx6kUDI88YoEAoAehQK4cc9qAGdBTAXtQIkJRowoG180xjMZz7UCD3x+dAEkjmVwxUDAxxTAYDmgBaCRD1pjHD6UALSuAEUxATTGLjigA7UCDHrQIXHPSgYYFMGOGKQgAFUAY5x2oAUfKaBDuecGmITjHvTAfj5Sep70rAAAHagB2MDrTAfjOD3piHqO3QUwJYwdhoAmTryKLASKMZPegYuOM9SKAHj8j6YpAOAB6ZNMB+AOtIB2CTn8qYDxjn1oGPVcN70AP2sM8A0gGj3oAXbjccUDEPHXn0oAcFoAMcZoAYAKAHEelIBpHrTADigBpAoAZjg4NAA4OBzwetAEeOcnpQAhzu69aQDO5GDj60AIc8elADTx2pCEI7gAVSAaevK0AMJyaQxuCM45pCEIyfemCGgCkAhUDkkYoGN79OO3NADcHJ9KQhG7UDAkHjvQICMKeMntQMbjJBBpAWYbG6uUnlggZ4ol3Skfwj1pAUuDgjJPpmmMUr8vTHtQICx4xSGIwxjjrQAmMAg9+9IQm0DB70xg3HBXr0NIBMY780wEwT15pAN/2QaQBjHHWgYMp6kcd+aAEJBX0FACYJPFAitioLF6UAAGcmmAe9ACjmgBKYC0CF6UAGKAFAoAAOeaYAeORQAuTQIADQBJHHvDndjH60wGfWgBaBAAKYC4+agYoOR6UCDt60ATNCUt45d4O8n5QeRQBF6880APkULtwd2RmmIbjNACjHrzTELjmmAAdaADbTuA7jPPWgQ40AOxxTQhwXoQKYEgX86YEyA7DQBMg6UAPxz9aAHgenUHNIZYnnku5RJIEDKoUbRjOKAEXZsOOG7UAG3AANADto60DHgDPHNAD8YY80gHbenWmMUDHakITnlT0oAUAAY28euaBjcHJoAXBI44oAQgdOlIBMcDH5UAHA4NACEGmAz60gGv8oxigBHx5YA6imA0420gGMMDOaLgK6bUVlbLN1HpQBGcHknHtQIHjwBtcNn9KAImGelMBMDHWkMaQSvTv0pAOlikgKCdGjDjKn1FAiIq2Tgbf60DGdSeMnvQIPu5wAaAG4JU45IoGWZdNu4NPiv5YsWszFUYN3FICmehP5UCAfNjigYwAZP8AKgAHIII+WkIsQX11aQyxQ3DxRzDbIoP3hSGVs9RwDnrigY0j1796YCMoyKQh5+YdRkCmMjwWHPJpAKMDgH5u2aQCyA7huwcDqKYEQ6nnmgQfKe5zSGhMZycc9qBjBkZzQIUAeppAIwJ+lAyR4mRVLKRuHy80xFKsyw4oAcPummAg6UwCkA5RQAFcE0xB+FABQApoAeuwYY8sD9096AGsQzFgMZPApgGMGmIUZNIAwCeKYIUc4zQDFxQIASaoAOQeaQAMc0CFz1HSgAPrjp70DF/CmIcVIQPjj60gAeuM0xChsN0xVDE759aBDsYpCA8jJNMY4AH+E5piF28D9aAHgdKYh4B4z0oFckA9xiqGTRr8lAEqr0pjJQBkc80gHY+egB4HzUgH7c96YxwXIBxQA5RSETIjbCWXKj3oGNHPUdaBkhXigBMZpALigAxQAmP73SkIRhlTj14pjGnA46mgBzYDLj8aAGHkn1oAbjrk0gFIIGSRimBH16jigQ04B6cetIY1+PpQA1vXFFhDSOc4oGMIA4NAhMAGgYhG3+lMBmOaQhjc5GOe/NIB8k0kmzzZC4ThQ3YUARH5h8ufXmgBVjZw7g8DkigZFnPPfsKBByecY9eaAAyOUWMu/lqchSeAaQyN+ev6UCEb5ee9AAR8wyeaBgyuD90DNADAM5GMmkBIsgVHXysu3fPSgCIAKAP0oAOjc85pANxjvTGKVGNwPJNIBuMMQQT70CAjOATz9aBidOi/jSAbtBc5zigEGCBlcqAe9Aw6khetK4hrKSOeo9KYAqjIByDSGSSSOxy+cAcCgChUlC4oABigA78UAL060CDHJoGKOeppiAGgA60AKKBAOaBgaYhSMigBSDnNMAHP1oAMc0CF4poBcDt1pgKR+dICSSUzFcoo2jjFADAT3pgB5HFIBRndimIXJHy54oAX5mHA/AUAIBjqKYCj8/pSFYX2NMQoGQRmmMtx2N09pJdrbyNaBgpl7A0AQY79u1MkcOR1pgPAHemhEqjHYUwJlGV6UATKMigZLjHbj1oAUCgZIAMdOaQAF9aYyTacDHApAPXnjHFAhwXnoaBjgPUflQA48AA8k0hi7fTrQMXA/GkIbtyT60wDoD696AGkZwSOaAD8KAGkkUDAjGOR70hDCQRwOtACNj1pgIRQAxs8g0hCMxZVUqMD0pjGc/hSEMagYnB6HJoAYeT0oAbzyc5FAhp9qLgOCgws+/kHhfWgCIe6896QDSMZ9TQA0fKeDzQANwPegBuM5JoAbyeCfzoAbjHABNIYZw2CM+9AhpIzk8mgBGzwd1AxfmIyDSAaMnODn1xQAhGT70DF7YCkkUgG45w3NAgI2cHr2NAyYwxm0a4FwPNDYMfc+/0oAgK8bvl3dSDQAgyAwbr6+lAhwLZDkI2PU80DGyymRzJjlj81SBK8MbRI8dxvnY4KYx+NAyuyhYxk4YnketAmKONykZB/ipgNy55zz2GaAKfSsywpgFMQuKBij9KADOaAFoELTEFIYAc0ALgetMQfLzzQAnemAuB60AKRjGKYDgcUCsJigBQOQaYCkfNSEGOaBi49OaBDgh2b+2eBTAQdc0AGOeaYDhweDg0hCgjYQwyCevegZPczRTSRmC38gBACufvH1oAg/DmqJHH1xzQFzQi1a/i0yTTEunWzkYM0WeCfWgCmcdOmKYh2KYEi4PTrTESjJPtVDJ1HHSkBOoyKAJcllAIG0dKYwX3pAPAbntQA4AjPGaAHgHGM5NIY7HQYoAkAPUdKBi+uKQhTywz2oGO2/NQMTbx1xQIMDIOaQCHIZgOgpgJjjigBCKBjOp9qQgAKnOB7ZoAaWy2cc+1MCNh1oAH7YoAbyM0gGY700Az1oEIQBmgBCMqCDyO1Axucnnp6UARkZzjihiEJx2pCEIDYHGB6UDGcnPY0DG5weuaQhuPmoGIRzimIYRjnJ4qWwFyM8Hn6UAHJBY8EdKYDeTk4HNIBoGDg96ADbt/iyc8DFIY4pgSPkEKRgZ7/AEoGMySpbGCfSgQhwM5PNADiGZwwGBj86BkQY/NxnnpQAvJXrgemKADjOVwef4qQAxEsoLkDJ5oAY4+cjd/+qgBqjBwAc565pASQ20tzOsEK73Y8DNIYwgxyOkiZZflOexoAjzjAYcCgBxUk+gx+dADV7gHFAirx+NSWBFAAKACgBeOlMAoELQAY5pgA4oAWgQdhigBw96Bh1B9KBDzE6xCQr8hOARQAw8800Id9KBgD1zQIMc5pgBHIpgKQKQh21uuPl+tMBvUf0oAdjNMQvFAAOtAASRmgB7goQpHPrmgAHtVCDuaAHAfOKAH4GSKYh4/E0ASqOuapCJEGBzQMtKrKAWGF7H1oAlUd6AHqvNAx4780ALtHWgQ8CgY8jLg96QIkA5oGKF478dKQx+0YzkE0AKFxQMXjNACYxmkIVzwAV6UwGdWPpQA3btyR19KB3EPP19KBCEY60ANx+VIBNh8ssD36UARkcZoEI3TjrQMQrldwyTmgCM889KYDcdcdaQhvOeaAsMIAHSmMQ8888UhDWVl+YqQp4BoAaRjPBI9aAGEAUADAcHOT6CgBpJI6YpAMJy4OKYCFcseTSAaGAzlc/jSAMA57+lMYwrwAaBCtwQOlIBCeSNvJoAcrMjsVYZ680DGEbsnofY9aQCY3HkdKAGg7eCKAF2jJOSPSgYmSTngD1NIQgXDMEBJ6jbQMCQwJRTkdeelADQMYORkHoaBCyyF5DlQGPp2oGNA6gnINIQ0FlbchIZf4geaQwJyfm+Ynkk0ANI2jpknpQA5gphB3/ODgD2oGDKVIG3Pp70CKNSWLQAUhC5HYUxhjH1oAKYhw6UAJTAAaAFPWmAUhCkcg0wFApAO3uU8reSoPSmMbwf6igQ7BHtQhD1VSjMZMEdF9aBjO3HWmIUcdaADPtTELg9cUAAI60ALQIXpTAMgdKAYvbpmgQuc44pjHY70xB16UAPH3hx0pgL1zx+HrQItTyQySxtBF5QCAMPU+tMBqgjr1NMRYVePc0ICyrMwVSxKjoKYyRR2NICRR89AxQvWgB4AxzSEOI6CmMeAOPWkMkAxRYB4X9etIYoT0FADgOBmgYpU9hQA0DOcikIQg4Y4pjExnnFADQfmNIQzaDnjmmNCHjINAMT2PSkIYfQUAMNACHFAhq4BPamMYRwR6UADD90Gz83pSERnI5796BjS3YigQhAxzRYAaSRo1i3kopyoIoAR4isAmDggnBXvQBCcAep70gGsPlz3zTACAPZqQDCMd+aYA2Tg96AGjOewHcYosAznBINIBcDoBkikMaMkjON2aBDpVaNgG+bI7UDIsYzzwetIBWACg0AHBAw2KADAVwO3fNADMEs2OBQAoJKHIyopAT217NZl/s4wJF2ksM/8A6qBkDOZEClRvzlmHegBA2xsMgJ7GgBhJLMWHJ70AMwB16UgEYEkY6UAIfegBwUjlRkCkBHjJOeT/ACoGOHCn5+cdfSgCnioKFpgFAgpgO7UAHegAFAC4PNAB9KYCikAnemAuKAFPFMQo4IPYe9AXHjY0oLKRGTzj0pAEnl+c3l/6vtTEN4PagYdKYh+eOPvUxAMn/CgBoz3NACnGOKAHDjtmmIcp2tkrn0oAQc545NACg4600AoB79KAADmgkcBxu9KoYuSfrQBIOnHWgRIgyOtNASoCOMZpgTBeR2oAtKuR6UDJF9MZNAD1GGoGOC9e9IRJ26UAOUYwCKBolC4oGOAzikA/aBQCY8AigBMc0DFwQeDQAmCeppCGkH1pjG496QDW4NADSRj0oENYAc447GmA3aQTSAYfYUAI2eKEIbgZxjmqGMYc4NIVwUpvAdeOtAET4JJHTPFIBDg49aAEYMOmMfWmMjye44oEIfm4/SkAgUZyAMfWgBnJHP4GgBpGegxSAWWRpipZR8owKYEQ680gGkDmncLi7ehBoGMY5OMfjSEK3zY9qAEJJB2kAd+KBgM9AxPHekA0Ar1A+tIQONow2CT0IoKBVVnY5GccUwGZODgjFIBzIQwLFSx6BTQBIbScWhvNv7tmwMN3pAVxuPzAHHpQA5tvBLrzxgCgZFtJzwc0CAcjBbikwGEgHGMjtQAv8WV7dqAYzIB96QhSMMCDyetMYdRyR7j1pAIMN0UYoAp1BYvamAChiFoTAOfwpgL1PFAADigA560AGOKYD2AG3B5PWiwCfzpCJNi+R5nmfNnG2mAymIU9qAFoGGPagQYxTAXPNMQYxQAoFACj9KBCgegzTGAH5GmIBQAuOtIQEcHHWmhkhAwpGcng0AJgDimiRVAGVpgPAyKYDlH50ASIMcUAWEXjhufc0wJ0AHbPrzQBOF4HPNAyyFO0kH5h2oAABgnHFICYrCsKFHJmJ+YHtQAmABjvQBIq0DHgepoKHgAikA/AHAHPvSFYXb70xi8CgLCMMUABHtzSAaV44PNABtXk5+agCLtz1oAa2eeKBCNllC8cUwI8dfSgAVMhyD0GaQEXUdeTQIT6dfemMaTkkkUCsMOaAE6jpigCP8Mk0gG45zj9aYxu4d6AG4GCaRIhAMfuKBjGA4GaAEY5GKQCYDD+goGIf9kc+hoEN7/NjHpQFhpHXC4pjEbjANSIRgBz19qYIe0bDDBcAj160DIgCecHJ61ICEckMaAQE46qceuaBkkEaGTEhwpBO7Pt0oAh5xwpw3rSEKCVUqQvHc9fwpgBfcoVS3l5ztzSGMJDZ9QePekAvygEbAAehPagB08YEqiNsgrkkd6AIwhZsbct6UANwdzNt9jQMjGAxBz14oEBOOduBQIZ/Fn8qBi8CkAm0DsaAK1SWFAwpCCmAoOKEIcST7UxiD6UCFJoAXt0oEJjNMY4dKBCYHHGKBi/hTEKOetAgx1pjF5KjFAgx7UCF44pgA5oAMc0AOFAgwfwpoA70wFxSAXmgTFABB9aAJ4Y4HSXzpSrAZXjqaYEQzwetUA/oee9IQYx3qkBKARjA5PrQBInXpzTETKAp6UAW1HHGM+lMZOgwRxSHYlCgGgBy57cUASAcH19aBjscDPWgB4XkUhkqrjoOaBkmDkHApAPkYyuXK4PtSATFAC456UxjGHOaQBgA+9ADSOtArDSvoKAG4496YDTxmkIjIwaYDWHfNIQ3JUEqeDximMYfTHNACEE4OKYiM9eetIYZNAiMjJOTQAhOUPPNADCM9TzQAgBOcD60ARknmgQ3rEcDvzSARmLYI4NAxpIPU5+lAChtkqkcAHPFADZcOXdcgMeM0ANVSQSvXvmkBbm0i9g02PU5YSLOdtqyA55pAUME/TPBJoAcFY5X7z+gpgOlidHWCXC5O4HOaQyFxgnkHnAIpAODY4CYJ9TQA1QOd4zzQBPi3SVXj/eKD9xx1/Kgdx0kE0yS3qWjLa7tpdR8oP9KQFIqxO4YPp9KYC8Bckc0hFo3+NIbTzax58zeJ8fOPakMpBSuRjI70AMOAozyaAHnbgFT849+tABKSzl2IUscsKAFMqizMP2cby2RIOuKAIDlVAPI7GgBoOOCMmkAhxyCee1Fhic4zu57UxFasyyzZxQTXIjuZzDE3V8Z5oGQyALIwDbkBIDetADTTEHcUxC96YCjpSAXnrQAgJzTELxQAHnGKBinkfTtQA5htwcEZHegQgHJpgL0FAhWCiNCDlj1pjENBIuPb8aaAXHPSmAd/agApCFzVALxSELgBv6+lCGLgBsHdQAuDnpxTEOHX7vPvQAo49vWqAeygHCHOaLCAAHimIkx09aYEqjI7A0wJVHQY4NAFtV/eccUDRZRcUhkqigY5QeaQh4X8KBoeqnb6imA9RjGakZLg9QBigY4DPYZoGPHA+YHFIBcfLkDBoAT0FACMuAaQgbyzHgD5/WmMjIGP6UCGsAOlADSvPWmA3BA9qQEZzk0xDDx1FIQhz97HFMZHnJzimA3GfrSARsg9KBDT9P1pgN7nPHFICL+H60AKy456UgGOMvkH5T+tMCM9MdDQAnOMdqAG9ep/SkITDDPAx9aYxmW+bHSkAhAKjjGKAEwSA3Qd8HrUiJWupzbrD5rmBTuEZ5ANAyF/nI6DHUetABuY528MP4gOTTGNLNkEkk9MGkAn+6vP1pANHU7hz2pgIVwPmNIBc/KDt+bOOKAL0Wq30GmTaaly4tJSGeM9CR3pDKMp47YHGR3oAaON21SaAGcq33jmgYuzByM59aQhoK555PfimITaqttIAz0NAx7AM4GdwA5OetICEffOD7UAMXILAGkAnPJz9KAAITuCjJxnNMYm7PJHApCK1SWLmkAfhxTASmIdxx60AHUmgAxgGgBx5C0AGOaYCfWgQvSgB3X0HHeiwEksryiMMFwgwMd6AI+e9Owhc+g5pgHpgc0DHfhQITnueKaAUDnGKBAeKBDuSOKYCY79aBijHPBzQJij6cU0ItW12bZLhfJSTzl2/NyV9xQMr44A/OmA8frQIAOuRTEOAHbtTAVRzQBKgGev0piHqMdaYixHjgEc0wLaDLg5oKLaKWzgc1Ix+0k89utAx6jmkxMdtA5FCBEmCcdMUFEi/SgCUBgc9qQ0PUHJNIYpGRzQApWgBCBQAhHGaAGYJHHWgQqxhkZvMCsOx70AQEcDPBoAQjBGOTQAjqQ1MCM/rSEMOfSgRHyCaYxDwCTSEG2AwszOwmzwuOtMZFjjrz3NAhmM/WgBh9KAG9OMUAIcY5oEMI9BQMCy7Cu35j1NAER+6OcD0oATbjoOPWkIZhtx55poYcnq1IQwhTxu5oGKem3oRQAgyf4sgdRSAYQGYkmgB6RtKRtcZHrQMbjDHKdOCc1IDT82T37UwEyvuTQADO7HWkAhBDH5evoaYDGLZAY5FSxivgLjnnk0BcmuDagQfZWYybf3m4fxegoArEcEj7xPPNDAQ4A560gJFBflSuR2PemBH8u98rxRcBuBuILd+MUgGt16Yx+tIB6IJJVR5FjB4JoAjkTZKU4ZR0I70ANDlPut+FAAFO0Yxu7igCpUlhQAv1oAXHtQIB1pgHAPWgBSaAA9qYC0ALj060xCc0AGKAHAZoAUihCAdaoBcUhiYpiHcYpCHAxiJgVO/PBpjG4wAKBDiuOTnnpTEC56igZKFi8gyGQiYn7uKAIxx2yfWmIUdeaBDhgGmAu3uOKYCg4470ALj5iOlBJJgY61Qx4xgcfjTBksf0JNBJYiyaYFqLryKRRbC7c4zk0iiTAyOxpASBRSEPHA6cUxjwucUDJVHIoGSKmc5yfSpGOVcg44oAeAMeppAIV5z2oAQjngUxjWzyKBDSAaBDCvtQA0D6Z96AGEtyO30oAYeRg80wGEelAhGUiPzM8k4xQBC2D9aAGng49aBCEH1/HFAyNuRnt3oEIxKngfSgBnc4HNADTk0AN60AN6GiwhhPPPSgYm3PHQEjmkA+6ijiuDFFMJowM59aAKxwcnNIQrYHJTHvTGAKFznnI4I7UAR4G4+/SkABTk4H40AMPX5ucdqAA4ySATn0oAAMjBbikMZgYPPSkBMUPkiQNljxtpgQ9csAc0gEGckFvwpXAT5sHjj3NAwAOMgZz+lAWEAYHDc0ANICnrj+tIB4UeYRjoMnPpQMj6A85J5XFAhCfl+Zuh6UAPhkjRZA0ZO4YU+h9aAIMYXjnNIBuB06mgAwB260DG46igQu0AYwc+ooAq1BYtMBKBDsll69KYwHIoESRSIm7dHuyMD2oAYOPxpgLigBMUCFwM00MUDPXtSYg79aBD1VNjEt8w6CmMQUCAdaYCjrigVw6dKAHuhjAOQQaYCD60AKBng8DvTAVwuR5bFsetACYz2xQIXjPShAKOp60wHHp0xTQgA65oEKAO9NAOC/LkYoAcDz0zTFYcBzzTAlVSSF7FhzTGW7i3e1uTbuyuy85U5FAh0Y54HNMC5EpJz6UDLSjPU/SpGSjnoPzpDsPVe9ADsZBoGiXZtVSD19KQD1Uc+tA0TAdOenakA7AJJ6CkMcq/P7dzQA0rhmPbNMAwTSAYeD05pgREcn0oAawzQIaQDQA0g7T60wIsUhMaccigRG3HPQUyhGHPTn1oER8nPOaYDMUANOc4HSkISRg7KQMYFAEeSc44I/WgBHHRgevWgBmM89zQA3PY8mgBp44IoENIwBxn0NIBh4GdoFAC5xg8Z96AEZWLsCQxPfNAwSNmPUFV+9igCIkFiVHAPFIBSCecdP1oATcu04T5c84PWgCxePaXM8TWMBgAQBkJzuOOTSGVG6HgLj070gG/Kdoz164oEBxu+7gDpg0FCHnnOKAHA5xtwD70gEVC5I4LH06Uho3rTwtqV5pE+o21tI9pF9+QDgVNyrGHIjB8DGM9ScU7iaGBGDfP8AMp4znpVEktu0KXcLXMRa3Vhvx1YUDG3KxS6hN9gXEBYmNW64pAVpE8tiCpyenoaCSPLDrmkMacdfSmMX5SMq3PvQIbnPzY6UgHqYPIbIbzSflz0oGRDlcA9etICt1pFDzJmHy9g65zQMZ/KgQuOlMBe+KBC0ALxTAKAAGgQdTx1oGaNhY211b3Us96tvJCm6NGB/eH0FAGf2Jx9eaYg/CgBaYCgc0CDvQFhcdqYhR7E8UAAFAhcDrmmAACmMAMUCY/jGaQEimAQOGVhPn5T2xTAaRlR3J5NNCDoTigQoxk8UCF4xxxVIZIB3A4oBjl5+lMQ9RntnFMCwmM4PJPqaYFiJcGgC5AowaRRbXntipAkC5bNAxwHGc0DJQN3T8aB2HgfMOKQEgUdutIaJtvII60XGOGM0gDbyT2oATHGO1MBCMUhDCOuKYEZzyCOKQmMJ9qBDGAPWmMY3FMBjUCGsBj1oAbt4KscZI5HNAEt9apbyxrHOs6SKGyOxpAUj3x2piGkZ9zQAjgrwQNx6GgCMnHWgASJ5X2r6ZoAhOPmHTmmANk/h2pAMPHQfpSFcbyckA0wGZboRQA1j7UgExyTsOPWgBpHc0AB3E5Ax/WgYgyc56UgGbRyaADOTwMkGgBQdrEP3GAaQxm0bmHUg9PWgBC5AZCvFAB1AJPTtSYxpwT0x70gF27pO5x0AFAx6th8ISuDyM8GkCOisvGGqadok+lW9xttpz86AdaixVznpZPMY7wC2fyqkhNkH8RyN1MQjMWGckAGgBpOc4z67jQA0MW5LbuOPagQwZPGaTATKg4IyfrQA8BBvWRSxz8pXH6UDInOThQcD1oEGSR70FCYx1DZNIRW+lIochCn5kDUAGATwKdgE6UCFxQAtABimAuR6UCYo9e1ABnNAB97gDn3oAD147daYDv50xCd6AFA5NAC8Z4oAMYNMQ4Z5IoAQZP1pgL060CHYx0FAxOehpiHdD7UIQ5c4JBye9MBD2J60AOwRggUwFB55/SgQoGCflNMRIM+pHtTAcASKYiRAaYydBzQBahXB5OaALsHPOMUhotKpPakUSAN0PIpMBwHAGKQE209iBTKQ9QPTmkNkijDc9KQIlAwcbSTSAXGDQAo4yMUDG44oEMPoaBDWBx0xQMYRnOBk0AR9T05piGN3B6UANcbeq8HpQIiOMjPSmA0k5I25oAjIwOeTQIaRkcDB9aBjDnvQIjIGSKAEPIxk5FADfbv3oEM5VvvHd7UDGt1waYEePm96kQpLD0xTAjYcE8j2zTAacH+Ln2pAJnA56+lIBcr9mK5O4ngdqBkW0YwT+FIQ3GW4pjG7sdRSsAELj1NABtx8wPzdgKLjFdfLZhsyQcHPXNIC0kVh/ZU00txKL4OBHHs4I9c0AUHX5Q3UseaAEKlScDj3pACEMDnBPvSAmtbqazdnj25ZSpB5wD6UFEOfmO8Zyc5pCHNuDnb93sMUDIsZyTnJoAMDGRuLD34oHc0bHRJ71pDHPFHiEzHcwyQO31oAz7swCYrAS0ZxhsYNICsR8xGcDtQIGwO9BI7LY4IyO3rTGT2v2NvPa7aRZAn7rH96kMqE5A3nIz1zQAckZC4PrmgBo3YJ5yaQFcUhiUxigYOaBB3oAUUALmgQqnFMAGaYACTQAAUAPRyj5ABoEIeecYJNAxSMc96YBzigQvUGgCaeVZViVIFj2LhiD94+tAEP4c0xCnn2poBTyeKAFIHc4HegQ5whciPlOtMBBgn0oEHGemaaAXjbx1oAd146mgB7rsVT1cnpTAfGqPIFkcohPJA6e9MQ+dY1naOKQtCDwx70AMxzzTEPxTETRj5TTAmjFMZdh54GaBouwKeRSGWUU9+lSMkVe2OKTAeB7Uhkm0ZFA0SBelIolC/jSAsPgqiqm0jr70AIAT2ANIBAMk5piI8DByaAHeXAYj5jlZQwCjHGO5oAbeRxxTtHG4lTIww70AQy+WxXZ8p/iFAEYJOcHimIhYYFACOWIGegpiIzuwcYxQA2QAplSd565oAiJx259aAGPgAUCGnHbrQMYeXbNMREQOaQAV6EUwGAZPXIFADGHX1FIBZEZEVjjDdMUARFevGaBDGAA60ALgdQwz6YoAYy4cgdD3NADdxPGAR64pDGHnr96kIQncQCCOaBikNk4AFAEe5uRwfUCgAYbcbR831pAOZmeRWxhh1PrTAbuLZGAWz1xUjEjUllHXJ6E0DJb6zeymWKdg24bgynIxQIrYG7kHnoc0h2FA+YlvWlcYuQrEAA/wC9QAOpLqA4YAZyOKBkZYZyw59qBCEszswGPY96QCI2xty5A7/ORmgCMsScH5j6+1AEkSRNHN5j7WAygx1oAgBOzOM560CAAFiCeexNMBCSMELg0higEqeAaALF1YS2kEEszo0dwNy7GyR9fSgCnxgnkDtg0gK9BQvFAg5oAXFAEzW0qQLMwG1ugzQMiFAg70wDHNMQ4jpQApOenWgLCAZoEKe1AxQCeKYhSu3vk+1ACA56UxCnAoQBkelMQuMnNAwoEO/nQAo+vNMA2+3NAheO4x+NMBwUZ6jH1oAQYzmgQ7rz3pgOHTpzQIevHGBg9qpDFUYGOtAhw96YiZOBTAsIOc0xl23AZsc4pDL1uoI9qQyyq8epFIZZg+zbZvPDbyPkI9akYirgDJzmgZIEpDJQmOT+FAx6jGc0gJgo/GkAm30H40wExk9c+tIBnrxxTAae/GfrSAjOQemf6UwI2HryaBDGXPOMCmAxsNigQwjJPpTAix1z0pCGEDNUBH070gGnryKBDGOGPrTGMJPPHNJiGNzx+dADSA2cDkUwEbAOfbpSAjxkkUAITk98ZoAj4UnP5UgE3DI4xTEJjljtBoGMPKkHJH1oARgcDPSpBDd3XbnPvQAmMud2TxQA3OR94kjoKAJFTcHYNggcD1pAQ/eQkjkn16UANIGeTyOlAx4bkFc7v7ppAR7ck5++T37UASiZ3dfMfeF+X5hnFBRGRGjuCNy54YUgNGyn0yG1uxe2sktw6bYWBwEPrUjMxnbYACOvGRzQBFtIZuOTTEIGxwTSAYeW5zjsaAEfnvTAQcHp1pMAIAPTmhAKeGAHegBzKRtdZA0mcYA6CgBHfzJSwXa3TFMRGwHU8HuPWkAc47gDsTQMaMHtxSArUihaYB1oEKQMUgHYLLn5tqn8qEMTBODVCAdaAF70CFHvTEGeenNMYA4oAU9qQhTz2pgGBTAOlAh2KEAdaYC9BjFAAvYY60xC8L2NAAOtAhw4PXrQAcDrng0xjiOMhcD+dAmA+9g9KYhwOKAFXrzTEOA5pgOAPOOlAEi/SmInQHHIzTAmRT/FTA0LZ2CshAw3WkUXIE+UUhltV6+lIY8A844HapGSKpNJjJFTBxtzSGSquR7igZKoxSAeFxSACnWmA1yRjjmgBjcjpTAYcUgIyvWmBHjqKRJGQB1NMBuwnkdKYEbAFuPxpgRE9fl4oEMI444oEJsBjLhvm9CKBkXO3IPNAhpJ9qYyPnJPegQwjd9R2pAXtL019TvDBHKkLMpOZDgcCgClcRLbzPHwdrFQyng0CK5yMnGDQMlaAmATCRTzgoOD9aQFfPy7lXgepoAZ1Y5BzTAVUch8cBeTSAhPIzQIQ8L6n0NIESzzCaKKLyVUp1Zf4qBlcc554oAb0Ygcg0gEICqfUd6YBhSRzxUsBM/MG4GOuT1oQwcr5jMScHo1AyMjGSTyeRzQIlVZGVmVlwOWFIohUthsYyTyMUCDeMjJ57H0pAIygyZ7UANyD1PIoAXJxkFfpigYwk855oERAbutAD/mA+7xQMaMZOTQAcKfvD2FIQ1gBgbeR1NMY9dpYZkI+goAQjazEDK9M+tIBgBy3U4oAOv8PA96AK1SUGRTABigQtADw8ixGMH5WPNAxuRQIfHt3/N0pgN7+2aBDunSmIMZ570xhSEBpjFzx70CDr1FMBzALjkHPTFACEd6BDgCRkEUwAetAB2PrQIkkVV2lH3M3WmA0Hn3oAAMkigRbsHh+2Il1I6WzHbKUGTigYy48oXEiwuXt1c7GPXHamIjGO3NMQo680wAd6BD+mOeKAJEUM4HY1QDwPmNMRNGvGDTETxgdM0DRfgXrQUjRgUBRSKLCqADUjHKvvxUjJwPQ0hkqD5qQEgTBNIZMqcZ4oAUqSBzSAQqMmmMaR2xQIjIxTAYwweBmkBE3Uk8UwGnOMjAoERsfamIY2eeePSgZFigQwkZ96YETDkg9KBDCASaYDOCSMc0ANbPUUARknPJoENOASM8mgQm4rgnO4nqDSGRsSGxjP1NADDgHBoAAqsrNgZHvSAjPK9MUABGecgY6juaYhokZFZUYgN1U96BkbY7HBHYUCE7kKMkikMb8w68+lIBEG4kY5oAYTgsCOvvQA3PryaQAp79CKAGbcg+poAlaVnhEOwYBzkDGfakURh0D5wce9AhhOzPqehzSGBJZ8bhmgCMkBm447UgF3Ar1I9qBjeDyBQIbwCfWgY3kk0CHqmckkbc8gdaQA4UyM0ZPH3c96YxmBn/AGvSkA7cVYoVBB5oARf4sqOT1NMAK7VOWyexFADSMKBg570hEkE8lsz7SMuu0gjPBoGRAcYK896TAq0hhQBIzL5YXy/m9aYxnTtQADmmIM47UALnPUUwFxzQIXvTAOlAh2RyCKAEyMccUwHDkDHWkABSegJbPSgA6Z4OfSmAA9u1AgOMCmgFI5FMQ7t9KQgA9BTGAPr1oAdjB45poQdOvNADgcEYHXrQAu0jgCqEAJ9c0CHgcYxzQIcM9CKpICVFG/584xxigYqjvTETrgYxzTAsxgHnFMZftl6Z70ho0YR0GOakoskEDOBg1IxyAbcc1IyYJ3pDJkQ5zjigZIoznjrSYEm3C4J5oAcy424PNIY0qMmmA3HWgRGw60ANkUqBg5J6imBA3pigRG+KAGtyMZpgROPmFAhjDnigCNs5OOBimBCSee9MQ3gNyKAIzwxNADCAKQhPpzTGRE5Y8UCEbAwe9IBpHpnPpimAzqc4470gEKfugccmgCNh0xSARsAjA5pgIS2ckDj3oAiPc96AFPIyDyO1IBvfjj1oAaDgMe5oAZk460gAH86QDCMkk0DEI+UGgBTuODyB9aQxuQTzyo9aBDXGWyOlIYzjB9aBATyDjApDEPUkg4z1oATqQQPpzQA0nJJIwT70DEB46UEgQMZx9aBhgHGOD6UDHt8rrggHHShAM5YnI5oAQKMkMaQhy7gRtGCD3NAyS7upbuZZZdm4DGFGOn0oAiyCCRyT2oAbtI7HJ96AKlIYtAC9s0AApgL0pALnpkc+tNAAxnv160AFCELjFMBe9MQd6ACncB/8QNIB0MjQTLKmNynPIpgDsZJHkYAOxzQAmM9cUxCcnr0oAXuKYhQOtAhevemAoHPHPrQMQDk0wHL6YpCFIx9aBDsE4OTVASxW8ku9lIAQZPPUUANAyMrn8aYhV4zmmIcoHv8AhQFyZRnBFUBYTPpxQBPEM5z1pgaVsPmHFIpGjGpGTjrUMomCDA7GkMlQEipGTKnBzzSGSBcEUhkwXpk80DJNoA6ZpADDGKAEYD8aAI2BHHT+tAiNhjtTAiZRTAYfSgCJjk4I4oERsBmmBG55oAjPXnpQIjI6EEmqActtK9tLcKBsQgNz60CKhIPOCSfegQ0kc8c0gGDB4xzTAa2T7YoAZmgBmcHpQITLDoaQwUxFz5wPPPFAELYZsj7oPAFACEjvwaAGMSTjaB+NADRkE8/nQAmRngZpCGY5NMYgX1pAM6E88UAKRxlTgfSkMjweT+tACZ67jUiGt94DHy0IoGBLfKO1MQnThuR6UgDIBxigZFt+YgmkAvbk59qQgLuIfLwNpOaBkZ5PXFACgZXKjcRQMaSSOBQITOeH4pgOIycDr2NIYgxnHegQpxjluRSHcM7sYIB9DQINuRuzkjpTGMHXmkA453fLigBgBwcnk0CK4x3pFCGgYuelAgxzQAd6AFHpTESLIUiePYCG79xQMZ0FMQ7qKBDgqlSc/MKBjRwBnrQIcBnFOwhO/PSmMXndTAMYNIQuAO9MA2+4oAUDIyKYgAoELjHNMB2PlJHU0wFGe3WkIkiWNi/mNjjjHrQhjB/OqEO7j0oEKBg4GfwNMB2MHBoAdg9qYh6k+tMRIqj6UxlkcqMc4pgWIuvSgEakA5pFo0IBgYqGUTFcAc80hkoTj0qRk6qMVIEoQHHNIZMB83TrQMfyOCKQCdc8UwGkZFCAjkJcqWoEQsMZpgRkYFMBj57nigCEkBiDQIYwPpimBE365oAjbqRzn0PFAhhBIxjA9jVAR5OCoJAJ5AoERFvbpSAac5PFAhhO05zzTAbIMMMd6AIzQDI89eKAGetAhpOKBh93nqfSkAwnDfd60wEYHBY8ntQIYQcZ7ikAro6xCQ8q3QUhkLAYoEBAGDnFAxMbiQoyT60gFkJCGPgqKYyAnIB6CpAXOR8vP1pCEYYXJxljTKFdEMa7WPm5xg+lIQ3GGJGM9MUARjlj/OgByRvISoxupDImUhmBAOKAGtxSADjqOTQBNBA09yIjIsQIzuY4AoGQNnkcYB69M0AOUjIxtB9zQAnVjkcjpzSAaOScjJ9qBCZ7MKAFKjdnbkfWmMRj8ihc4HWgABUHnnNIBSjHIxjjIoARdxGev1oAqDrzSGB7UDFPWmIQGgBTQAo6UxCiiwBTAXpSEAGTz1pjHY4xjpQIByKYAQPWgBcD1piFGSKAD60CAYz70AOY5xxj6UwAUWBgPpTRIGmMd1AoAXj0oEGKYxxxxTJHgckimAq96QhwGDmmgJURyGIX5R1NAD0G7n+dUBZA4HFMC5CDQM0bdR6mpKNGIZ9qllEpXgCoAnAGR3pFIsLGSMjAFIZKqjqBxSKJgMjOMUAKBnNIBuPWgQxh1pgRso8otnJ7CgRCw4pgRN6UAMbA60CJobqGO2uIZrdZGcfI/dTQBnEds55pgMcnnb1FMQtzM1w6M4QFRjK8ZxQBVPHUcmmBH0Y4PNMBjA+3PvSEREE5yKYDThjQIZkZwRQA0g0ARlj68igBrMAB60MQ1jnqOtAwO0Anbj0akBHjA9c0ANI2igGI2APrQIQ5IC8hfQ0hkZAzikAhOGwTTARzySep6EUDGHoMHJoAQ4289RUgITyRzg9MigQn3T93g980DG9+mT60mANjA45oAQjpj8qGAh4b5eDSGMPPBJPvQA3GDjqaQg+6SMHFAC8FSMKSf7x5H0oKQzAxjGB25oEGBtPFIAAUgbjz6UxkpB+XaRuPBWgBqIWZsDH1oAjycZ3c+gFACsc9Bgd6BD3mDrGhjGEPUDGaQxhyckE/SgB0SLIGLOFI+7u6ZoAo5pDA9RQApJPSmAuDQADH+TSAD0qkAooELg0CAH1pgOwp/ioGHtQIOnagQ73280wEywpgKckDHFACjpwMmgCcxxJbJKJd8rHlMdBQBDxj3oEFMQfhTAX60AOTZ827j0pgAyPegQ5Qc9qEAY5IPJFUA/qeDzQIcCB9aYMVec5oJJYyVyqsQD1oGSKPu5qgLQXofzpgW4F575pDRpQfeGBU3LRpxrxyDUsokA+fpSCxZQc9KllIlRQDz+lSBOoAGMUiiUrkAigBdtADccnvQBCe/FMRGwBbpQIhbjPtTAYwzggUAQsfbmmIjYkfjQMibgn0piImODQAxwdoHGRTAiPBoERH7xzTAjOOfWkIY3TOKYDC3PvQAw9TmgQwjqfSgBp65CnJ70ANbr70CEJwMHn0oBCu5MKxFAApzkHmkMhPI44HpQAwkcikITscfnTGWLizaCzgufOjkWUn5Fb5l+o7UgKh78dKAEJB4EZ5Hc0AMycgFQBQMQnPSgTEP3eOtSCJJ7h5o4IpFXbGMKQME/WgohOcnnI7UCEBJ5FIBhGSc9aAEAA+tAxrevekA0jHOaAFGO3NAxuGBJGaBAf19aQA5y4AHGKAEHJwxz7UhhHjcd3r1piA8uSeQe9ABk4++SRSGPAAhMgcb89PWgBnOzpyetMQLuzgnNIYhH8JGB7GgBoGc7eD2oAq0hinrg9aYBjmkAvFAhyGPnepORx7Uxjev0zQA7FAhCfeqAXbxxRcBePTNIRLEsTSMJWKjFMBmACTnjPFABjIpgAHsc0xCkDNIQuATjNMYDB7dPSgAPWmSLzigBTkiqAUH/Z/GgQ4A5BP54oGKFIXcQ2wng460CEweTTQhzHI6c+1MCVmRo0AXYw6n1oAQDPT71MBwHXNMkkXLAc8UDJIyOOM0wLYXAA6k0CLsIxjg5oKRpwDAGODUFmogGzgEnvmpKHJyc0hllUqRonjXFIomC88ikA/GSKQARzQA1ifSmBE2MHjmgCI8+1MkhfqR2pgRP8AMuB2PFMCJifWgCKQcj0oAjP1pgRHntxQSQNxzTAaT6daAI3PrgGmIaVQwl/MPmZ+7SAhJHG480wGnuaYDCfXpSERnue1AgODbH5juB6egoGR85IJBHrQIaOM85FADOn1oAaQBnB5oAU52hgR9KQWGHJ/hAH1oAbkZ3AYP1pANYAv15NAxDzkHJH1oAbg4z2BoABG213Ayg70gI+qk9PrQAEg4+QAUhjSMgjdn0oAbgcc4PtQMXB9OakQzqxz1pjGnJODzSEKSFI+XNAxCSSCBjHUUwGqV5x1pAIRnk0XAOBjPNIBT2I4NAEkXAkZ1zxgfWgCEYI+cH8KAHMM9O/tSGIADkY49qYDSvrxQAucj7x47YpAM4x83WgB+0YGDQBU6VIxQckE0xid6Yg6UCF6UDJIkV5AruEU9zQA1gA7DO5QcA+tCAMZ470xDlA3D0pIQDPPHBqgD2xxQAD0pgO6cUhD8ReVuDnzB2NMBmMYyOaYhTwetAEkTIrMZI9wI45oAZx+GeKYAeWPtTEKc/8A66AFAyPemIUAkEYyKAJ3uJXt47d2DRIcgUDIj25GKBDxnoBTQhQpBGfzqgHKB82KYAooJJVAGKALCc9qoZYUZAIFIDQt87Sc4NBSNK34bnrUlGlGCB/OpKJIx14qGMsqvIB61I0WVj9KCkShcYqQHke3NAEkUCSmTdKEKjj39qAK5U5JzkDgUwImyaYEDDrQJkLHBxTEQMMUARsOMd6oRG1ICEjrTAhYk5GeM0xETEdMcUAMOOwpgyPPqKBDDznuaQyPr1pkkb5CjnmgBHwMevpQAhiba2MbR2zzQIjyjElt23OOKBjG3bR6fzoEMJwTQAmR1yCTQAw9yRxQAhAJHb1pDG8BulAAeG5X9aBDG5kyRQMaRzyaQDeB8vakAZI4VjtHvQMYTkc0CBgCoPJIPSkUJjGdo/DNAhBnJyvWgBnbljSAUnCAj72elADSPmIwdx7ikMQggHBJNMQbd3uaBiEdh94HoBSARsZ24O6kAdTwOfemAgGS2eaAFxx8pz6ikMQE87cYoAAcn5jnFADQOuD70CF3KSODSGDAb2yTgdKAAdcrj8TQA0/d5GSfWgCrikMKAF4pgL+NMABNITF/CgAHFAEsQi+cyZBx8v1pgRgcUCHYJHUnFMAzjigBeO3JpiA/e6UCFGM9KBhwOtMQvGOBg0xCgd+9AwAyeelAiWAQtPGJ9ywk/OV64oGOukt1u5FtXZ7cH5WPBpiZER+FAhfTmmIcvP0pjFGOmKBFyysnvJhEFLSOQEXHU1WwGvr/AIT1Dw6YV1OFoxKu5DnikncaMEg/M2Mf1pkscCO1MRKucdMmgCVB0JJpjLa425FAF+3BK0ikaluCSvTmpZaNHByCP0qSkTxgmpYFmNQe1SyiyiHNSNEoGRwvP1oGO4PakA0jPbpQIiYZ9qYEL+1MCBh3qhEJxzxQIgY9eM0IRE4xzmqGRnrmgRAx5NMCJjjtzQIY2T2oAiYng5wKAGHgngk0CImHB7UwGPnOM0CYwkDIxQIQ524wMHvQBF3zyTQA1z8uentQA08hRnIFADdwB5TNADSflOF2nPHPSiwDc/KR1oAkubcwRQuZEk80Z2qfu0hkBGQQvSmIYMjoSTSGIxww9aQgOSMgjilYYzvkL+tUAZ3dOB3qRjc4OKBCEHByM+9IY+5a3keP7PGYlCgOD3Pc0gIW6cH5RQAgwCetADcZPKkYoAUHY6nPPakMRh8/WmAgXOf0pMQ6ORo5BIi9KChrMS5LAbicnFAgwSDjqDwaQDfvfMT+FMY3PzE4wKQh3X7oyaBiHdzmgBFOPvZ59KQh7ADgn5D68UwHxztHbyxeSGVyOT1HpikMgwQAuM+9ADVYjPOce1AitSKF7dKAFxTELj2pjE5PSkAozQIAfamIXihDAcGmIcRx70CDHpQAvcZqgA/eoAXODQAZ5xnmgQuKYARgDFMBTxikIXFACjnimAgpiHdeKYhwH/16BigZz6UxFqzuXtnWSNm3qQVI6rTA1tZ8T6lrhiGoXBn2LsQt2FKwGPIAJMA5HJz7UxAvJJpgOUdcHvQIsR+mOtMC6gKp79qYy9ADtXPT2qS0atqATgc4NSykaCr0zUlIsRpzxUsouRpk8VAywqknBpFEwTHQUgExSCww554piIH4ycUxEMmNtMCu4z3piIGOOlMRE2SDxzQBCxNNCI5FZQDnOelMCBzigCN+nvTAiPORTEyM9CO31pElm7msH06zjggaO8Unzn7PzxQMzmA/E0CEbkg5GaYEZOSfagBmByQaYDD0J/OkIjYkjBFADWI3CgAbORjGKAI/4jRcBM5ByenYUgG8DkDBz60DGtjqetMQNnsQAe1IBh5z60AJgDvQMa3POelIAwM8Zz7ikA08DJ5oAQ7SM/pQAcEdenTmkMax+XIHBoAdKYikaxp8/wDF70gI8gn5gT9KAFzhxleB0xQkMYRkn39KAExt60hCkNnIHagY0EjqMmhjEZQBnJ5pAKfugEcjoaAJWjMaI+VIfoMUCGeW5BfIOD0oGMPHG2kIfE5ilVmVSB0zQMaGUO+6PIJznuKYhHPzAgHaOhJpDG9m4JWgABXGcfKPfmkIqewoKHE5RVx0pjDtQSGOeaAF2nH8qBks0DwBPNXaXGV57UARZ54piFFFxBwD1oAMc56CmMd0zihEhnimAuMUwuOyMCkInSeNbSSA26s7NkS55HtTAhxjAxkeuaYCGgQpH51QC4NIAHAJpgKD14oEHegQ/GAMHk0wF5OM9KYDgc9qBDlOOMUxijAPPei4hw4pgSdhximgLMfbnmmBcjXC0gL1uvy4x1qWUjeSWKa3t0SARyx8M4/jqGaIsqBwBSBFqNenpUMtFyNc9AakaLgXYQMckUih2MN9aQxCD2Az70hETZ5JqhFZ/TFMGQPyTmmIrvnJwKYiLaXdVHBbimIhnjMUpjY5x3FAFY8/SmIiJ9M0wI2yOvT1oERSjaASck96YEJ70xDCaQiNjwBnmgYw7s84piI8cn1oAbyetAiM5529BQMY5wo9aBDWPcnJoAaeWBHSgBBG7F9qMwUZJA6e9AEWN2ecmgQ6JofN/eplDwcUDRG20EnqCeDSAaw/GgBpA65xRcYu09R0PcjrQA3OcnFIBvYkflSAfLIzoqFAAPTvQMh4PTp6UAJ3xigQhAHagYvUAjoO1IBvU9eaQAST0zTAaTnIJz7UDBgMYFIA6Rg7eDSAbgEnP4UAOGTjGAfpQMa3CDIy2eaQhQVR1Eibh2yaYCtwdykFR0HpSGMXb2HNAhc4QgDnuaAEXGcMMn1oHcmhghYyGacDauRgdfakBAcMvyYAJyRQIebhjbG3GChbPTBzQMiUnABU8UAVc4pDDP5UwFJ5osAv86BC7fUZIoGKzu+CzlscDPagBApOcAkDrjtQIM9cUCFHv0pgOWMs6ocKGOMmmMfPF5Nw8JcNtP3h0oEN6UxMPxosIXBH0qgEAoAUAUCFFACEc0wHdO1AhDjHAxTGPwewpCYHnNNCHEZxVAKOtABkjOKEBIByCAee9NgOwc+9Ahy56Y5piJFHXP3qYE8Yzz3oGX0HyimMv24OKllGtZjI96hlo0UGccVBSLkSHGKhlIvgCRkYJtGOR6+9SNE6qeRSGPAFIBjqeWNAEDjIqgK78ZpiK0g+b60xFZxnOe1MRC77uABx+lMRA5wfr1zQBCeOlMCJ+/rTERMM9eeKBEJ+ufamBGT2xTEMP0pCI2GB0pgRlgTjvQAwjk880CE2793J3DsO9AxpXdG0nowGO1AELcseDnvQIaTwfWgBp+Yf0zQBLBd3NqJRBJ5YlXY49RQBVzkdOQaBAVfJ7ADnikMYTzjHyigBC3tzQAn3WKjB9zSARiW29sdBmmMaSOh6jsKQCHhgDwtACMNzMQDx0FIY3ABzn8KAA5JPY0ANyR1NACEYAxxSAOFYY6+tIB5hJUv5g69KAI9rEZGOe1ACMQMjHP1oGJ/AOeAaBDsYbK4JpAhoUlnODkUDG5PAbn0pAOxsAbGGzyTTGhDgOeCe4pAIB8xJoENwOeaAHYyAFI3d/eiwDT8xwFGB2FADRg9R+VIB7EBge3YUFDedxx940CKoPoKBhk96ADvQAucGmAvvQIAM0ATxXEsCyLGRtkGGzSQEPt370wFAPSgkPY5NMaHqVVvmGRjpmgBOMkjp2poQAfjTEA60wHcevNICTYnkeYH+fP3aYDP50hCojO+xRkmqAVhsO0r831oEHbmgLj9sZgLF/wB4DwKYDe2KYC45FADuM0xC9M5oETK7CJkwp3fnTGIvOTjn0piFUUxEqcdaAJ0G09eaYGhFynNIZft1+WpZaNzTY4ZN3msV4yuBnmoZaLyADjjGe1QUXI16VLKRdRRxipKLAHIpDHFeTkUgInHyn0pgQPxgUCKzjk1QitJ94mmIryY7daYiF8mmIrseenFMREeGNMCBxnJ96BERwOtAEZ9uKYEZyc/pTEQnvk0CIzg98UANPXpQBGTjdmgQffPpgdPWgZFyQAMgE9KAEc/OeCD2FAhnJ5AA9eaAI+pNAhp460ADkKMc/N1oGOuAo8vZKXO35s9vagCHcCcj7x7UgEz8xPegYnUHP4UCGED8aAQvHbrQMax2+5PSkA6XynjDKW84/e3UgIs7T93j1FAAxLEsODQMQDnKjd60gGkjn1oEOPMYXIyD+dAxm0Z45NACbRzxz7UgDAzgA570DEx8hGRnNIAI+U4HHY0CEJGw4yM9vWgYvA+UY59aQgIIX5hk9jQNFi5ezeO3W1jeN1X96Sc7jQMrHJ9B6j1pCG4AYD+GmAFTubC89qBifNnnGfQUCFbdkYGCfWkMRRwcjPvQImtLuWzl82JFYkFcOu4YoGZ3SkMXAxx1qgDrnNIBx2FRtBz3pgJ+FAgHWgQvSgYuOM5H0FAEzw+XDHJ5isXP3AeRQBFjj3piDJzTEAOOvSgBSfSmAufagkXPY0wAAHHGKBh1JoEOHByM8e9ACgdc55oAQ9RxTEOHJ6CmAoyOvSgQo+YH0pgLtI7ZHrTAdnkAjmmIULzQA4DHB6UwHfypgTJjtye9AE8fPWmI0IB8lAzRthxUstGxZKSB7Vmy0aUa5zgVBZfhU55HFQxouRqEP3RzUlE4Xj3pDEIJU88UhEL4GRTAgkHANNCKr5yaoCu5GaoRWfvxQK5XcAc1QiFuhpgV2PJzQIic9QDQIjY8jigCE9TnmmAwj8qYiLdz7UCGEYBIxQA1t2chcUARbgOooAjbjOeSelADX+8OwpgIzHPTmkIZ1570wG5yOuKQho5HQmgdhpJB9WHagBM/MMgZoGJyCQOTSEMbkZPegBD8vekAHHbrQMTqx45FMBvPNIBBz9aQCYOSCTQA3CqcHvSGPjkeNmK9xjFADOnBHOaAG9SSetADj6he1ICPGRnOM0APAB+7ksO1AE1o9ok0n2yNmUodu04w2OKQysBlcA9T0oEG0DJUHikMHxgAjnvTEGVGCPm+vagYcn7xzmkAwL1wcUAKCCDhcnvQMnkEc/lCMiNx8pX/AOvQBBtO5scY4470AN6tnB46DNK4huPU9O1Ax3B6ZFAipikUGKYCjjpQA7B/GmIOnXFAw70AFBIuMH60AGPbGKAFOCRVIBc+1MQAg9BzQAdOD3oAkkKMy+VkDHINMQ0EelAhSDnHemAvJagQADkc/hSsNGtNrAl8Ow6QbKENFKX+0KPnbPY0DMo8HHU0xDsd+BTEIOaYmLjmgRIPQMRVIYg/OgQ4daQh42mqAdnA6Uxk45CYH1NMRPEM84oA0LfkHikNGhbL8hqWWjasl+Uc1DNEakaEfSsmWi/EnHvUsZbRenFSUShMH2pAIRycUwK8nBOelMRXfJ78UxFVu9MRWlGMmqEV3yevSmBWbHIx+FAiFyRz2pgQtnHvTEQucCmBEx+UY60CI2zk5xj2oERZPbgdqYERPXjnvSEMPBPFMBhAOetADTyMgqB70CIicHGM/WgBuRnFMBDnJ6YPSkIjJ5wxz6UxiHnAFIQ0nBYA8+lACtu4cbcnrzyaYyPrnA5pAPREkjkdpQrr91SOtICLDMBnpQAgIzgcmkAncg0xgwbGeMD0NIBhYEZHFAAQPLDAck0gG/xfNz7dqBgP4lGCfQUgGEHryKAFPbH3qAuS2y2rSt9qd1QLwVGST6UgIsboyAcruz9BQA3hhkcfWgLC8bixDYPRqAGZI7Y5/OgBx456UmMGG6JSGBJPKikA3gMQck9uaAFG4HG35T2NAEY3HJH5UAA5UnqRQFhc7s7fmz1oCw0cHlQaQwZSfb8aBBkYGTk9hQMRmHGMLnqKEAbeAMZJ6mmwKo96QwHBpgOI4pCACmAcYoGBz+FADsigkMDPI4oAc/ll/wB3kLjoaYDe9AhxI44poAHB6UwAcGgBf4uWoAdgnoMUCEA+aqEGKAH9Dx1pgAGD+tIAA60xBjFAh3A780xi8cetAhwHIwetMBxUrg5z9KdhCg+2KAHLgk8/jTAeMnrTESrwAR1oAu+SU2NuD7+RjtTAu22CD1pFGjaghcVLKRuWSfJyM5rNmiNWKMDr+FZM0LyKMjHepGXFQZz6Uhj9vPPSkA1oyQzHhR+tAFOTBU9qoRXkpiKz96YirKcg+lVYRWYZz60xBFbNceaVcJsTdg9/agDPY7vbmmIiYegpgQvwOcCmIhc4AyMHtQIjfAxzzTAYw4HsaQhs8ckDBZF27hkfSmBCfemBGcZNIQisBvLJkEUARZ/z6UAMzyc0CGHhaYwJHbk+lIBvHmCmA3o7HrQFhrYHegBGPA5pCAjjIApDGHk5FAxTu5OBtpCGdc980wEHfIP4UhiEgg/w+x70ALKFwgT8frSAaSB1Gc9qAHs9v9lVAhWcNy3tSAiOCQG6D9aAAnL+hPSgBcMe/Tt60hobtzk4/AUABIwD0PTHrQBZmv5prGC0eNPLjJIYKAT9TQBVP3R2A9qQCEAnaBlge9IYjEAjAwBTEOzlSNi5z1zzQMYBuznOQOOaAEwW9vQUhBgFCdvsaBo0dUv7S+jsRbaetm0EQSVlP+sYfxGkMzCNvHc0EiEAY9aYC5Gfl5agY3GSf60AAHoaQFXmkULxTACOlMBRQIXHFIBR93HfNMBeelMTA+9AB1pgL0bFABjJNCEAz07UwHD0xxSESxSrHHIhhVywwGJ6fSgZHj5QB175qhCjnPFMQDvSEOwRgjFUAg65psB2fTrUgAB5GMnvTCw7GOMUxAODg0AOHPXpTQhQPzpgOxxzzmgRM8nmRonlBdvUjvTGJ0piJ1CGPj75POfSmBYiHzDAOfrQM07UZzwopMZo2qnbzUstG/YxjZWTNEa8Ay3Ss2aJF6Ic8rUMCwq9c96kofsDJknp0HrTAhkJ246DPAoEU5apCK7nt7UxFRx1qhFaTIGMcUxMrPyTTEVmIz3564oAgc9sdaYiF+hpiIm9utAhksjSbQ4Hy9qYEHbp/wDWoAibkHuaBBNNJKV8xixUYHtTAgPHSgBjcjpz6UCGylRtCoQe5zQBGTnkcUwGbhkjH40ANOefSkIU7vJ38cmgZEW745NMQ4hipIAqQI2HP3STVDGDOSCMgUrgGBuOePagYHBB7UmIV/8AUIA4Jz09KQyMtnHNArBnrt59aBjR1JNACpHJJkIhbaM8UgI8kg4XPPrQAuSRgmkADHpk0DEOCzcH0Ge1ACBep3Zx1FIYYYsccCgQAZ3ZPI70AITlTuPFACkBV6Eg9KALVxdQTaZbWy2aRTRMd069ZB70hlNxtypBOTQIQAc+oouMZgAZ9T2ouAEAN3P0oEBPHy9T+lKwBnKldxoGABB9vegBmeuaBDyuQuOvegYwnu3JHakArNkj5SPUCgCCVEUrsk3lhzxjFIsZ19qYgApgKaAF7e9AgxQA7AP1piE3dBjmmAuQD70AKepNAhRzQAY9KYAOM0hDsY96aAMfNVCAHnigBQPzoABTEOGPWi4C0gHxgFXOecUwGjOPc1QhynHagAx60wHcYoEL6U0IkxyMUwHrwcUxEqj1OTQMtRDv3oA1LVMIRjNJlI07NcqKhlo6KyHy8LWTNUbESehFZs0RdjXOMVLGTgYqR2GuuBigCu/FNElOQcmqQFV+9USVXIG4dqYitKUMTD+PPH0piKrHHTp6UxEDcE0wK7H2oEQvTAjbkHFMRAeuaBEbH/aNMRE3AJoARyNmFJ3E85oAiYnHJoAjf7w9KBDT/kCgCM+5IxQIaxIBI7cYNMYmMHG4e+DwKQCNkNxgKPSgBgP5UWENIAJzxQAYYr8uODxzQMmaxuRYG/MP+iltm8HvSArDHBHT3oAaO/cfSgBCM8gY9BSAQgEg4J/pQMDknnigBAc5z0oYArMmdjld3XHcUgG5/vHvQADKnG08d6QC4LFiQM9jTAaT2c/lSAVgcgqAMj1pAG0/eK5b60DLz6W6aONT+1QMjSbDCG+cfhQBnMNw44oEAOMYGc/higY5YW3GMjcx6bTxSGJMhimKMCWUkE0AMwFY5zzSENbgYPCk8UDJZfs2Y/s+/OPn3f0pgRbsk/Lg/WlcBmMA5PNACkALnPNACMB0HJ9qBDmGxW3Ahmxj6UhjCdp45z3oATkdc5NAEKlP41J+hpFiHsO1MAoJDtQA51CsB6jNMYUEhjg0wCmAo+lMQCgBR3qRCkcjuaaGX9J03+1tVhsjcR23mnHmSHCigZBeQG0u5rYsrtE5QOnRselMRDwKZI49AQRn0pgBAyMfeJpAKwKMVYfNTEA+lMVhfrTAOPw9qQxSB1piHDpxQAYOPemIsXFu1s6K5Vtw3ZU9KAI+tUgJNvyBhnJpiFUDPJpgTx8kjBxSAtRAdNtUM1bRcRnvUjNezQ8cVDNEdJZJ8tYs1RqwxgGsmaF1E59qkZMFGOAfxpARSDBpgV36n1oEyjJwTVEsrPzz2qiSnI3U4qgKjnHPemIgk/hx+NMRVY8n1pgRtk/TvQSyBjjIxxTAhk4AApgQtgigkjbAHWmMj9RQIjb1I5oAYelADGPT1osIaeM84oAlEEZsJbr7WqyhseSRyfekBTdjg7gSx6+9UIfM0LJGsa7XHDj1pDIeG6cDuKAG469qAEGPrQAE4GO9IQ7zZBD5AlfyWO4pnjPrQMjJDEkg8GgByXMqQyxJjy5OGBH+cUgFltJoLWKeRMRSHCNuzzQMYwi8j5STKx+YHjH0oAiwOpP/ANagBP4SMGkAmSCPQGgQ4so4CnJ9aBiFiqMMZLdaAGbcjGelIBcHHAoGIRuzkUAHGMZwR6UABA6kY9MGkAjfeBoAC55UqCDTQChmCleCD1zSAaGJBzlvXNICW2nhhWRZYBIzLhT/AHfegZX5GT1GfSgBeo+9zSATOR94fjQAzK5waAEJJY9xQIUHjHegBS7E4cZOOPakUNHAIxxQITJJ4PIoAr4xSKDFMBelIQ8iLZ8pO/8ASmMbx9aYgAxTAXrQIQjFMBcGgBwpCDvTAUHkjtTAXOAM5z29qQAMdOTTQgHFMQfhTAcR0wOaAHMxdgW9KBAPamMBnNMljs44IpAKMf3aoBM0CY/g0Ahw59SfrQA4K2OelUhDhnAB+7TAePYc0wL1itmwuDdyOjhP3WB1agCSEcc9W96ANa1X5GHrSKRtWS8LWcjVHSWUfy1gzZGtCue1ZssuKmPvDipAmlEbODHwvegCs4xQBUkHU1QilKfmqiWUpPuke9UIpuetMRWlb2piKz4FMRC/AJHWmIgb9aYELHqO9MCCTOBQIiOMUEsY2cdqAGMVKkfxk0wIiV+ntQBG3I6UAMJBxjNMTGE9iKQhhOTyKBjSecEcUCEy3XAGKBiE5GRxSENOO3JpghSrEnbgeooGR5wzYXIoELzwMEJ9aQE0S2LWc7SyyJcgjy0Ayp9cmgZWILKM9e/vSAUMTwC2AeATwKBjCSRnvQAnAH1pCJWNn9gVVR1vfM5Yn5StAyAkAf7RoELyQcnOPWgBgyKBjshGAHWkAEZDMD81AxpxjljmgBcHAK4x9aAEkwXHakAmTk4IoATGTzycdaAGqOpBoABkcd6QCDIBx1oGTSW00KLJLHIkMn3ZCODSAr44OOB6+tACEYAoEOO0/wAOaBgflUfr7UANAGTzz60CDaR1/OkMYR1yaBgFHr+VAiDvQULQIBRYAHpTAADQAv400AYx05pALkUIQvUUwHmJlTccFT6UANzmmIcgB4Y4/CmAY29fwoEIBzTAdn2oELj16UwFUAAn8qAE64J70AOPT09qBDvlCsP4s8GmAg+7TEKM0IBwpgKOtMQ4c80IB+3CBuhNUAoHTNMkkAoETR9iOMUDLsOfwpjRrWYODxUMtG9Yx/drKRtE6ezTIHFYyNUa0SYNZsosquakY4qTjNFxFWTgtxwKYFObkeg+tMRRm4BPfNWhMoyk5NUSynIfQc0ySu5PcCmIqueSCKYED89eKoCGTGetAiPDMcDANMRXk4cqfvCgRCwweeM0wIvXnNAhhIoATpyuM9+aLAREkg+lMQ0kmPhhwaQEZJxkAfnTATn05oAZu55YD1pAG3k8HHsKAGMGI9B7UCEJwfmH5UANP3ic84oGISf7uM96AGjHIakIUfNjBHHagYzGck5oAXqRQA1uoHakMQcZFIkUEk88+ooKG44OfzoACML1xSAAcHjr70wAHcWyMmgBgHUHrSAUbRgE5NA0IcKcH14oAD22gnvmkAhGQcgA9iKEA0tkEFs46UABIz6GkA4Dv+poAZgnJ70gRYe7uZ4fIaaR4ugQjgc5x7UDIpmSXBQbSxwVH9KAGfdbHX2oAQKzFgSQTzxTENOcYIJNAx7vEIQqghyfmNICPILcnjtQBJE0SzDz0JTuBSGRkD5tnOScfSgRWxikUHrTAKBCnoPWgYucdqYgoAduYAjtQAmMCmIXHFAAPrxTAUZ70CFxnqaYAOOCKAF4DCgQvUmmIQUwFPFADux/PNAE8gtlggMDsZm/1gI4oAhPAximIU+opiHY9OtCEOKgYxyTVWAF7560AO29eOKaQh2egxxTGSDmgkeuO5qkBKgwBQMvRLkigEbNkoIwe9QzRHQaeo4rGTNonV2iYUcVhI1RpRxgdagosBMUgBxSApy8ZpiKMg6giqQihKPmPHFUhGfKTk+lWSVZOnB5qiSq+aYiBm5OetAFZm5OelMQ1Y5JX2RIWbsBTEVpNwZl5D5wc9qAI2YljnBzTAgIY/KOWzTENnjlgmMUqFJB1FIRCc55FMCPIyeKYDGwB7UriGNgoOQBmgBjc9KYCNjrmkIMkjKkD2NMC5ZanLp6XKQxQuLmPy2EgB49RnpSAzyMgAcZ6j1oAaMZxjgUDGHknigQpyRu9KQxvAz2P+1xQAEYyT1pgNPIHoKQAQOufyoATqc47UhjBwT60hB6+tAAchdoGc0MAb5TtxSAVvmIOaYxPm7nGKAE3DPTg0gFGAShHGKAuWbK7FlO8ptIpgyFdsnQZHWpGVG5Dn7pYk4oENKhfr9aBj2Xa4BTOecg80wGActlSSeoPakAmOdrDA9jQArgGTI+6OhoAaMsTu6elAxNw39MEUANPBOTyTQIUgeuD60AJjgkcep9aBjRk+5pAKuBxg5oEIeDz1pFCAdgaAIOv0pDA4HTmmAZoEBoAceaYB9OtAD9q+VvDfPnpQA3+dMQelAC/SgA5piHZGOlCAcPUDgUwG98mmA72NIQd+lAgpiHE+1MBQeRxTGAGDimIUA8g0hC9uKaAXHSquIePbrQAo9+RTEPHv0pgPGeoHFAiROtUImTjrTAuwjODu5pFI27FMsD0qJGiOl05c84rnmbxOutIsWykH5s8jPasGaIvRp19allE4UDrzUgMcYPSmIoTcsaaAz5u9WhGfNuGcn5c9zVElGQgZznHtVElWcR5HlvnPUEdKoRUfBBINMRA5AJwCT70AQMcHnnNUSReY0bhlch+xFAyF23tluXycmmhFY5ANAiNugPT3oEMkkeR9zsXYjqaAITkdaYDCc5z0oAax2jpj0oEIzKYhHtCtnJI70ARkn8PpQA3IPA60CEIJZsdaYxuTn5jmkIaR82O9ACHntSAOQQStMBhyMndznNIY+ad5thkVcDjI4zQBCcnqeKAF4BFIQnIyQOO1MaEJLc9DQMCfQ4NIQhHJPcUWGJuP1xQIUqN5B7ikAfdHPJoGIV3DIb5vrQAAE/MoyaQDQeT8pP40wAjJ56VIDW+9ntQA4nsG6+1ADSCcFjzQMaN3qaQBgcgqTTAafTsKQCuFwDu/KgaFCtxlcgjigBpPbqaQhCBx3pjFKnG3HU8UAS3Vs9rOIZGUnGcqeKQEPPPcfWgBhPHJz6UgHDBAAP1oArUihBTAXGKAA9aYC4oEL0oEGPamMU+p60CF7e1AEjujxIoj2MOrDvQMaozyQceooEA6njimhMAKYheMHjNAE6G1FrJ5iP9oJ+RgeMUDIsr0YZ9KYhO3TimIdkDjFAB0xTEOx+JFAiRPs5hctuE2fl9KBjegGf0piAdfamhD+nQGqAVTg0Ej88c0wJnKts2jBxznvTQxynP1pkksfXmgRdgHegtG9YAHkCokaxOq0tDwPeuaZ0ROrt48KABWDNDRRc4qWBIRgcUgIZO+elAGdOTuNUhGdcEYNWhGdPyKokoyHPeqJKch/IVQitIRj19qYiGZ1dEAGGHXPemIrHHIFMCA5GaBELdT700BC3BPrQIZJkgDGMdaYiInd1oAjOPqfSgQ6AwfaV+058r+LFAyF8GRyn3dxwPagRESfWgBGIIz6dKAEPHbNAiMDOfWmMXpyBmkIWOVoZRIAGI7GgZGxJJOMFjmgQ3LD+KgY0E88cUhATg8jAoGIT85x0oAbjk+tIQcgctxTGIw9OlK4CHH8IOaBBweWBz9aLjHOh6Y2ikApGzY7LjcOM0AMA6g80ANxtoGKSGx2NAAM4I7+tIBucfe6UgFzkjbj8aYxhGWbPWkIXouQcmgY5kAgWUNlmOCvce9IBnzBSMnPemAnGeR165pAKw2/KME5zn0osA+PEiFJHAC5YDHU0DIeQuTzk8GgBoPBBXmgkUgnqOexpFCbuT1P1NACAH8KAE9jSAXjHPegCtQUL2460DEyTQIXrQIdj5aYAPegQoFMBKYDs0AGB3NICXe4ttmBtY9e9ADO2KoQAUCF6UCHD9adhiDOaAHKp+YhSQOuKYCcHoOKCRcd6YC9aYhQeaBjhn8KCWL2zjNNAPZg20DjA6VQCqefegTHj0NNCJM9CaYyUZP0HXimIljx2BJoEXocnHHFBSOg0xd3GKzkaxOu0iIEAnrmuaZ0ROrtl4HHWsGaF4R4NRcYrLgUwK0vSgRnTnk1QjNnzzVIRnTH1q0SzOl6nmrRJVkYgEY5piZVdsdqYiB26kdqYiu7cZpgQOeKYiLOCR7UARNnmhEkB9aYDG9aAGnIGcUCGEnt170AMPXGeaAG+vBOPSgQ3KhlDD5e+PSgAkKNMzRjav8IoAj4zwDmgBBnJ5/CgBD15oADjHHWkMa24DJwMelAEkkKpbLL5ilnOPL7igCE7eQBlvQ0AJ0UHoQaQErRR+QJI5syluUNAEJGF4Ye4pgIQFZVOQpxjmkCJZ1CT7IpN6gbs+tAESc7vkyKAGN0+Y5oAeWZwBglV9aQxhOSD0HcUCHB8duPegY3AyT1z0oABuPekA0YycDI/lQApViTweO9AxucNh26dsdaQhGz93Bx2oGJgYJA6UAPMb7DKAQoxk560gIxnG7A5oEN75xTGGRnGPypAAGfwoQxASOOpoEN6daAHdgRyRSGOaKQQicj5Dxx60hjPoM/WmhAGbr1x0oArUigoAeUKgEqQp6GkA2mhDhnbVAIPegBc0CF4xSuAcelMAz+VMBw54B49KAFBpiAfrQIB1pgL0NMBeB16GkIekrpG6K+A/UYoGN6jj8aYhR1pkigc8A0wHdR70gHtG6Irsvyt05pgHPFNEjwCfQVQxV5Y46UxCjvk0CJhgD+tAFryYvsazJPmdm2mM5yPemA5PvEgHPemBeth2PSgaN/SxtPB7is5GsTttJj5GF71yTZ0xOrgjwO1ZM0LSqB1qAGOvNAFOXjNNCM64/iq0DMyY54qkiWZsvUjFUiSjOSM8celWiSm7dcLTJZVckE1Qiu7ZHYfSmIruQO+RTAhc/lQBExzmmIgJ680ARtigkjPQnNACSDleeT1pgR56nPNIBp+91oECu6EkY5GMe1AEWSMjPfpQAhBOMA5z0oAJQySeUw+b2NADeT04oAbnkg9aAEA6+9IBAD0CEt25oARl+cgjJHXmgYdDz+dADehJpCEzz0oGNOOTjmgB28Lhl5b1oACuQCrHd3FAxozjK8fXvQIANwOBmkBNPbzWqx+cu0uAy4PWgZCcB+Bz65oEBB3k47daBjQxz/s0AIBy2Oo6CkBNC6K7F4iykY68A+tIZBg7Dg9TyD3oEOVuSNmQfWgBhfAwVJPTNAwxwVBpCF8xvLKK2U9DTGM69B0pAIQSOeKAQ4oVg8wgYJwB3oAjOOTmgY4hgu4AY9u9IBmQCQRQIOuQAFx1x6UDDdwVDEJnpSGDZIBFMQgB7UAQUigwSOelAx5lcxiPOUHbFIQ369KYC+wpiEyB1oAUdaAFPHagQuSaoBOPWgB3QZH5UhDsH6VQCADdTAB35pXEL3pgHPQ0CHZI70wA+3FMQ7HHHWkgHgfJuzk56VQCAZ+tBI8MTgbiVHb0pjAeh6UEikZI7CqQDxzjB5pgPAOOnWgRdvLF7B4kkeOTzEDgoc4zTAjHudxPQ0xFu3VWViZdrDoP71Ay7bgnBIGPSgaN/TVyy8d6ymbRO+0dMgVyTOmJ1EEfFYsss7KQEMnBpBYz5urUxGXcHlqtCZm3HGTWiEZs5yDVEsoSnGRTRJUkxzk8H0qhFSYMvysNp9DVElV/vUxELEcjFMRCx9uKYyJmznFAiFsc9eaQiLdnjFUAxjnjFAhhI70AI3rwKAGMRv9qQDDyxIOKAEJwOKBCH1B+agBCWL/ADHcT60AMxigAGMt64oAaOnJpCFWQo4eMkEUFIRiWJJHzHqaBDB6Hk0APR9gZSmd36UDGDK47ikAhIB+Y/pQIABuORxjigYhG0crz7UgEYgovoO1MQpOR1wfT1pDF3E4y5J9CelAxm3kjkmgBen3m+lAAwIAzxmkA9Y+GfcMj+H1FAEXVOOO5FACEDoOtIBW5I7mmIaMhuTz2oKEI+fnvSAMnJ9B60gEwT04+hpgN4xz19aQB1BUEgdcE8GgYhIYcfpSEBHyrwQBnmgAAOeBnIoGTCD/AEN7nzV3Btuzuf8A61AFc8gdvagANAAQOMGgCvikUGaAHkfJuzn2oAbjimId0FAC+w60AWLWS2jeQ3ULOGjITB6N2NIZXA9aaEAFO5JJG4WQMUDAdR60DEdg8hbbtz0FMBB70xDuhGOc0AIO/rQKw7JI44piHIoc43bTQMTGGIAoEKOc5piYfypoBygZPGKYCqMUxDgD26UxC/WkIXAxVICTYcbgvHrTBi+56+lBJIvGDz+dMZMgCmmItRDnBHApjL9uvoKQ0dLpi5YVlM2id9o8QCjmuOZ0xOohTismWTFagCtMMZpgZ0/eqAypzyatIlmVcdDzVokzpzgcVSEUJD1qiWVHye+PeqJKtxK8jBpOTjANMRWb72PamIrt1PNMCIkjPpQSMMcnltIFOxerZ70wIGYFFIQbjyTQBEWY9eKAGZHPPNAhmeelADC3UHkUwGnkfSkAjZHUFQRx70hDGA6YpgIfXvQAcevNAxmMgHqaBAThjnqaBiZx1FIQhzn2+lADT1z6UwAMPQ0gDGWJNMY0gYPNIBQdpwBkd6QiWHyVuUNyjNEDh9vU0DIW2l3MeShbj1ApALkZ+7z70wBerZ6joaAGg4HPU96QEjAjDBsseoHpQMjUkDoMH160CG55O7mgYgwc8EfSkAHBI7etACNgyZoAQgYzQIXjseaZQjcZ559akB8iJsQxybnYfMD2pAREAe3rTAaO4wc0gDJzkrnHqaEArHP8O0d8UMB8kzyQxwsBsQ8EdaQyLB6Hp2piE4J4FIYnvjmhgOIyMkj6UkAm0gZKnFMCvmkUKOelAARQAdTTAU0xC4pCHIQrglTtzyM9aLDB8F2KDC54FAD2iZY1kOCG9KYhmKBBTAdxTEJ0OcUwHY70AFIQoUE8VQxRz07UhEsIhbzPNcqQPl46mmgGdjnpTEO4JyBgYpgIDmgkf6c00A4EdhTBij3pkk8MLSlgrgbRnk9aaGNB9etMkkGSMYOKYF6UWmYhaF8lR5m7ufagY9OWJIxzTFc0LdecUmNHT6SmXXNYzOiB6HpCHA+WuOR0o6OKP8KxYyVlGKBlSY8mgDKn/iqkJmVcMOeKtEsy7npVokzJj7VaAz5DyeaoRUkPUUyGVZWBGAD1qiStIeWamFyJ8jkAfjRYRA2eR1piIy7YKhiqHtmgCPAfOW2nHWgRCTwD0B4oAZ6nGfxoAYS1MBm7jpzQDEY5GSeaQIV5pJERHIZE6etICLJ5BNAhoI57imAh+6T+VACHoOeBQApwOgOT60DG5IJyDmkIMsRgtTAaT8uMHikAsgaNyjoQSM4oAbkKTx1oAQHnBHFACtuyTt4NADM+vX0osMdwTwQKQASWdjjr0x2oAZx0c0AKQD1PA6UhiHnr+FAhSAfrQAHjGOaBjcYJbBPNIQg65UE5oAMkFgQCT0oGNwOjGgB207sgdvzoGMLZY8cjqM0hCZzj5RjNAxTjAzyewpASFoPspTym8/dy+eMUDISPlwDz3oEIB1HWgAIwCKBiEYAPJ5oAD2+vSkAH0AzQITGMikMXc4xyTigZXoGHQ8UwFAz160gFoEKRxVAJ1oAUY/GkApGKYhewHagApgBoQhRwaYCjvmi4goAUA9uaYhT8v1pgA5oYDhg0CAimA7gjPemIUYx70CHYyOvNNATNDIkCTkfI5wOaYhgIHJqiR4HsMUDHrnpiqEyReOvJ7UATpnvTAsRD7vv1pgadsvz1LKR1ekJmRcCsKh0QPR9JT5BXHI6Ub8aHGOtZMAdcZpDKM/emMyp2wTVoTMm4YgmrsQzMmbBI2g+hIq0SZtyj+SZ8fuycHnvVCMyQ+1UhFV268VRLKrsTkN0pkFViD3qrARSHJHPQUCIGOc5pgRNx3oERtgdqAGnODx8vuaQC3LWzGMW4Zfl+fPc0AQPycnimAzPUYOaQhuSDzQABSQxIYgHkgdKQxh3E9OP50CGZOMimMCwxg9aBBnBxjNAhWJYglfujgZoGMDHB5oAQ4UUhAc8HoB1NAx7yyXDqZJNzAYHHb0pAR9ONuD6ZpgNzwcjmgBMAjmgBcj8aAGjknpk0gHOSoGdoweq96BgDk8Dn3NAhAcI2ec0DAjoMHFIBOAcHmhCDgBhj8KYxwkZbd4tucnOc9KQxhDY9QKQhAcnnAIoGNBPORQAMXPfjtQMQncByAaQCFsjFACE4X7vNIQ8tEYAqgrLnknvQUMJ6Z696BAOehAoGJ3NIBB900CJI5TFMsyoCynPzd6BkbuZJXc4DOc8dKAAHGRjk0gGgH8aAIaChe1IByhPKJLfOOgpgN7e9AC07iFxSAkjiLpI+4LtGcetMCPtQAucU7gA60CFoQhwwSAx4qgFI5wDkUgE+lMQ7JI4GKYB07UxC4A570AKeWwBTEKMDrzzQgF25J/lTAM0yR+cj09aYhwYkBdx29hTGO780yWOwKYiRRnpimMkTqc0Ekq+3NMC5F0GRzTGaVqvzjNSyonX6KmZB9a56h0wPSdJi/cj1rikdCNxF4qGA2QdaQzMuO9MZlXB61aEzHuD941oiWZdwxPfjNUiDLnchSAxVCeR61SJKEp796tCKkp4NUSVWIpokrOcZ4oFciY5A5wKYEUqlMliCM8YoEQkjr09qYETYPWgQ0+menSgBjMfrSAaRhT6+vpQBNJastgt2JlYOxUx5+Ye9AFXjPB+opgaWna3Pp2n31pFbwyx3a7GMiglfcelSMyiBtGDgZoEABJ45Pc0wAnD7eGA70ANHfPegQ0AHvmgYZGMdTQwEIAGDSAfIi7UCNljjcKAGZ+YlWwfSgY3kksc5NACD3NK4hCCPxoAOe457Uxjg4VGBTLn9KBDQcKABn60hiDBJzQIQgY6/SgY8qvkq3mfPnBXv9aQDMHqAPzoQByTg8imA37uc9aQwK85xxSEM4GQeDTGKxI6dKkAYg4oAQrgZCmgYvPXoPWkAwHOaYC9jnJNK4xM5IwOfegQFTuJ70DA9CSDtPekA3HFAhT60AJ6dM0DE5JNAAAR3oGQ1IxadgD8KACmAuaBCjnFFgDqTxgUAHWgB1Ah6GIqwcHf2IpjGDJHTOaBC49fypoQcZpgL06UxDuo9xQIUdOlMAxyM0xC5y2RQAoPX0pAWVS0NhIzSOt2GG1NvBX60wIeORyP8aYhcEclcY9+tUId7gcUxDlOe1MTHDoc0xEij5O2aYEin05piJl6cUwLkPOOuaQzVtB8w4pMqJ2Wgx/vBXNUZ1Uz0nS0xCtcUjc3BHx/9eoAgmXGaBmRcHrTQzHum27jWiJZkXLAqe1WiTJuCMcVaJMydgeKokoSnBNUSVXIqkSVnzzVCKzMc0ySFsc5Gc9KAISccbevbNAEbnigQxiARQAwj5j1oAYSQCOv0pADGPywACr55z6UARnaSOo+hpgNPOSQeKQCMCTnHFACE/MDtIxQAFieen9aBDOvbr1oGJ0OCaBAcdj+lAxVXIYqRx3NAMYDnNAgBx0WkMCwzuCnNADSeOTSAdxwQAT3yaBjhC7cY+bjAHegRNd2M9i2ye3eKVl3AOMZFAyoRtI9TTELtxyB+dIBpHHPJ9BQMc3yDaQR9aQiYsq258uYmVm+ZSo6DuDQMrHlRjr3oAAPTrQAhxg55NAxCB2PNIQoXJ45P0oGIW3YI4FIBYhHJMiM+yMsNzYzj3pDJb0RQ3M0UEplgDYV8fe96AHXkFnEsH2O4aUugMgZcbW7igZVLAdRweo9aBEtx9lYp9l3qSPnDdj7UDIhyGBwc8D2oEN6/ePIoYCtKzIqE5RTxxSGMyaADNAgx6CmMPqaQxMg+tIRGi7225xQWNNAC0hAKYC96YgpgKD70gEpgOoELzQAdCD6UAKcsc0xCgimIUYzzTASgB3NMQCmIXgYoAfzjigQo4HYn3pgWzeP/AGcbLyU2l9/mY+b6ZpICuQThT07GrEKOtMQ8BgfaqQhVOPpQIkXOegqkIlAPUACmJkydOKANCJt0EcJT7rZ3Cgo1LQHNSyonaeH48uvHeuSodUD03TE/dD5a5JG5q7MdMVmIq3A2kj1pjMa7P3qpDMS5f73FWhMxrhuDmtEQzLuWAwatEmbO351RLKMj8mqIKUh5PcVQiu5I9qaFcgc8elMRCT2HX3oERHJLEDn60wISSc96AELZU8cUhE1xBEkMTxXAlduqdNtAyo3HA696BCZznJyaBjMf99UhEkEYnuY4mkWHzDtLt0H19qBiXMK29xLCZFlWM4EiHg0hEBIYcdKYCHGRQAH2pjFyRzgUhDOec8imAgJ3Ejj2pDA5HOKQgOT0oAbzyCaBgCMcChgJwM8VIE9rM1u8cqZ8xGBBJ64pjNPxB4iu/Ed9HdX+zeiBF2gAcUgMMgg88k96YgA+YqckUwFHIABB55zSGOmkeSTc4GQuKAIwRj5ccdOKQCEY5xg0CFIH1PfNADopFSVWMe5R1B6UFDGILM47npikIcHPJ3bVJ5GKBjTEyxCT7wY4wDQAw8jI+7SAMjjHI96Qw78gc+lADQDkjvQwF5pAN2jPWmAZ28L1oGLk/wAQGKBCZ9uKQxAOTwSKAG5zzQAuRjI60hh0A4oEQ0Fhj0oEPdDGBuOcjigBucEnvTAX3xQIM+1MB2fagBM0AL1oEOKMq5YcGgBDTAD/ACoEO496AAHPUcUxC0xByadwHZoEKOaYCjrTEKBigBxACZzyaYDgOlMkdg5B4+lNCFXk5qgHDtxQIlwVwGGKoRIq80CJ0BzjpTEXoRg4x1oKRr2ajNSzSJ3Xh1NzqfeuOqdUD0/T0/diuORuXyuOtSIoXfBpjMK7bG6rQzCuH+9WiJZi3LcHmtEQzMuHzjHSqRDZnSuec1RLZRlJ5NWiSo7E5HanYllVz1B6UySJyMjmmMicksTg0CIGbnDHmmIaxOBigBhyf/10hIaTyOB+dIoYTknI5oEJ3OKYxo75qRCEgjpk9OaAG8DjbQMTHv0oELkdsnNAxuOTzyKYhAM5oACcDrSAABuHPFAxHB3t7dKADk8k/lSAMk9OMdaAGjJJOOaAEJIBzSCwp4C46igBp4GMd6AFOOMdaYBg9QQT3oGN4zjv60CBucbjkUDE7nGakQhHrQAHGAaAFOTyM/nQUNySeRxQITAyQaQC8j2HsaBiAAnrx6UAGTyNtIBpHqaQDsDaTuy1MYw8LnvSAOPXmgAPLZ6YoGICfwoEGPl55FIByySKpUEbW6imMZwO340gE4HU80gAE0ARUigoAXuOtMBMUwHUCYooQCc0wHUCDr1pALk8ZJpgGKBCnnpTAkkkMuzKgBRjjvQAymIWgQo6gnpTAUnJ46VQCgenWkIUcUCF4HbNUhC/hVDHdBQSx2OnrVCHYGevNADxkimhExd32hjuA9ulWIkiWNo3LH5x93PegRNGOPr1piL0CjcKBpmzZ8sOOahmkWd/4ZjJZeO9cVU66Z6fYR4irjkbFt14NSIy7xuTVIpHO3jYLVaBs5+6blua1RLZjXMmCcetaIzZmXDlgAMACrIM+R898CqRJRkcYINUhFR2HQUySBnHpTEQucDrzTERuSwUZ4oAiz6KaAGN3oAb2zSENyD0Az9aBiZ5zjGKLAMJyc5pAIWyehzQAfN2GR70AN+9k9xSAkaCRLVbnK7XbaBnnNAEZBcY9TjHTFABKrRSGJuo9KAGdzg0AJlfSgBAQucjIpgGQD0zmpACOfamAbSdzfwjvSGNySOnNAheccAUhjTk80xi9uASaQhhHTjH9aYxx5YlQBjqBSYCAjccjGaBAB1GaQEhiH2XzvOUvnaY+/1oGREjGOnrQAdAW25HY0AIMFunH1oATliSPwpBccvllXLud2PlGKBkYGR9etACgdcdqQD1RWieTzAGB+6e9AEWcDnqaAEIHpQMDg0gF5UhsZB60ABznPbtQMXynEfmn7pPrSERnkccCmMMZoAXGR60gHSGMquxCrAck96BjDj0oEQipKA0wAUgFxz1poBWUDGDmmADk0CDHvQA5gAQB+NACd6Yhx6UAH1oEHFMBaAYuTTQg5707ALk0AL2ouImdYxEjq3z9waaEMNMBRTRIuDTC5IQNoKnLelOwhQCe1MTHcgYP6VSESlY/KQof3h+8DTEKoFUIeOv8qALK9RTEX4PvDigaNuyGTUSNYnonhVM4+orhqnXTPTbNP3VcbNSeUYyaQIxr443VaGcvev96tIoTOeu5ePxrZIiRj3EnLelWjNsypnA6+tWQU5Xz0/EVSQinJJyRgEU0iSo7YPPJ7VRJC24lgBn2FAEJbceBwO1AiInOcUASJHG8EkrXIR16IQfmpAViRwMYJoAacZoAZ0zxTAQ5AGW4pNjEzk5ApCA5JBwOOvNAxg5J9KYCEgjFSAg9enPTNACHGT60AKSc85PHOaAG45+tAACeRikOwAHJHb6UXEByc4wRTATPHIzSAAxHTgelAxv4UAwwB1oEBxj69qQx8piynk5VsfNnuaBjAMgkfe70CJZLSVLcXBKlWOAFPNAyHgHA5PvQICMOQfu0AN4P8NIYE5HpQArOxjCY+UHNADeDyBzQAMPm444596AEzz0FIYEDOc0AKy9CCD6gUAM49KQCHjoKBhn1pCEPXkcUDBvXPFMYp6cmkIbnPGTj0oGBGTSAVu3amAYPIBoYBjjApAxOaAIakoUDOaYCZoADQAvTnFO4B9KBDqADFMAoEL1FAC0xC0wAe9AAaEIcO1MA+tAD2VkAzxnpQIT8M0wFPY96oQ6gkUU0A4AZ61QCj60EjxgVQh47cfjVCJAPQUxEg5I44piLMZ56UwL1tkkA0AbdgKiSNInpnhNMhCB3rz6x3Uz021XEdcbNGLOMA0ISOd1A8NzVIo5S/l+9gVvEls567k4atomTZizyZyM8VokQzNkkycds1aRBRlfk5NMVytI2BxyT3pk3KzsQSKYiLzWRsq5BoQELE8HGOetMBjEY68UhDeCenHrSGNyM4pgNOC9IBvPIzSATkrgDvxQA+5ieCQRSfe65BpDIcDdTEJj1PNIYg96QMTmgAOeuKYxC3t+tIA6KexpCJESJ4pGaUqy/dXHBoGIkrokgVVxJ6dRQBFg4AHHrQAA+vWgQhJz7UDAkgZwQPWkAm4emaYhc4HrQMQ84HekAcHvlvSgAH4j2zxQMcHyXyobPegCME9fXrSAVeTjGTSAUxtzkcUwGDrx2oAOmc0ANxnJzk0XGPZGVFkK/K3Q1IEZ6UCAgCmMDwPl60hgBu3HcBjt60AAPp0pAJ1xnpTAByQCcAnrjpSGPmjSOXYsqyLjO6gCM89BSAdtbyzJtOzOM+9ADAPTmgBzdM4+Y9aAGfjQAYoAiqSgNMYduKBCg+tABQA92RgoVcEdaAG0xC0wCgQGmA7tSEJTAd0oAKBC9xTAXqaYh24nHzZx60wAZoQg/CqAdQIdx6800xDgG2lsfL60wFB700SSA+g5q0IcuTn1piHKMnrVIRKh7YoJLUeMjPWqAvW42sM9KQXNyxPzAYqJFxkeq+DUJVfTNefWO+kz0iFNseK4jWRFdD5SaaQkctqcuNwrWKG2chfS4LZreKM2zm7qUc81skZtmRPKME1okZtlCVz1A5zVIm5SeTIPrTJK7uRnBx7UxEMoAUMjZJ7elAiAkkHAoGMJ45FAhhYAHj8KQANhUg/e6igpDdrsuccegoAYTliR2qQG7jk4HNAhSNxBI+b60FDdxbJLZ9c0hjOmaCQJHYc0APiMW6Tzf7vH1oGRdFHfNIBfmPrimA0YB+7+NAATz7UgAnB6cn1pDEJxj+QoAG4wTkH0FACgxBW4ZX7UDGkEL70Eji7MgjIyoOeaBjRkjG3OKBjQcE5xxSEGec9qYCk/N938aQxp9aBDtrHnjAoGNJydwHSgBS59dtIBvU5ySaYABweORSAb9aAFOCPT1pDF3sYxHuHlg5ANIBpyDjqO3FADeuQRmgYvQYxQA5XKxyAoDu4z6UgGFcLimAnQUCAgAUhiY9uKBi49elIQ/wA6UQeRu/dlslT60FDPp0oJG4FAxTigBODQBDUlC0AL04pgCkjjAxTGAoEAzQA7NIQlMQvWmMKAF7UCFoEL260ICXZGLcSCT95n7tUBGeMetAC55piFApiAUxDhmgBwpki5J6UwHB22bAflzmmMAeKZDHkdKpCHgfNkmqESDnpTETiN1jWQ/dbpTESxjB4pkl+2bDYYA07Em7YnDCpkgUrHrXgdd8a57GvPrxO6jU0PSEHy1wdTpvoVL47VP0q4oXOcTq8+0vmt4xIczib+4Hzc10RiQ5HP3FwGJFapENmXNLnPpVWJuUpJMl/0p2JuVmfuFPvTC5WZuuaZJESOoFADGOCecUgGkrkZQEfxDPUUDJLyS2lu2e0haCDA2xs2eakZW6HJoBAPvEc57e9FxjWJyBjB96QADnPBzQA3jo2aBivkH0/rSAacjOeM96Qh0oVCojk3lhz7UxkfsOKQDTjp70AKRz0piQn1PTtQUB44xwe1SA+Vo2dWjUoAOh9aAGHOOfvUgJLiB7dYyxDeYNwwelAEQDEk/wA6YDcZOM5AoABwxBP0pBceqFjkkDHb1oGR9QcjjtQJh0GMZpAOYo0SKqkODz70xjMj+6SKQhCPl44PpTGB5IBpXAUZ9OO+aAExgmgYg7/WkIDwcAZ/GmA3o2CMmgY7naeOO2akBvzEdfwpgLkbWHc0AJyRSAc3leWBHneeuelAxn160gE/hpgA9Oc0gEz7UAGOeelAARnmkMQCgAHFIAIAGc5oGHHpTEQ1BQtMANMANAC0rgOKgKCD83pTAOgzimIAaBBQAtADjs2DH3880xidO1NIQD6UhC/hTAUUxC7fXpTAM8ccCmA7GKCR3y4OOtMAUcUxCgUxDhnvTAmjQNHI+8AjGF7mmIaORnvVIlj1waYiRVYr0GO9MRKjdOW2jtVCJkIB68mmQy5B1yaqxLNmxfBHNDRi3Y9W8C3sfnIhOPxrirrQ1pVT1RGBUc15slqepCd4mXrF0sEBLEdK2pxuctarZnl2tamrs2GzzXVGBmqtzkbq8Dk4rZI0UzJmmPNUkPmM+SX1NMRWeQD+lAiKZlIDKTk/eBoArluKBDQybvmzs74oGNkCiQhDlOtAEZbPSkAmSfrSGJkY6ZOeRSGiZHtVtZlkhc3JP7tweBSKK7bid5OTjGRQITJ5BbmgBFI/H0pDEYFVAbqaAE4x6mkIbx0xTGA+U89aQCZJJyKAsBwRyaBAR024z3oGISSxPQ0gEHcHk0DDHGDyaQkGScDJIHY0DEA4z6dKYAx3DjA+lACEgHjnIpBYCuCRn8aBgeVAB4FAMATnFAgPHByT25pDHysshTCbMDmgZFknqePWgQp2nG373vQCJIofO81ywUKM4P8AF7UDIj83ToD0pANbgmmAEDFIQoAZlUvgHqcUhjplRJNqSb19aAIiATTAQgZ4oAXj6mkAc+goGJ9aADsfSpAe7QsE8oFW75PWmMYcnsMe1ADR3zQAHikAp6UhCYyRzzTGAOMgc0kMMnuM0wsQ1BQ4hQODzTAaKAFFAhe1ACUwHdV9qYCCgB1AhQMUxBQAdqLgSYj8kYP73POaAG9elMQtAgqgFA3EY60xC9DQAopokUcGmAtFhDhVCHAD0pgx/XmqQiQDqccCmSKOSDVCJVI7mgTJo2HTbVohlmFsnNUSzTt5cMMCgxmjqNI1eSylR1PCnsaznC6OZ3TPR7T4gBYArLkgcHNccsPdm0cVKKOe1rxc19uBf6AVpClYylOU2cdd6gZGOTz9a2jE6ad0jJmuCc07HSii83J56UWKuV2kHSkMgL9eeRQBEx4560gGNTAaxJGe3ekMaecHGP60DG7ueRzSATvmkA0Z5z0pAAYDGAfzoKQEc5AFADclu9IA4LE4zSGNb5sevpQAmQDSEKQeSOMUDQpBWJW7t2oAYTwM0AI3ycEEZ6UAJ6k0AIQByTzSAXIP8PPrmgYg6nmgAAFIBMYPNCBCAZJ4pgLu5Hy8CkA3OenegBzchQKAEJUHnnNAAflBH5UDEx8oxyaQx8kbx7S64D/doENP3xnpjAIoATkfd4x39aQy3psVhLcuuoTSRRbSQyLk5xxSApkdcfMoPFMBAQeowKBCHI4xSGH4daAEyF7UABJH1NAw4PQZNACDNMBPbnNTcBTkcEEGgAz0yc0DEI544oABk0hCYBPNAw5J4Bz6UgsBOHGKY7Ac5oATGO1AEQqChe9MQHK0MZJCYQzeerEbflwe9ICP1/SmAopiD2oAUdOKBEiRswLAjApgM9aAACmId+NAgB/P6UxjsDjHJoEIOtMQuKAFXGfeqABQIfj6UIkAcdcUwFBzTFYcMGqAcOOMUxMcBTRJICQm3HGaoGKtMkmBwRTEPU5PJq0TYniYKetUSy7HIU4PegiRahuMHOenamYuFy4L8juaixPsyN7xskE5/GkXGFio85J9SaLG6RXeXGeuaRaIGk5pDIyzcjt9aBoIpo0Z/NjLBkKrz0PrSKK+cD69KQDSeaAQgGQ4AzjmgY0ndyOgqQG564oGN65NABkEcGkMQ544oAMDPSgYbRjNADT1yPlpDAnceOvekADJJ+X9aQhvfDHJoGIeeentQDA5PHekAru7spfnAxTAZ16daAFGcHuakB80MkO0SLywyMGmMjznoKQCHlsAUAHGOmaAHJ5O2TzA28/dI6CgYznb60CDqOcCkAhPoOlMYpAI6/NQAhPoDk9jSAGxx6+tADmd2xuYkDtQBHjHXnNADnOQuO3agBCfU89xSHcOSc9PSgBueeBzQA4s2/PB4xmkAmcjsCKAEBG3jlu9AxOoNAD5Qy7A6lcjj6UgI8c+lAhVba4dRnFAxGcsxc9SeaBhjBI2mkA0j86YB0ApMA69B0oA1dE0S5169a1tZY0m2FwJG25x2z2pDM+4hNvcPE4yyuVODkEj0oGQ44560CAc0AQ1JQtMQpJPXmgYZOaBAaYCjrQAYoAUUCDvTAX2oAUYoEH4VSAKYhaEAtDAWkIB1zVAOGPrQIPwpoQ4YyBjn61QC4xQIeBxnoKaEAyaoTHg9sU0SOGaYDlODTJHjAP1qkwJFwO4p3JZKjc9Kq5NiVZOcmmTYnEnp1ouKw8T5bvj3oDlGtNkkjtSHYYZgQaRSCVofKVkdvN7g9KRRXLED+dIBmeuaBiFu460ikMY9BQDEJ79aQh0UzwOSADkbcGkURHgZ75oGB3EZH5UANzzzSAQkgkgcetBQm4k8ikIT60hiHigBSM4I6+lIYsq+WVDD5m5yKAI3JyTnAoAXtyKQDTjg/pQApyccYoCw3GPrQAE/wB09aAAjnA6mkAde5JHqaBiE8c0CAsCQV60DEGQxpAKT3zzSGKUYKZAPkBxn3oAVBFl/N3ZHIIHWgCMnIxnIJ60AODlGBXGc9aYAzFm3bcN65oAYD1z1pCDA6E0DAcDp+tACcE57UDEbk0AK3XNIQYPXigY0d/egBCAKAHcgDmgBCM8n8TUgPeR5dvmNu2jA9cUyiL8OKBA2CRjgd6Q0Tw28k8UjoqlYxl/XHr9KQwllt3tIY0hMcyk73z94UAQH0PSgQmaBB6c4NA0SwSbHLjcD2Kvg5/wpDIyxJ55JOcnrQA3GKBC7fcE0AQVJYtMAoQgpjFpCFoAkkVVC7G3Fhz7UwGH2oEHSmAp6UwAUCFxTAUUAJ1piHdqQFqRrP7DCIlkF1uzIxPykUxkCnaVOM4OaCSW4lSabfHEIhj7o6UwGKA2cnHsKYhFPtTEAoEPycAZqkIdjnNUA4FT2pokUe5pgOX60ySRSTwCPxoESbjG21cEdjVCFVjn3PWmKxICFOOpp3JsPVjhiMe1AxfMJ5PIouAjSZIwKAELkUgG5yPegaGkjI9aRQhI696BCgc5GB7UiiMnIJNIYnToOKQho9/woGIR2NAxD1FIAxjp1oAAWzzSKG7j9BQIUkdutIaECs77VXczdBSBiMCGKlfnHBzQMQk8ZOfQUAJyOPU0gJbqE28ojZw4IDZU0AQnBI9KAEYZ5FACZGeeaQC4G4cYFAxp6kg9KAFxkUgEBA60CFA60xiDnqaQCe2M+9IBwZlTAJK9fxoGSz2/kwQyiZJPNBJUHlfrSArkgkelMBevy4oAQDOTQAA4zn8KAEG7qKAJIlSRnLSBMDP1PpSGRk+uMUAJkdulABwDzzQAmOTTAATnikwDIz60kAY+Y/ypjE9qTEB4FIYE+mcigAJ/h4yaAHIyqj/Myk8EL0I96RRHkihCDODQAd+eKAEPWgBTjPtSGJTADSEL8uOOCe9AyGpKHxlFzvXPpQAzrmgBeMe9MBKYC5pCD8OaYC0wF6UCCncBe3vSFYO9Uhi8nmgQvSgQvXmmAZ+poAXkAD1piF780CDAz7U7gO247Hb2NAC5oJF7VQCjg07iJFUncQMgd6q4AMZPNBI4HNUTYdkE9KaEOX6VQCqQOKYh4bnkUBYXOOBQIfndgZouMQNnIANIYE44J69KBDc8Eg0DFyB25NAxvrmpFYQkbhximMDwcHhaQCyhPNxC2Ux3pDI8r1FAASSSfypDG555NIEKiFpNoGSelBQ04yQDwKQAi5bbkAE4yaQhWQrJszn3FMBEd4pQ6Ha46UhoRmZ2ZnOXY8kUANHcfzoATg9eaQ0J07HP1pDDjHANAhMcUAPkBjUBkUFhxQMjJGMYOPSgBWAXb0OaQxP4jzSEH0GfemAgzzimAnp60gHYPUYoAb16/lQAADOcY/GkMXB7D86AJoIo5jJvmEZVMjI+8fSgCA5Iz2HpSAMjsDmmAhOOD0pAOIyO2KBjPXFACdjmgBxG3jGPTNACEYoEKFVs7m7dKQxN52DI/KgY3jI9aAF6HFAhpx9aQyVmR0UdHzyT3oGRjPPegRJFC0m9gwAQZ570hkPLc0AKCD0HPrSAUqBLtY5HrTGJwOCMDP50mBZvmsnmj+wo6oIwHDnq3egCp19qAD9aAE554oAiqShcnrQACmAUALjd3oAB9KYBSELQA5gABtPJ6imAmfWmIP4qAFGTQA9FU7iTjA4piG/WmAtMQvFMAPWgQ6gBRn6CgQ8SuIzGG+X3pgN6HGKYhw64JpgLnFBJJkgkIeCOaoBoPAB6UxDs4NMQ4ZNMVhelMQ7j1p3AeeNuO9MQbvm5FAhTw38hSGTxRSSLI4IAQZPPWgZCGBDEcfWgQ0kYGKAYrH5evNAIc3lmMbf9ZnnPpSGMB4J65oAYPfk0hoM+1IbEz2IpiEJyB6CkMMjt1pAKGKMGz83akMltbSW9u1t0ZFdzkFmwKQyCVdkjo2MqdpPY0ANGOuOKYB3JoGIKkQhGOc0DQoHpzSAEZfMDMm5QeaBjnMZlYxgovZTSAjwCSOgz1piFJLNjdkD1oKEU47ZoEBJDEEZNIY5VjKszuQ6/dGKQDCxOWHU00AgPp1oYg78ikAmPm4pjAcdKTADkjpQgE9AaBCkegyaRQHAwcfrQA7JIJBAIPT1pgMHVuOKQCd8ZoAM4HoaQAenH40APlcuI8pgKMZHegZF196BAT+dACnGRzx6ChjEPXOOKAEP3qYC5xSEH45oKEGaQgPsP1pDQjY/GgB2TzhuO4oAZ68UAOGR7A0MBpJNIYrDBGDxTATJ5HakAg9xQBFUlC9qYB2oEFAw7UAL9aADGKYhc0WAAKADpTEL0xg0AOZcYPXNMBOM4xQIWmApNJCFA9ATVDD+dIkdjIyOtMB+xvKMuBtzimFhoHFMQvSgRNA0CuxnjLArxg9DTGR9iR68UxDh+tMkUZHaqAUN1zTExc8cUXELkdc0CHjgg44/nTAAQeenpVCFHB4H40AGcE5pgOViowCRnqc0gDgnBouAHI5oAQnjIOTQAZ55pDE6d+aAD6VIDmjZYhJkEHoKBkdIaE69aAD8OKSFYQZ59KBgcD1z9aQxMgcGgYg75zQICOeOBSAbkUAKcYx3pDQHJbjj2pgJ1BqWNEgizbNLvGQfu9zQBFjIGetAAQTzigBMsKYg5HTIzQMOvqTUjAjn0oAfHDJIx8pdxAz7mgCMgnt9eaADA9aAHwoks6o0nlqerHoKQxjY3sM8A8H1oAO4yKBCA5fpTGLkc4X8aQCYPJyfrTAAcqaQAATyBzQIckgSUMyBh3FAxhYFiw4yelIBCWI5PTsaBikELuAwKAGj1HWgQ5XUKVZM570DGngYHIpAHTrQAdD0oAbjPemMO9IQUgA9OTzQMdIoXbhgcjt2oAaaAFydm3NADAKADFAxetIYnHpQIipFBQAUALQAoBwxxkCkAlMBaBAaYCnORigBR9M0CAd6AD0qgHcDkdaAAmgQvb1oAllaFo4hECrgfP7mmBGD60yRaAHbm2bQ3y+lMBOuKYCg0Eig98c0wHEdPemIUcUxC4J5pgAouAvT60CHqccEZpiFBJHXp2qgAetAhwJ5BGRQAh4HJpgSNsCIUbLHqPSgBgpAJkdKExBkVVwF47UihCcmkAmQakAzxwCPxpjEHJNJgPTyS2JSVBHGKQxhI5A+7nigBp4pABoEL6Y60homhNq0cxnLLIB8mO5oGQZyMtzzxzQAnU+lACZ5bPPvQAhGOTSGKeQPTvQIbx6UhgfWmApwaQCAcUwDJNIB8flbXL5Dfw/WkMYBnJPINAiS3ma3mWVGwRwfegaGhVLNvcLzksvSgY0BMHJ57GgQmc9utIB4kIiMexCCc5HWgZGCQMdc0CE/yKBkkitEfmTaWHHNICPtimAhApgDDH0pAKBk8dfc0gE789KAEbkk44oGBHzHjAoAMnNAhMetAxyryVGST0pDG880CDty1ACEg/WgYpoATPYmgLBjH1pAA+lAwAxQITGKAAUAJ0qQHIYhu8xST2waZQ38KBEVSUFMBaQBgetMB4kKxNGMbWOfekAymAooEBoAXvQA4nB4PB9KoBSF2LtJ3HrmkAg7nHIoELn0HNMBAfWmIXNAhQc8GmMXj0piAetMQoOT7UAHXoKAH4YAMVIU9CR1oAOtMkfgbM5yfSqAb296BDuOvWgBcrkZX5e+KYD5fKMuYshO2aYDR1ppki4wadwFYjp6UCFJzTEKPpQADr6mgYDj8KAHMSRkcH0pBYbxnpQAoGCeKAE3ZOe1FxgAeWCnb3PpUgIcnp0oGJk9RQIM4pAgPHIPJoKG7s9uKBC5FACDrg0BYMkfSpGJ/KgBCo64ouCExnvg0DFyMGkBJIkIhjaN9zn7wPagCI8cUAB7UrgHAPSmMQjvigQdBSYChgPegYmCPpTEGM5pDE60AGe2KLgAwDSAMcmmMQGkICMikMczMxDMxPGAcUDG9D05oEITQIXjHTNAx48j7M+SfOzxx2pDIsDApgL26ZpAWrg2DWFqLcSLeAt5+77p9MUDKZ6ccY60AGR9aBCglHBzz2oGIQQT60AANAhOg96BgQOO1AwwO3PrSEAPXnigAHSgBO+DQMGoAQ8UgDNAC/jzSGJzmmIjIA75qShKACmAuMUAFIBaAHKqmMsX+YdqYDaBBTAUUCFpjFBxnFAg5XpTAXP50CDr1pgLSEKrEelNAJ/I1QDueKTEKfrQgJXuJZI443fcifdHpTAj5FMQCqELx60AObHG38aYBmgQoOTQA7GG9qBBkr0ApoAzkg9zTuAp60XJDgUxi5AoAUAZINIAbJZj0oAUZIPTmgBuWPU5x2oACcj+lADxNKsRjD4Q9qkZGaYCv1GDzQA3rQAo4yB6UhidMg9aTAMjHv6UANzjqKBisegFACMfakAZB4OaQIlhSF1maVyjBflGOpoGQ9skZJ70CFAyRnFAxpBH1oELyRSAV3ZwoYDApjGn68UAAwDQIOeaBieuaAAdDQADPapYDwu6MvuGR2oQyLr9TQAucjigBcEcgGgQhPFIZJ50jQ+USpUHgGgZHyOPzpiAH15pAPTydj+Zu34+X0oGR5/vck0AKex/i75osA0c5oEBP51IyTMHkNncJiePTFMZHnIxkZ9aAGj360AHQ0AHuBkeppAJk0wDjvSAUn5cYOaBjcUCFI9KAE4FAC45AJ4pDBgob1FAAFGeowfSgBCNo+vSkMTvTAjqRhQApGBmgBO9MBwUZ5IxSAO/tQIKYxKYhaAFBoAKYhe9AB3oAKAHDGKoBO5oEKaBCkdKdwHcc80CBaYDl5OCcDPJoAVwgICcjuaYCZoELn86Yg9OKoBQeaTAXvQIO9MQppgHpQApznnpQFgBHpTAcDwc9aVxEkbxqsgeMsWGF56GgCPtgmi4AcfjVDF+hpCsNAxSAM8UhgcUXAT3FFxinHbk0gGnnmgQ7JBBAHH60DFlkaWTftCn0FADQfbP40AJnn5vwpAGcH2pAKrgRldgJ7H0oGMz7ZPvQAuBz2oAbxQAueOOc0xifWpEHFAxeARjnNMYmBnnmpAOR7UxCUhAaZQZHbmkIMe1AwOaAF2sAXHAoATJPegQh4oGLxjJ60DG9O/WgAGRSELkA9KBidDxQIOCevNAwANACe4PNIAz60DEx1oAXOB0oAbnmgQ4sQNuMigaE5wMUAAJB3DqKkB88zzyeZJt9PloGRnigQlMBfpQMaTk9KAFHcYzSACPWgBOT+FACnFAEVSUFACUALTAOtIApgKDQIWi4Cd6YxzAr1GAe9Ag6UAGc0xC0ABpAHAJ5qwFJoELmgBfQ4oAUnLZxTEHTNAhfwpgHemAp9KBDiVIUAYPegBKYhwAOT19aBksggEaGJmLnO7NAiLoKaELRcQdOaYw+uaAHfnj6UXCwZNArC5HvmmAmc0AL75pXAXA+8M/WgY0UCDjv0zSGPlEaviJty47igBn4UAPlMLRJ5fD4w1AyMnNIQHtTAOARQMCD2oEIM0hhk80gE560gEBqhiZ9qkBcj0oEHU5AplCA0hARjtQAvA4xnPTmgZe0xtMW5kOqpMYvLbaIjzvxx+FSBQOD3+UHpTABz2oYhOO9AxTgYKkc0wE5NIApCF6rgHj0pjGjpQAtIAIzj1NMYmDjHOc0hktxbzWsojnjMbsoYZ9DSFYiyO3WmAdPrQICMNQMD6mgAfDAY4HpQMTHHHUVIEmIBblt7efu4GOMUDIzzimIQfeoAU5yeBQMaOvNAB0NSIerbVYFFOf0oGR5x2pgBoAQ47UAL3pAB68UwEGeaADpSAXHHTJoAhqShc0AFABnmmAcUhi0CCmIOaYC9TSAcWY4BOQOlMBOSOtABTEGaAFHNAD0fbkFAfSmA38KYCikIPrTAWmIcxQhdox60AJQIXIFUAvvQADnFAhelMQueDjgelAAOe1AB9aBDiBjg80wEz7UAOyMYxmkCLxLxaUQGgcTP93H7xT/AIUxlE84xxTEAJFMQv1FIAz/ALPFAChvl2Y4oGN7CgAz+dIA/nQAdvpSEGc9selMoTOSaQCbvXpQAvAoAQZBNAB+NIBVEZ+8SPpQMacZ68CgBehwP1pAIQe/6UDHOrxkK42sR0NIBhpiHYOMjFADQT1oAORx29aADODSHcCD1oAcUZUEhU+WTw1IBnT6mmAc9QRxTAAeOlIBOh5pAAPbigYZ9KBCHpTGOJyooEIffrUjJZJXmdTLJvIGAT1FAyIfn70xCDrzQADjNACGgAY0DAnp6UrCAHnpQMCKBAATyKQwYktk9aYBhe5/SkAh9fyoGBBNACHikAvXPXNADefTFADux45oATLHjFMAB9RzSAQ0DEH1oAZUDCgANAC9etFwHSAAjCsEPTNAxtMQZpiCgAHUUwFI+agA5zQAo60wCgApiHD1oAOaAF4wfX3piEFMB1IQlADsimIOv0pjCgQtMQAUDFoEL0FMAoELjBpgA4pALzz2oAM5ycfrQAuec/xGmITByaYADQMUUCDpSADSAduQ9sGmMaKQCUCA47UDFOT04oGKis77Vxk+tIBvUkE9KBCetAADQAc0DHbGEZfHy5xSAZ1GaAD6UDHNI0jBpGLEcdKQDKBAOtMAoGLzs4oAQUCF7VIDjPKYBCX/AHYOQtBQjrtxwCDzx2oAj79OKYCmkxC8Y6ZoAe0itCkflAFTy46mgZGeDg0AITQA8YK434oAaAevQ0AJ35oAUqfpQAmQPrSATv7UwDPtQAHkUhiliQFI4oATmgBBSEBxxTADj6mgYduBSAVmzGq46UDE6d6AA4P3Qc96QEs0kLJEscZRlHzn1PrQMi4BOfvdjQA3BNAC8nvTEJzzSATnvQMM+ooAZUDHFl24C4PrQMbjGKBC0wHvI7IqMcqvSgCPpTAWgQUwDmkAuPTrQApGAD3PWmMQdaBC0CCgAqkMUcUCFxQhCUwHYPagABoEFAC07gOwdu6gBM96BDj0BBzTAAaBBTAUAbPegA7e9MA/GkA5xggZyPWqATPNAgPFABnmgBc/hUsBCT2oQC56ZqgAmkAo+ozTuAg96Qw6GkIAaYCUmAoO1gVODSQxOucnmmADoaAG0AL2qQFLts2bjt9KYxvUcUAAyKQEkSCQtulCADPPekMjPBPoOh9aYhKBC0hi7BsLBhu9KYxvbigQfWgA6GkAoO08Y54pjEGfwpAPhhM0wiUgFu5oAawIdlPY4z60ACjPsPWgBM9R1xTAT1zSuA9Y3cEgDC0DGdSccYoAXPGMUhCcfjTAU7SPl6nrmgY0H1pCAn8qBiqpIY4yq9aAExkZoAOTjmgBM+1IBOppgLx+NAxM5NAASc0gENAh3IHFAxoPakA8FcHP3vftQAw9AO1AxSOmKAEx270AKeMEdaADJoAhqBi0AHamACmAtSAGmAuRimAlMAFIQtAAKYAKQCjbu+bpTAVtu75Tx70WASmAuMDNMQCgQuMUALyQeCQKBhn0oEGaoQvWkAgJHGeKYxQOuc4oEL600AUCFzQAcYxTAWkIM+1MApgGKAFFDEL3pXAsW0VvKs5mnMTqmUGPvH0pFFfORlun86YgJpiDPrSuAfTigZIUTyN4fLk420AM/hyOvegQnNACZNIBM80DAmmAZ4pDFOBSEA2Y5zuoQxAc9aYAeDSAOhoEB56ikUJkdO1MQGgAz2pAHuRmgYMQeRxTAQmgBenUUhCAc00MAPWkIM89P1oGHQ0AHrTAOfxpAHTrSABx0yPpTGKqhmCk0AJzk+gpAJ0PNMQdM9DQMQH2pALnPBpCAMwVlUnBplCfWgBAOaAFJpCDPpQApG3Bx19aYxpI96AF6j0NIYmR6ZoATHzUCDnNIYYz1oADQIU84xRYBv8AF0oGA96AFxjHXNJgRVIxaYCUALQAUAFMAoAWkAqhSw3cCmArBQx2nK+9ADaYhehoACaAFxkZ9KLgFABmqEFAC5pCHo7Jux0IxTGNFAB0piFoYAOh70guWYjaC0l8wP8AaM/IR0pgQemTxQIM4piFP50AKzB8DbjFMYmaBCimAUCDNICe3g88v+8WPaufm70DIM0xCgj0FIAzjtTACaYhcmgBCSaQB17UDCkIKdwEzSGHPagYZ9aBAc4oGDGgApAJwOtAwA5PFACGi4hcd6AA9PWgBTjafU0ANHSgBKQxTQAp56cnvQA6aMxlVLZ3AE+1AyPpjFAC9R70hAQygFlIBHFMYlAhelIQHB5zzTGGKYCZz3pAGAKBge3akAvGDzigCSQQBU8pm3H72RQBEevNAB9BQAEd+lACfQc0gHKFIYk4PamA0e55oAO/NIAGc8HgUxiA9j0oAOc1NwFAbBJB2560AIeRQAduKAHiPKbw447UDI+T2oATH50ALkdutACDn60AAGaAI6gYUwA0AFIBelMApgFIByjIJz0osAlMAoEFAB1pjFApCHIVDgsuV70wFfYXJT7voaAG5zTAWgQYNMAPFABQAv4UCFAO0tg7fWgAzQAufwpiEpgLmgBaBBxTGL2oEJQA7qOvNACZNMAxntSAKAHAikAnWmIXGaYCUAGKAAUAHekwA0AJgUDAGgBQaQDcc80wFPIpXGBouIOMUgHlY/J3bv3gPQ0DIyccY5oACOnrTABwelABnJoATpSAXoelACA9aAF6jpikAn4HH1oGHQdaAHOoAGG3Z/SgAZ3bAZs7eg9KAG/SmAZoEJQAuBRcA6dqLjA80AFJiA4FCAB8w4FDKJYJo4vM3wiTcuFyfun1pAQfpTAKBC4FIBvFMY7HTjikwEPJ9qQARz14pjE+lIQpPHzUAKHfYUB+U9qBjSOlMQZ7UgDgUDE9KAF4pCDHfHFMAPTikMBs2Y5DHvQBFUjCmMWgQcCgAPJoGGKYgpAFAC0xBQAUAL0NAAevFMAzQAZoAKYC5oELTASgYvSgQvX60CHb28vy8/LnOKBje/tTEKTQAA80wDnNAhR9KQB+FNAGaYBSAU0xAOaAFHX2oAKAHBvlKkcmgY0cCgQUgFwSCwximAmaQC9RTATigAzSAUDOT2oAaTk0xi0mACkIk2IIPM80b842UDIicCiwCg8dKYg78gHNIoT1B5oEJmkA7BKk559KYDePxoGGDSEBbmmAfQUAJz1oGLn5c4oAPrSAMe2KYCc/jQAuTjjFCAMk0wDPX2qQDjr1oAQH8qYBSACfWgAPT0pAKeAOu70oAbk8UALnBpjDGGwaQWEOfpSAULkbuMUAKpQH58kZpjE4LYB+UmgQNhSQDx2qRkkscKwxtFLvkb7y4xigCIAZOFP50wAgg/ypAN5BpgOI4zkZ9KAEGTzQAmcUALyKAEzntQAGkAYNAEdSMKACmAUAFAC0ALtHWmAlAC0CCgAFABTAMUAKKACgAoAdtIXdkYpgJmgBaBBimADrQAd6ADn0piFzQAo60AB70AK4BI2+lACDJpiDpRcBRSuAU7gL0FAhKYC0AHFIYGmIM+tKwCg4PBoATtQAGmAuaAAmkAmTQMMDPNACE4pALnFAAMHtQMO1FxAOlACYNFwHDKDJApDG5piCgA+gpAOJj8vAB30DGHjigAFMAyaAFB+T8aAAHigQhBoGLkDtQAdeaAEAoAOM9PrUgixdTx3FwJI4BCoUDYPWmMr59M0hAKAJImCShmj3gdRQMQPsfeg5zkAigBrNuYuw5PpQAn40CAnJ9jQBLbNB5pFwp2Y7evakMiIGTg5XJpjGg/hQIBSAMZ60xhnsKkA9OpNAGvoOktrOq29grrG87BAzngfWgRb8XeGW8LaxLp8k0U7IAd8ZyKBnO54zjrQIQjkYpjDGOlABjj3pAhzhgQrdvagY3pRcQfhQADmgCOpGKduBjrTGJQIKQBTAWgAoAKdwCkAtAB3piDvTAXGKQxDnvTAWgQvUUCEoGL+FMBBQIdQAAcE0AFMBaBCdKBi+tABTEFAC/SgA/nSAcPL2HOQ9MBvX6UwFNABRcQEe9CYBQAlAC0XAUYzSATNAhewpoBMU7jF7UgEpMAPUZ6UDHqiswDHYueTjOKAH3UdvHcFLaQyRj+IjGTSAhzn2pgGB60AIKAFPTpSEIM9s0DAGmAHrSAKAFGPegA2n8BQMbyaBC5FMBeAMd6BjQKBDuKkBTzEMHnPSmMZj8KAFPA4oEHIHFAC9uG5+tAxuT3oELxQADG04PJoGIDn6UgAfSgQc0xgeR0oAM+3HpSAToeaQx20hS3bNACD16UCDHvQAmAO9IYo29d3IoAlhneB1dSQ4OQwPIoYh1zdS3UjPNIzuerMck0DKxyckimAcduTSGJTELSAeRuXeXyw4xmkMZ0FMQpOetAAdvY4FAyKpAKBhQIWgAyPSmMB9KBBQgChgL/KgBXVBjY2c0AJTAKBB60WAUDFMYlAhe1MAoAUCgBKAFzSAKYC0XEGD2pgGcj1oGPYoUUAYbvQIaKBBQAlO4C0gCmAHoKAHAEjIHTrQAUhBTAdsYxeZ2HFAxvagQZpAKWyu3b+NMY0GgBaVxAee/NMAbg4oGGaYg9qkCaKbZDJGykl/0IoGQ+360AJ0pgHG33oAAOKAFH+T6VICsuHwGyPWmA3r7UAGQc8UDEA5ouIXjsKBh0NAhM0AKWJ4xxQhiAUAL04oEJ1NACgZoGIcDgnn2pADHNNAOIXauPvGgBooAAMUmAGhCE70xhnmkIUmmAZNACGgYDOcUgFI2nB/OkAm44IB4oGB9qADtQIOlACcZNAwzzSAMd6YC8ZyensaQAxBYleBQMQH2piD8M0gHEr5e3Hz5oGM60CCgAoAZSKEPWgB3akISgBaLgP8pvJ83jbnHWgCOmAtMBKQC9KACmAvWgBKQC0CCmAU7gL+FABQAo60AKTzx0pgA96BB178UAIKYxx5XpjHWkITdTAKADNIBfw4piEpgL/OkAdvegCRZdkTLjlu9AEdMBaBAPTnFAwzz70AKfY0xCCgBeKQw570WEIaADJbk0DCmIX3oGJzUgGMUAJn2oAKYC1IDmcFNoTB7mmMZxn+tAC4A5zSEKqEqzAjjtQMbnuaAHFgeQMGmA0c/WgAouIDUjH7YhHksd/pTAbzQISgBTxQABiO1MBPwoAXPqKQxPY9KALFqtoyy/at4IX5NvrSGQtgKuGyT+lMBMDGe+aBCYz2oAbQMXIFAg4z7UgAfeyO1AxOpNMA6UhC9qAGmgYvakAAUAIaYC5oAKAENFwFOQB70hiAmgQZNAATigAz7UAFADM0igpALhduc80AJQAtFhB2oAKYBTAO9IBTigBKAF6UwEoAKAFpgLSEGTTASmAo60gDvTAKAFpgHQ0gDHNMBefSkIOaADqKAFLEqFxxmmAd6QAwwcUwDNAgoASmAtAB06Uhh744piCgAFJsAxRcYpzTEJmgAFABigAP0pgLnipYCUDHMCOMY/DrQA3FACZoELSGB4pgGfakIT8KBh1oAKAFxigAyfSgA7UxB9aQw70wEPWgBT0oATFABnmkAGmAvbjrSAeJWWJouCrHJ9akZGaoA6GgQDP40AByO2KQAMkHigYhPrQAZ9KGAtAhO9AxDzQIUjFAwAINIB0kZj4yGz6UANoATmmAUABpAKSTimAlIBQCxx3oGBGDg0AJ1oEHFADKRQUhBQAUAHFAwNAgpgLQAGgBKQDgvyk5pgJQAUAGKAFPBqgCgQUgFoGPaNlUMcYPYGmIZQAUAFMBcUCEpjHZJ70gEoQgpjDvQAufekIOaYC4zQAZx2oASgAoAXJxSEKWPlhcd80xiUCFxmiwCqAzgOQqk80DEcKHYKcqKAEoAKADJpgGaVxB2oGIaQDnPIIbIAoAbn86YCj8KTATNIBKYDiVA7596LjEzQIUAZPPFACdaQDmKbRjhu9MAYALkHJNADRQIDTGHHrQMOtK4haBASKYxM5pALgetAhAOvtQMQZpAKaAHuqhEKnLEcj0pjGfzoEGTQAnPrQAGkA5m3AYXGKBjcYNAA33qAE6UmAp460AHXtzTAPzpDEFAheKAE4pgFABikAZoABxyDQMPegAwKSEGMUwGYqSgpgIKQC0CF/CgBKYwoEFABQMKQgpgLQAUAB+lAB3qgFoAKQCmgQUxhigQd6ADFMA5oADSuApxgetMBKAFoAOTSuBKkDtA8wA2KcGgCPOaBBnimAUwDrQAZxSuA4gAZByfSgBBknGRzQAMpU4P50AJQAfhQAUAKBkgZxmgBZF2PtzuHrQAnPbmgA5oQCZpgFIA4oYAPakA5PL+bfnOOPrTGNzQIM0gHgqYmyTuzxQMYM9TTEITzkUwFBpAHFABx6UAJn2oAWkAYOM44pjF5AzjjsaQCe9MQnemAoFIBKAA9aBi8D60AJmkIKYwAoEBA9aBh070AH1pAGKQCGmAHpSuA4lSmAPmpjGnqaQhTk0wE6UAGaQGjBoOpXMYkjtWCkcFiFz+ZpcyQ7EDaddx3YtngZZSM4PTHrn096d0BWcAOQpyo6H1oAQ0IQYHrQMP4aAAUCChAJQMZUjFoAKAEpgLSAKBBQAUxhSELimAlABQAtAATmgApgLQAcUAKduOBzTASgApCCmAuaADimAUgFJ3ADHSgAoASmAuaVgFHQgZAPbNACdT0pgFAgpgKKACkAlAB0oGGc9TmgBT8vvQAE0xBSYBRYBOnagY+Tyyq7Mg/xZoAZQIWi4CZoAMc0wFPBpAHpQAUwCkAcEe5oASgBTQADOaGAGhAJk0MYtIQgNAC8/SgBS7GMIR8oNMY09qQC0ADqA2B0pgIKAA8UgA+tMA4pAJTAWpYCUwFyMdOaAFK84zkE9aQEt3ALefyw4kGAdwoGQZzTEHekAd89qYxPWgBaQg4oA1vD8ET3UtzOu6O1iMu31I6UpMaIJb6+1O7yZmDHkANtVB/QUrJAbOnyWslnqNtAXluDbsz3Lfx8YwM846VLuNGPo1nFe3qxzZ8tQXcA9QO1W9hGhZ3Mt+zf8SmGSyyVKxRAMv0PrUtW6jKaaXGsMs1zcC2iWUxKHQs2R2OOlVcVhV0Vd0UE17HFdyqCsRQnGegJ7GlzBYlbTnl0q1gjhX7Sbh0Y4549T6ClfUDKu4I4Lhoo5hMF4LqMAn29apAQ0wGVIwAoAU0gEpgLmgAoEFABQAUwCgAxQMWmIKACgANABTELmkMKACgBWAGMde9MBKQBTuIKBhQIM0wFoAXNACD3oABj6UALxmgAoEJSGLimAGgAoYgpAOQKSSTjimMZ60ALSuIU9BTAOtMApAJmgYhoAXg9BmkAYpgIOpoAD2oAX0oAKBBQAGmMKBARxUgKwwoPrTGJk9qAAHnk0gFyaLAJ1zxTASgA60gFPAPpQA9xHtQxnLH7wNAEfTtTAWluAfWgA7dOP50DJJ3jkdTFHsGACPegCP6ClcQnT607AGc0ABoAOMc9aAAnB9aQwx6UAHOaAE59aYCnikAGgQlIDX0G4itZriS4/49jCUk4656Ae5/xpSQ0Ni0PUbiHzLaLdBIcr+8XJHbPNO6CxcMaaBp1xG8qPf3C7NiHPlr3zS3Y9jI0+8awvUuFG7HDKf4geopvUSNCG50m0nFzC14xU7lgOAM9snPSlqxle41FbnTPJYN57XLTNxxyKOorl0anpk08V/cLOLuMLmNQNrMvQ57ClZ7DIv7dZYYmQHzvPeSRT91lb+GjlC5mXhtmuXa03iE8hXHK+1UhEH0pgMqBkqRb4nfzFBX+Enk0wIqAFzQAdqAEoAdt460ANpgLQIKACgANAwoEKMd6ADvTAKACgAoAXNACUwFzQAZ9qADrQBI0RWJXyDu7elICOmAtFxBQAnegBygknAzQAmM/hQACmAv4UhCUxi59qGAlAC5oAPwoASgCRVj8lmJ+cdBSAjpgKDimIXIoATrSYwJxUgWJmSSJXjREI4Kg8mmBWzn2oAegDZ3NjigBpoAU42jHWmAlABmkIQ00MXpSYAaEISmMXFJgSwNAHJnQlSOMetIZF2OOeePamAlMQUALkd6LAGaLAA57UgEoASgA6imMWkIcqM6luy0DEDMBxQA00CF7e9AC49aEAgoAM/lSGG75SvXNACcUAFIQUDFzke9UAnagQpYlQuflHQUhjklePOyRlz12nFADSDnPXPegBO/vSAO9MBT9KBCCgYUAApAGPagBlIYUgCmAUAKelACUAFAC9KACgQUAFOwwoEFABQAvSmAZoAM0AFABTAKQBRYAzRYBc0AFMBVwWG7gGkAr7A52HK9s0ANpiCkA9XaMnacE0xjc5+tAB0oEGaADNMAoAM0gCnYBc0AJSAKACgAp2AXNFgEzSYBQAfXpQMKBBQAdKACiwCk0AFMQGkMUBPKJJ+bPAoAbmgAoAM4oAKACgAyaLAGaLAGaYBmkAZoAM4oGJRYQZ4oGGeaBC5x7UDA0CDNABnBNAATQAlIAoAdv+TbgZ9aBjelAC5NIBKYBQIKBhmgAoAM8UAOJTyxx83ekA2mAUAGaQBmgBBwaAFpgf/9k=", + "acknowledgements": "Rubin", + "relatedPublishedRafts": "10.11570/25.0065" + }, + "technical": { + "photometry": { + "wavelength": "r’ , g’ , w’ (LCO 1m); g_p, r _p, i _p, z _ s (FTN)", + "brightness": "34", + "errors": "5" + }, + "spectroscopy": "# DATE = 2025-01-31\n# PIPE_VER = 1.17.1\n# PMAP = jwst_1322.pmap\n# \n# Wavelength (microns), flux density (mJy), uncertainty (mJy)\n0.702500 0.008520 0.000057\n0.707500 0.008672 0.000152\n0.712500 0.008581 0.000158\n0.717500 0.008611 0.000035\n0.722500 0.008685 0.000236\n0.727500 0.008698 0.000281\n0.732500 0.008840 0.000337\n0.737500 0.008911 0.000223\n0.742500 0.009085 0.000068\n0.747500 0.009339 0.000077\n0.752500 0.009099 0.000283\n0.757500 0.009083 0.000080\n0.762500 0.009554 0.000182\n0.767500 0.009573 0.000239\n0.772500 0.009470 0.000204\n0.777500 0.009413 0.000156\n0.782500 0.009386 0.000248\n0.787500 0.009661 0.000188\n0.792500 0.009957 0.000121\n0.797500 0.010151 0.000271\n0.802500 0.010194 0.000289\n0.807500 0.010149 0.000218\n0.812500 0.010214 0.000287\n0.817500 0.010268 0.000247\n0.822500 0.010172 0.000156\n0.827500 0.010391 0.000091\n0.832500 0.010540 0.000113\n0.837500 0.010348 0.000326\n0.842500 0.010313 0.000324\n0.847500 0.010329 0.000305\n0.852500 0.010343 0.000298\n0.857500 0.010453 0.000187\n0.862500 0.010533 0.000145\n0.867500 0.010544 0.000206\n0.872500 0.010550 0.000187\n0.877500 0.010612 0.000189\n0.882500 0.010828 0.000269\n0.887500 0.010962 0.000234\n0.892500 0.011032 0.000191\n0.897500 0.010936 0.000179\n0.902500 0.010980 0.000215\n0.907500 0.010974 0.000122\n0.912500 0.011049 0.000140\n0.917500 0.011122 0.000180\n0.922500 0.011236 0.000132\n0.927500 0.011183 0.000190\n0.932500 0.011211 0.000261\n0.937500 0.011268 0.000246\n0.942500 0.011367 0.000210\n0.947500 0.011516 0.000254\n0.952500 0.011558 0.000200\n0.957500 0.011538 0.000356\n0.962500 0.011524 0.000370\n0.967500 0.011571 0.000301\n0.972500 0.011615 0.000255\n0.977500 0.011809 0.000207\n0.982500 0.011889 0.000250\n0.987500 0.011899 0.000256\n0.992500 0.011769 0.000309\n0.997500 0.011821 0.000263\n1.002500 0.011858 0.000268\n1.007500 0.011832 0.000252\n1.012500 0.011880 0.000242\n1.017500 0.011669 0.000305\n1.022500 0.011628 0.000220\n1.027500 0.011654 0.000287\n1.032500 0.011767 0.000232\n1.037500 0.011810 0.000213\n1.042500 0.011832 0.000221\n1.047500 0.011864 0.000279\n1.052500 0.011843 0.000261\n1.057500 0.011899 0.000269\n1.062500 0.011900 0.000254\n1.067500 0.011973 0.000136\n1.072500 0.011861 0.000092\n1.077500 0.011728 0.000177\n1.082500 0.011720 0.000192\n1.087500 0.011814 0.000249\n1.092500 0.011783 0.000275\n1.097500 0.011788 0.000306\n1.102500 0.011842 0.000332\n1.107500 0.011808 0.000383\n1.112500 0.011870 0.000310\n1.117500 0.011860 0.000261\n1.122500 0.011838 0.000171\n1.127500 0.011914 0.000280\n1.132500 0.011797 0.000253\n1.137500 0.011849 0.000321\n1.142500 0.011944 0.000403\n1.147500 0.011997 0.000345\n1.152500 0.011952 0.000260\n1.157500 0.011870 0.000237\n1.162500 0.011845 0.000260\n1.167500 0.011886 0.000257\n1.172500 0.011927 0.000342\n1.177500 0.011904 0.000309\n1.182500 0.011912 0.000231\n1.187500 0.011881 0.000215\n1.192500 0.011901 0.000177\n1.197500 0.011884 0.000265\n1.202500 0.011879 0.000267\n1.207500 0.011965 0.000265\n1.212500 0.011987 0.000283\n1.217500 0.011818 0.000346\n1.222500 0.011871 0.000381\n1.227500 0.012059 0.000326\n1.232500 0.012028 0.000288\n1.237500 0.012192 0.000135\n1.242500 0.012154 0.000236\n1.247500 0.012081 0.000172\n1.252500 0.012170 0.000301\n1.257500 0.012142 0.000325\n1.262500 0.012140 0.000302\n1.267500 0.012076 0.000297\n1.272500 0.011801 0.000269\n1.277500 0.011842 0.000319\n1.282500 0.011854 0.000314\n1.287500 0.011975 0.000356\n1.292500 0.012057 0.000295\n1.297500 0.012031 0.000291\n1.302500 0.011965 0.000353\n1.307500 0.011893 0.000373\n1.312500 0.011848 0.000400\n1.317500 0.011760 0.000309\n1.322500 0.011729 0.000268\n1.327500 0.011639 0.000274\n1.332500 0.011646 0.000321\n1.337500 0.011657 0.000333\n1.342500 0.011607 0.000319\n1.347500 0.011597 0.000363\n1.352500 0.011594 0.000363\n1.357500 0.011602 0.000290\n1.362500 0.011603 0.000307\n1.367500 0.011638 0.000251\n1.372500 0.011580 0.000254\n1.377500 0.011509 0.000182\n1.382500 0.011486 0.000218\n1.387500 0.011430 0.000191\n1.392500 0.011415 0.000186\n1.397500 0.011340 0.000144\n1.402500 0.011306 0.000123\n1.407500 0.011319 0.000163\n1.412500 0.011334 0.000161\n1.417500 0.011335 0.000162\n1.422500 0.011315 0.000230\n1.427500 0.011307 0.000233\n1.432500 0.011281 0.000213\n1.437500 0.011264 0.000188\n1.442500 0.011300 0.000244\n1.447500 0.011305 0.000231\n1.452500 0.011268 0.000215\n1.457500 0.011262 0.000153\n1.462500 0.011168 0.000212\n1.467500 0.011127 0.000239\n1.472500 0.011145 0.000197\n1.477500 0.011129 0.000250\n1.482500 0.010959 0.000401\n1.487500 0.010925 0.000450\n1.492500 0.010844 0.000355\n1.497500 0.010805 0.000211\n1.502500 0.010876 0.000190\n1.507500 0.010927 0.000172\n1.512500 0.010988 0.000180\n1.517500 0.011035 0.000230\n1.522500 0.010977 0.000280\n1.527500 0.010963 0.000284\n1.532500 0.010885 0.000327\n1.537500 0.010755 0.000287\n1.542500 0.010757 0.000331\n1.547500 0.010713 0.000374\n1.552500 0.010641 0.000295\n1.557500 0.010593 0.000251\n1.562500 0.010474 0.000212\n1.567500 0.010496 0.000225\n1.572500 0.010512 0.000210\n1.577500 0.010381 0.000111\n1.582500 0.010412 0.000125\n1.587500 0.010449 0.000128\n1.592500 0.010412 0.000037\n1.597500 0.010371 0.000057\n1.602500 0.010336 0.000098\n1.607500 0.010350 0.000090\n1.612500 0.010321 0.000085\n1.617500 0.010270 0.000197\n1.622500 0.010163 0.000173\n1.627500 0.010132 0.000172\n1.632500 0.010087 0.000128\n1.637500 0.010070 0.000122\n1.642500 0.010160 0.000234\n1.647500 0.010181 0.000232\n1.652500 0.010245 0.000131\n1.657500 0.010078 0.000211\n1.662500 0.009932 0.000397\n1.667500 0.009830 0.000477\n1.672500 0.009679 0.000390\n1.677500 0.009740 0.000126\n1.682500 0.009848 0.000190\n1.687500 0.009785 0.000126\n1.692500 0.009866 0.000119\n1.697500 0.009868 0.000140\n1.702500 0.009852 0.000169\n1.707500 0.009810 0.000185\n1.712500 0.009657 0.000201\n1.717500 0.009631 0.000217\n1.722500 0.009558 0.000182\n1.727500 0.009548 0.000159\n1.732500 0.009559 0.000179\n1.737500 0.009461 0.000125\n1.742500 0.009450 0.000199\n1.747500 0.009430 0.000194\n1.752500 0.009291 0.000067\n1.757500 0.009198 0.000207\n1.762500 0.009098 0.000209\n1.767500 0.009077 0.000172\n1.772500 0.009035 0.000142\n1.777500 0.008948 0.000241\n1.782500 0.008969 0.000231\n1.787500 0.008991 0.000206\n1.792500 0.008942 0.000244\n1.797500 0.008937 0.000248\n1.802500 0.008859 0.000340\n1.807500 0.008758 0.000281\n1.812500 0.008700 0.000181\n1.817500 0.008720 0.000252\n1.822500 0.008674 0.000272\n1.827500 0.008579 0.000310\n1.832500 0.008533 0.000268\n1.837500 0.008441 0.000274\n1.842500 0.008461 0.000263\n1.847500 0.008561 0.000169\n1.852500 0.008496 0.000233\n1.857500 0.008433 0.000321\n1.862500 0.008393 0.000354\n1.867500 0.008138 0.000235\n1.872500 0.008100 0.000248\n1.877500 0.008123 0.000291\n1.882500 0.008039 0.000210\n1.887500 0.008079 0.000299\n1.892500 0.008088 0.000250\n1.897500 0.008036 0.000250\n1.902500 0.008058 0.000209\n1.907500 0.008102 0.000150\n1.912500 0.007971 0.000241\n1.917500 0.007956 0.000245\n1.922500 0.007847 0.000130\n1.927500 0.007725 0.000161\n1.932500 0.007755 0.000176\n1.937500 0.007735 0.000139\n1.942500 0.007581 0.000256\n1.947500 0.007406 0.000199\n1.952500 0.007255 0.000141\n1.957500 0.007286 0.000205\n1.962500 0.007281 0.000214\n1.967500 0.007314 0.000147\n1.972500 0.007317 0.000152\n1.977500 0.007282 0.000175\n1.982500 0.007232 0.000196\n1.987500 0.007087 0.000323\n1.992500 0.007087 0.000332\n1.997500 0.007048 0.000252\n2.002500 0.007113 0.000175\n2.007500 0.007164 0.000090\n2.012500 0.007120 0.000047\n2.017500 0.007093 0.000069\n2.022500 0.007024 0.000128\n2.027500 0.007009 0.000133\n2.032500 0.007031 0.000129\n2.037500 0.006993 0.000077\n2.042500 0.006945 0.000145\n2.047500 0.006950 0.000156\n2.052500 0.006867 0.000034\n2.057500 0.006854 0.000078\n2.062500 0.006847 0.000111\n2.067500 0.006796 0.000107\n2.072500 0.006757 0.000157\n2.077500 0.006652 0.000189\n2.082500 0.006632 0.000177\n2.087500 0.006599 0.000137\n2.092500 0.006566 0.000175\n2.097500 0.006515 0.000207\n2.102500 0.006513 0.000203\n2.107500 0.006449 0.000086\n2.112500 0.006431 0.000064\n2.117500 0.006472 0.000030\n2.122500 0.006429 0.000051\n2.127500 0.006396 0.000045\n2.132500 0.006333 0.000066\n2.137500 0.006381 0.000096\n2.142500 0.006408 0.000175\n2.147500 0.006343 0.000253\n2.152500 0.006341 0.000230\n2.157500 0.006261 0.000134\n2.162500 0.006241 0.000132\n2.167500 0.006301 0.000077\n2.172500 0.006209 0.000113\n2.177500 0.006195 0.000129\n2.182500 0.006166 0.000138\n2.187500 0.006160 0.000150\n2.192500 0.006156 0.000139\n2.197500 0.006209 0.000060\n2.202500 0.006262 0.000029\n2.207500 0.006250 0.000052\n2.212500 0.006238 0.000146\n2.217500 0.006140 0.000263\n2.222500 0.006136 0.000258\n2.227500 0.005949 0.000100\n2.232500 0.005980 0.000120\n2.237500 0.005986 0.000136\n2.242500 0.005947 0.000119\n2.247500 0.005892 0.000066\n2.252500 0.005868 0.000077\n2.257500 0.005828 0.000089\n2.262500 0.005783 0.000107\n2.267500 0.005711 0.000157\n2.272500 0.005690 0.000159\n2.277500 0.005623 0.000158\n2.282500 0.005613 0.000163\n2.287500 0.005625 0.000183\n2.292500 0.005610 0.000155\n2.297500 0.005570 0.000096\n2.302500 0.005549 0.000050\n2.307500 0.005529 0.000056\n2.312500 0.005458 0.000091\n2.317500 0.005344 0.000084\n2.322500 0.005305 0.000107\n2.327500 0.005325 0.000150\n2.332500 0.005378 0.000127\n2.337500 0.005466 0.000027\n2.342500 0.005444 0.000124\n2.347500 0.005342 0.000175\n2.352500 0.005187 0.000118\n2.357500 0.005287 0.000198\n2.362500 0.005267 0.000093\n2.367500 0.005263 0.000093\n2.372500 0.005320 0.000056\n2.377500 0.005191 0.000126\n2.382500 0.005140 0.000121\n2.387500 0.005081 0.000156\n2.392500 0.005114 0.000198\n2.397500 0.005132 0.000233\n2.402500 0.005086 0.000260\n2.407500 0.005068 0.000241\n2.412500 0.005055 0.000130\n2.417500 0.005079 0.000150\n2.422500 0.005094 0.000087\n2.427500 0.005042 0.000067\n2.432500 0.005036 0.000102\n2.437500 0.004948 0.000076\n2.442500 0.004898 0.000058\n2.447500 0.004887 0.000094\n2.452500 0.004929 0.000078\n2.457500 0.004809 0.000047\n2.462500 0.004771 0.000074\n2.467500 0.004761 0.000062\n2.472500 0.004763 0.000183\n2.477500 0.004638 0.000262\n2.482500 0.004631 0.000250\n2.487500 0.004556 0.000139\n2.492500 0.004551 0.000151\n2.497500 0.004562 0.000182\n2.502500 0.004511 0.000173\n2.507500 0.004565 0.000125\n2.512500 0.004568 0.000129\n2.517500 0.004563 0.000111\n2.522500 0.004570 0.000116\n2.527500 0.004589 0.000103\n2.532500 0.004567 0.000076\n2.537500 0.004493 0.000061\n2.542500 0.004456 0.000084\n2.547500 0.004458 0.000110\n2.552500 0.004470 0.000105\n2.557500 0.004460 0.000091\n2.562500 0.004421 0.000058\n2.567500 0.004315 0.000038\n2.572500 0.004306 0.000112\n2.577500 0.004277 0.000134\n2.582500 0.004256 0.000120\n2.587500 0.004305 0.000088\n2.592500 0.004302 0.000070\n2.597500 0.004294 0.000039\n2.602500 0.004300 0.000079\n2.607500 0.004277 0.000097\n2.612500 0.004288 0.000065\n2.617500 0.004346 0.000048\n2.622500 0.004380 0.000024\n2.627500 0.004286 0.000108\n2.632500 0.004281 0.000094\n2.637500 0.004288 0.000072\n2.642500 0.004224 0.000040\n2.647500 0.004110 0.000118\n2.652500 0.004104 0.000150\n2.657500 0.004078 0.000190\n2.662500 0.004092 0.000239\n2.667500 0.004021 0.000189\n2.672500 0.004037 0.000088\n2.677500 0.003997 0.000092\n2.682500 0.003923 0.000044\n2.687500 0.003809 0.000068\n2.692500 0.003773 0.000082\n2.697500 0.003585 0.000087\n2.702500 0.003472 0.000041\n2.707500 0.003422 0.000025\n2.712500 0.003457 0.000087\n2.717500 0.003439 0.000140\n2.722500 0.003424 0.000087\n2.727500 0.003391 0.000081\n2.732500 0.003329 0.000055\n2.737500 0.003405 0.000086\n2.742500 0.003269 0.000060\n2.747500 0.003154 0.000029\n2.752500 0.003062 0.000037\n2.757500 0.002926 0.000034\n2.762500 0.002789 0.000081\n2.767500 0.002622 0.000050\n2.772500 0.002490 0.000021\n2.777500 0.002436 0.000038\n2.782500 0.002303 0.000023\n2.787500 0.002144 0.000032\n2.792500 0.002067 0.000079\n2.797500 0.001835 0.000039\n2.802500 0.001722 0.000010\n2.807500 0.001690 0.000052\n2.812500 0.001512 0.000026\n2.817500 0.001396 0.000046\n2.822500 0.001320 0.000004\n2.827500 0.001239 0.000051\n2.832500 0.001147 0.000071\n2.837500 0.001040 0.000026\n2.842500 0.000937 0.000025\n2.847500 0.000879 0.000049\n2.852500 0.000778 0.000016\n2.857500 0.000714 0.000003\n2.862500 0.000654 0.000007\n2.867500 0.000594 0.000033\n2.872500 0.000487 0.000008\n2.877500 0.000491 0.000026\n2.882500 0.000440 0.000028\n2.887500 0.000394 0.000017\n2.892500 0.000385 0.000029\n2.897500 0.000388 0.000028\n2.902500 0.000378 0.000027\n2.907500 0.000360 0.000037\n2.912500 0.000358 0.000038\n2.917500 0.000355 0.000079\n2.922500 0.000253 0.000032\n2.927500 0.000277 0.000054\n2.932500 0.000301 0.000024\n2.937500 0.000297 0.000023\n2.942500 0.000300 0.000038\n2.947500 0.000271 0.000011\n2.952500 0.000178 0.000085\n2.957500 0.000206 0.000020\n2.962500 0.000241 0.000019\n2.967500 0.000272 0.000043\n2.972500 0.000265 0.000047\n2.977500 0.000268 0.000051\n2.982500 0.000271 0.000044\n2.987500 0.000260 0.000044\n2.992500 0.000281 0.000033\n2.997500 0.000271 0.000039\n3.002500 0.000258 0.000032\n3.007500 0.000241 0.000012\n3.012500 0.000218 0.000051\n3.017500 0.000203 0.000038\n3.022500 0.000214 0.000034\n3.027500 0.000209 0.000001\n3.032500 0.000223 0.000012\n3.037500 0.000172 0.000015\n3.042500 0.000173 0.000019\n3.047500 0.000143 0.000022\n3.052500 0.000163 0.000040\n3.057500 0.000221 0.000029\n3.062500 0.000215 0.000050\n3.067500 0.000237 0.000053\n3.072500 0.000260 0.000023\n3.077500 0.000299 0.000009\n3.082500 0.000263 0.000017\n3.087500 0.000237 0.000033\n3.092500 0.000257 0.000043\n3.097500 0.000280 0.000050\n3.102500 0.000286 0.000045\n3.107500 0.000203 0.000043\n3.112500 0.000207 0.000041\n3.117500 0.000272 0.000028\n3.122500 0.000303 0.000034\n3.127500 0.000247 0.000011\n3.132500 0.000251 0.000029\n3.137500 0.000260 0.000027\n3.142500 0.000247 0.000041\n3.147500 0.000281 0.000037\n3.152500 0.000266 0.000060\n3.157500 0.000278 0.000025\n3.162500 0.000256 0.000021\n3.167500 0.000291 0.000032\n3.172500 0.000296 0.000020\n3.177500 0.000299 0.000019\n3.182500 0.000322 0.000010\n3.187500 0.000355 0.000032\n3.192500 0.000350 0.000025\n3.197500 0.000283 0.000018\n3.202500 0.000263 0.000050\n3.207500 0.000266 0.000059\n3.212500 0.000294 0.000026\n3.217500 0.000312 0.000027\n3.222500 0.000328 0.000023\n3.227500 0.000336 0.000051\n3.232500 0.000378 0.000022\n3.237500 0.000386 0.000027\n3.242500 0.000356 0.000024\n3.247500 0.000364 0.000028\n3.252500 0.000359 0.000029\n3.257500 0.000451 0.000033\n3.262500 0.000485 0.000013\n3.267500 0.000482 0.000012\n3.272500 0.000490 0.000017\n3.277500 0.000500 0.000009\n3.282500 0.000497 0.000043\n3.287500 0.000495 0.000050\n3.292500 0.000526 0.000021\n3.297500 0.000563 0.000044\n3.302500 0.000555 0.000086\n3.307500 0.000560 0.000068\n3.312500 0.000587 0.000038\n3.317500 0.000574 0.000079\n3.322500 0.000606 0.000084\n3.327500 0.000603 0.000101\n3.332500 0.000550 0.000052\n3.337500 0.000581 0.000023\n3.342500 0.000551 0.000025\n3.347500 0.000561 0.000054\n3.352500 0.000588 0.000069\n3.357500 0.000676 0.000044\n3.362500 0.000674 0.000017\n3.367500 0.000545 0.000044\n3.372500 0.000498 0.000063\n3.377500 0.000513 0.000099\n3.382500 0.000551 0.000109\n3.387500 0.000587 0.000126\n3.392500 0.000575 0.000117\n3.397500 0.000580 0.000081\n3.402500 0.000539 0.000078\n3.407500 0.000555 0.000075\n3.412500 0.000552 0.000023\n3.417500 0.000595 0.000044\n3.422500 0.000538 0.000021\n3.427500 0.000513 0.000044\n3.432500 0.000521 0.000014\n3.437500 0.000606 0.000110\n3.442500 0.000585 0.000113\n3.447500 0.000598 0.000072\n3.452500 0.000613 0.000079\n3.457500 0.000595 0.000066\n3.462500 0.000610 0.000045\n3.467500 0.000619 0.000043\n3.472500 0.000586 0.000041\n3.477500 0.000656 0.000055\n3.482500 0.000699 0.000035\n3.487500 0.000703 0.000056\n3.492500 0.000638 0.000036\n3.497500 0.000663 0.000033\n3.502500 0.000700 0.000012\n3.507500 0.000658 0.000073\n3.512500 0.000615 0.000074\n3.517500 0.000626 0.000019\n3.522500 0.000654 0.000016\n3.527500 0.000706 0.000066\n3.532500 0.000715 0.000037\n3.537500 0.000715 0.000013\n3.542500 0.000692 0.000071\n3.547500 0.000686 0.000077\n3.552500 0.000736 0.000040\n3.557500 0.000771 0.000074\n3.562500 0.000836 0.000113\n3.567500 0.000787 0.000076\n3.572500 0.000770 0.000091\n3.577500 0.000810 0.000135\n3.582500 0.000877 0.000058\n3.587500 0.000901 0.000060\n3.592500 0.000866 0.000036\n3.597500 0.000860 0.000089\n3.602500 0.000893 0.000103\n3.607500 0.000915 0.000049\n3.612500 0.000908 0.000090\n3.617500 0.000885 0.000119\n3.622500 0.000926 0.000150\n3.627500 0.001024 0.000063\n3.632500 0.001063 0.000056\n3.637500 0.001001 0.000082\n3.642500 0.001019 0.000141\n3.647500 0.001012 0.000164\n3.652500 0.001059 0.000152\n3.657500 0.001077 0.000123\n3.662500 0.001045 0.000101\n3.667500 0.000977 0.000028\n3.672500 0.001028 0.000020\n3.677500 0.001015 0.000040\n3.682500 0.000951 0.000038\n3.687500 0.000928 0.000028\n3.692500 0.001154 0.000060\n3.697500 0.001222 0.000066\n3.702500 0.001182 0.000025\n3.707500 0.001130 0.000020\n3.712500 0.001070 0.000054\n3.717500 0.001003 0.000025\n3.722500 0.001073 0.000096\n3.727500 0.001114 0.000086\n3.732500 0.001038 0.000055\n3.737500 0.001037 0.000064\n3.742500 0.001095 0.000107\n3.747500 0.001083 0.000104\n3.752500 0.001038 0.000054\n3.757500 0.001043 0.000147\n3.762500 0.001044 0.000158\n3.767500 0.000986 0.000158\n3.772500 0.001020 0.000131\n3.777500 0.001031 0.000107\n3.782500 0.001028 0.000035\n3.787500 0.001161 0.000073\n3.792500 0.001210 0.000019\n3.797500 0.001196 0.000092\n3.802500 0.001096 0.000113\n3.807500 0.000981 0.000132\n3.812500 0.000867 0.000115\n3.817500 0.001046 0.000221\n3.822500 0.001181 0.000131\n3.827500 0.001155 0.000086\n3.832500 0.001027 0.000106\n3.837500 0.001166 0.000068\n3.842500 0.001188 0.000119\n3.847500 0.001070 0.000041\n3.852500 0.000965 0.000053\n3.857500 0.000935 0.000053\n3.862500 0.001026 0.000103\n3.867500 0.000937 0.000023\n3.872500 0.000919 0.000022\n3.877500 0.000993 0.000108\n3.882500 0.001017 0.000073\n3.887500 0.000909 0.000026\n3.892500 0.000784 0.000125\n3.897500 0.000847 0.000088\n3.902500 0.000886 0.000075\n3.907500 0.000971 0.000034\n3.912500 0.000944 0.000056\n3.917500 0.000901 0.000028\n3.922500 0.000843 0.000046\n3.927500 0.000900 0.000060\n3.932500 0.001042 0.000040\n3.937500 0.001026 0.000060\n3.942500 0.000835 0.000120\n3.947500 0.000836 0.000114\n3.952500 0.000882 0.000047\n3.957500 0.000787 0.000010\n3.962500 0.000743 0.000027\n3.967500 0.000854 0.000042\n3.972500 0.000984 0.000134\n3.977500 0.000915 0.000153\n3.982500 0.000935 0.000149\n3.987500 0.000917 0.000127\n3.992500 0.000919 0.000131\n3.997500 0.001005 0.000164\n4.002500 0.001093 0.000099\n4.007500 0.001091 0.000026\n4.012500 0.001060 0.000103\n4.017500 0.001113 0.000097\n4.022500 0.000886 0.000038\n4.027500 0.000937 0.000077\n4.032500 0.000942 0.000122\n4.037500 0.000925 0.000009\n4.042500 0.000979 0.000124\n4.047500 0.000878 0.000059\n4.052500 0.000905 0.000027\n4.057500 0.000997 0.000074\n4.062500 0.000953 0.000072\n4.067500 0.000988 0.000109\n4.072500 0.001022 0.000086\n4.077500 0.001067 0.000020\n4.082500 0.001071 0.000051\n4.087500 0.001033 0.000141\n4.092500 0.000947 0.000113\n4.097500 0.000987 0.000099\n4.102500 0.000948 0.000083\n4.107500 0.000924 0.000139\n4.112500 0.000970 0.000043\n4.117500 0.001119 0.000076\n4.122500 0.001114 0.000022\n4.127500 0.001069 0.000140\n4.132500 0.001076 0.000098\n4.137500 0.001086 0.000088\n4.142500 0.001108 0.000073\n4.147500 0.001125 0.000092\n4.152500 0.001185 0.000053\n4.157500 0.001034 0.000078\n4.162500 0.000917 0.000069\n4.167500 0.000992 0.000095\n4.172500 0.001006 0.000046\n4.177500 0.000993 0.000061\n4.182500 0.000815 0.000056\n4.187500 0.000749 0.000092\n4.192500 0.000756 0.000038\n4.197500 0.000786 0.000046\n4.202500 0.000921 0.000054\n4.207500 0.000992 0.000072\n4.212500 0.000986 0.000014\n4.217500 0.000818 0.000045\n4.222500 0.000739 0.000150\n4.227500 0.000741 0.000085\n4.232500 0.000599 0.000008\n4.237500 0.000620 0.000017\n4.242500 0.000535 0.000062\n4.247500 0.000295 0.000026\n4.252500 0.000347 0.000109\n4.257500 0.000333 0.000096\n4.262500 0.000232 0.000051\n4.267500 0.000207 0.000028\n4.272500 0.000204 0.000026\n4.277500 0.000249 0.000088\n4.282500 0.000341 0.000027\n4.287500 0.000524 0.000114\n4.292500 0.000508 0.000091\n4.297500 0.000509 0.000121\n4.302500 0.000821 0.000108\n4.307500 0.000937 0.000076\n4.312500 0.000926 0.000166\n4.317500 0.000753 0.000063\n4.322500 0.000835 0.000100\n4.327500 0.000846 0.000060\n4.332500 0.000877 0.000076\n4.337500 0.000856 0.000136\n4.342500 0.000956 0.000167\n4.347500 0.000990 0.000097\n4.352500 0.000833 0.000030\n4.357500 0.000749 0.000083\n4.362500 0.000748 0.000092\n4.367500 0.000731 0.000053\n4.372500 0.000836 0.000142\n4.377500 0.000868 0.000151\n4.382500 0.000835 0.000073\n4.387500 0.000858 0.000021\n4.392500 0.000729 0.000066\n4.397500 0.000660 0.000129\n4.402500 0.000915 0.000037\n4.407500 0.000921 0.000070\n4.412500 0.000851 0.000070\n4.417500 0.000851 0.000055\n4.422500 0.000769 0.000061\n4.427500 0.000709 0.000167\n4.432500 0.000686 0.000052\n4.437500 0.000710 0.000049\n4.442500 0.000792 0.000081\n4.447500 0.000740 0.000074\n4.452500 0.000679 0.000086\n4.457500 0.000705 0.000088\n4.462500 0.000702 0.000015\n4.467500 0.000815 0.000085\n4.472500 0.000809 0.000104\n4.477500 0.000815 0.000147\n4.482500 0.000831 0.000115\n4.487500 0.000678 0.000045\n4.492500 0.000752 0.000130\n4.497500 0.000807 0.000121\n4.502500 0.000869 0.000175\n4.507500 0.000888 0.000235\n4.512500 0.000687 0.000161\n4.517500 0.000598 0.000145\n4.522500 0.000773 0.000121\n4.527500 0.000841 0.000132\n4.532500 0.000803 0.000134\n4.537500 0.000719 0.000116\n4.542500 0.000653 0.000141\n4.547500 0.000642 0.000130\n4.552500 0.000788 0.000095\n4.557500 0.000662 0.000046\n4.562500 0.000673 0.000124\n4.567500 0.000589 0.000077\n4.572500 0.000694 0.000149\n4.577500 0.000653 0.000024\n4.582500 0.000796 0.000063\n4.587500 0.000775 0.000085\n4.592500 0.000773 0.000026\n4.597500 0.000502 0.000068\n4.602500 0.000494 0.000080\n4.607500 0.000538 0.000088\n4.612500 0.000432 0.000084\n4.617500 0.000360 0.000015\n4.622500 0.000360 0.000071\n4.627500 0.000481 0.000037\n4.632500 0.000468 0.000072\n4.637500 0.000506 0.000126\n4.642500 0.000538 0.000036\n4.647500 0.000480 0.000101\n4.652500 0.000474 0.000121\n4.657500 0.000577 0.000153\n4.662500 0.000453 0.000138\n4.667500 0.000597 0.000224\n4.672500 0.000573 0.000012\n4.677500 0.000425 0.000032\n4.682500 0.000431 0.000063\n4.687500 0.000427 0.000075\n4.692500 0.000421 0.000131\n4.697500 0.000472 0.000051\n4.702500 0.000475 0.000115\n4.707500 0.000467 0.000095\n4.712500 0.000394 0.000049\n4.717500 0.000634 0.000140\n4.722500 0.000683 0.000024\n4.727500 0.000734 0.000053\n4.732500 0.000611 0.000096\n4.737500 0.000606 0.000108\n4.742500 0.000880 0.000102\n4.747500 0.000677 0.000124\n4.752500 0.000462 0.000160\n4.757500 0.000688 0.000108\n4.762500 0.000764 0.000117\n4.767500 0.000783 0.000111\n4.772500 0.000871 0.000011\n4.777500 0.000983 0.000082\n4.782500 0.000965 0.000151\n4.787500 0.000774 0.000078\n4.792500 0.000830 0.000036\n4.797500 0.000955 0.000052\n4.802500 0.000809 0.000098\n4.807500 0.000838 0.000088\n4.812500 0.001101 0.000050\n4.817500 0.001347 0.000087\n4.822500 0.001056 0.000101\n4.827500 0.000977 0.000312\n4.832500 0.001108 0.000379\n4.837500 0.001178 0.000150\n4.842500 0.001425 0.000090\n4.847500 0.001342 0.000080\n4.852500 0.001232 0.000326\n4.857500 0.001076 0.000208\n4.862500 0.001194 0.000212\n4.867500 0.000965 0.000257\n4.872500 0.000808 0.000204\n4.877500 0.000890 0.000139\n4.882500 0.001068 0.000047\n4.887500 0.001051 0.000072\n4.892500 0.001088 0.000172\n4.897500 0.001015 0.000122\n4.902500 0.000900 0.000182\n4.907500 0.001014 0.000129\n4.912500 0.000959 0.000110\n4.917500 0.000905 0.000148\n4.922500 0.001053 0.000253\n4.927500 0.000876 0.000164\n4.932500 0.000774 0.000147\n4.937500 0.000717 0.000075\n4.942500 0.000879 0.000182\n4.947500 0.000937 0.000153\n4.952500 0.000830 0.000152\n4.957500 0.000759 0.000102\n4.962500 0.000641 0.000071\n4.967500 0.000851 0.000103\n4.972500 0.000944 0.000080\n4.977500 0.000767 0.000120\n4.982500 0.000506 0.000147\n4.987500 0.000672 0.000075\n4.992500 0.000768 0.000023\n4.997500 0.000921 0.000024\n5.002500 0.000721 0.000169\n5.007500 0.000856 0.000069\n5.012500 0.000884 0.000102\n5.017500 0.000841 0.000055\n5.022500 0.000894 0.000046\n5.027500 0.000712 0.000062\n5.032500 0.000572 0.000076\n5.037500 0.000654 0.000015\n5.042500 0.000817 0.000128\n5.047500 0.000678 0.000100\n5.052500 0.000645 0.000038\n5.057500 0.000599 0.000096\n5.062500 0.000429 0.000109\n5.067500 0.000660 0.000076\n5.072500 0.000624 0.000049\n5.077500 0.000488 0.000093\n5.082500 0.000646 0.000151\n5.087500 0.000718 0.000346\n5.092500 0.000406 0.000174\n5.097500 0.000205 0.000080\n", + "astrometry": "", + "ephemeris": "*******************************************************************************\nJPL/HORIZONS 2024 McLaughlin (1952 UR) 2025-May-20 13:31:19\nRec #: 2024 (+COV) Soln.date: 2025-Apr-09_13:16:57 # obs: 4784 (1938-2025)\n \nIAU76/J2000 helio. ecliptic osc. elements (au, days, deg., period=Julian yrs):\n \n EPOCH= 2458400.5 ! 2018-Oct-09.00 (TDB) Residual RMS= .2415\n EC= .1382107439327946 QR= 2.004699739453365 TP= 2458839.6598570347\n OM= 69.22440795966661 W= 291.3108867001728 IN= 7.312173370871046\n A= 2.326206465606055 MA= 238.0015476925796 ADIST= 2.647713191758745\n PER= 3.54798 N= .277799645 ANGMOM= .025984685\n DAN= 2.17264 DDN= 2.40244 L= .6941802\n B= -6.8097269 MOID= 1.01530004 TP= 2019-Dec-22.1598570347\n \nAsteroid physical parameters (km, seconds, rotational period in hours):\n GM= n.a. RAD= 3.9575 ROTPER= 1.15\n H= 13. G= .150 B-V= n.a.\n ALBEDO= .173 STYP= n.a.\n\nASTEROID comments: \n1: soln ref.= JPL#65, OCC=0\n2: source=ORB\n*******************************************************************************\n\n\n*******************************************************************************\nEphemeris / WWW_USER Tue May 20 13:31:19 2025 Pasadena, USA / Horizons \n*******************************************************************************\nTarget body name: 2024 McLaughlin (1952 UR) {source: JPL#65}\nCenter body name: Earth (399) {source: DE441}\nCenter-site name: GEOCENTRIC\n*******************************************************************************\nStart time : A.D. 2025-Mar-19 00:00:00.0000 UT \nStop time : A.D. 2025-Apr-18 00:00:00.0000 UT \nStep-size : 1440 minutes\n*******************************************************************************\nTarget pole/equ : undefined\nTarget radii : 3.957 km \nCenter geodetic : 0.0, 0.0, -6378.137 {E-lon(deg),Lat(deg),Alt(km)}\nCenter cylindric: 0.0, 0.0, 0.0 {E-lon(deg),Dxy(km),Dz(km)}\nCenter pole/equ : ITRF93 {East-longitude positive}\nCenter radii : 6378.137, 6378.137, 6356.752 km {Equator_a, b, pole_c} \nTarget primary : Sun\nVis. interferer : MOON (R_eq= 1737.400) km {source: DE441}\nRel. light bend : Sun {source: DE441}\nRel. lght bnd GM: 1.3271E+11 km^3/s^2 \nSmall-body perts: Yes {source: SB441-N16}\nAtmos refraction: NO (AIRLESS)\nRA format : HMS\nTime format : CAL \nCalendar mode : Mixed Julian/Gregorian\nEOP file : eop.250519.p250815 \nEOP coverage : DATA-BASED 1962-JAN-20 TO 2025-MAY-19. PREDICTS-> 2025-AUG-14\nUnits conversion: 1 au= 149597870.700 km, c= 299792.458 km/s, 1 day= 86400.0 s \nTable cut-offs 1: Elevation (-90.0deg=NO ),Airmass (>38.000=NO), Daylight (NO )\nTable cut-offs 2: Solar elongation ( 0.0,180.0=NO ),Local Hour Angle( 0.0=NO )\nTable cut-offs 3: RA/DEC angular rate ( 0.0=NO ) \n*******************************************************************************\nInitial IAU76/J2000 heliocentric ecliptic osculating elements (au, days, deg.):\n EPOCH= 2458400.5 ! 2018-Oct-09.00 (TDB) Residual RMS= .2415 \n EC= .1382107439327946 QR= 2.004699739453365 TP= 2458839.6598570347 \n OM= 69.22440795966661 W= 291.3108867001728 IN= 7.312173370871046 \n Equivalent ICRF heliocentric cartesian coordinates (au, au/d):\n X=-1.736228608220124E+00 Y=-1.728027167124669E+00 Z=-6.128995280358761E-01\n VX= 8.146690433252512E-03 VY=-5.242549373162131E-03 VZ=-3.648855068563980E-03\nAsteroid physical parameters (km, seconds, rotational period in hours): \n GM= n.a. RAD= 3.9575 ROTPER= 1.15 \n H= 13. G= .150 B-V= n.a. \n ALBEDO= .173 STYP= n.a. \n******************************************************************************************************************************************************************************\n Date__(UT)__HR:MN R.A._____(ICRF)_____DEC APmag S-brt delta deldot S-O-T /r S-T-O Sky_motion Sky_mot_PA RelVel-ANG Lun_Sky_Brt sky_SNR\n******************************************************************************************************************************************************************************\n$$SOE\n 2025-Mar-19 00:00 11 49 21.99 +13 25 56.3 16.627 5.446 1.66461573348104 2.2321203 167.2099 /T 4.7779 0.6375890 289.49796 9.8643337 n.a. n.a.\n 2025-Mar-20 00:00 11 48 22.72 +13 30 58.0 16.641 5.458 1.66604536497299 2.7182936 166.5915 /T 5.0062 0.6341458 289.00014 12.010600 n.a. n.a.\n 2025-Mar-21 00:00 11 47 23.60 +13 35 50.5 16.657 5.472 1.66775529432523 3.2027152 165.8930 /T 5.2636 0.6300291 288.49583 14.147353 n.a. n.a.\n 2025-Mar-22 00:00 11 46 24.71 +13 40 33.2 16.674 5.486 1.66974442690953 3.6850963 165.1265 /T 5.5454 0.6252456 287.98401 16.273195 n.a. n.a.\n 2025-Mar-23 00:00 11 45 26.11 +13 45 05.9 16.692 5.501 1.67201149998313 4.1651432 164.3026 /T 5.8474 0.6198026 287.46359 18.386782 n.a. n.a.\n 2025-Mar-24 00:00 11 44 27.86 +13 49 28.4 16.711 5.516 1.67455507892620 4.6425535 163.4308 /T 6.1659 0.6137084 286.93339 20.486824 n.a. n.a.\n 2025-Mar-25 00:00 11 43 30.03 +13 53 40.2 16.731 5.532 1.67737355000323 5.1170075 162.5190 /T 6.4979 0.6069718 286.39217 22.572065 n.a. n.a.\n 2025-Mar-26 00:00 11 42 32.70 +13 57 41.2 16.751 5.547 1.68046510726646 5.5881571 161.5739 /T 6.8406 0.5996025 285.83855 24.641253 n.a. n.a.\n 2025-Mar-27 00:00 11 41 35.93 +14 01 31.0 16.772 5.563 1.68382773192029 6.0556114 160.6013 /T 7.1918 0.5916112 285.27109 26.693083 n.a. n.a.\n 2025-Mar-28 00:00 11 40 39.77 +14 05 09.5 16.792 5.579 1.68745916476980 6.5189279 159.6061 /T 7.5494 0.5830110 284.68827 28.726142 n.a. n.a.\n 2025-Mar-29 00:00 11 39 44.31 +14 08 36.2 16.814 5.595 1.69135687644602 6.9776154 158.5923 /T 7.9118 0.5738183 284.08851 30.738870 n.a. n.a.\n 2025-Mar-30 00:00 11 38 49.59 +14 11 51.2 16.835 5.610 1.69551804458225 7.4311559 157.5633 /T 8.2776 0.5640549 283.47020 32.729569 n.a. n.a.\n 2025-Mar-31 00:00 11 37 55.68 +14 14 54.1 16.857 5.626 1.69993954887154 7.8790432 156.5220 /T 8.6455 0.5537478 282.83170 34.696479 n.a. n.a.\n 2025-Apr-01 00:00 11 37 02.65 +14 17 44.8 16.878 5.641 1.70461799101502 8.3208278 155.4710 /T 9.0145 0.5429294 282.17127 36.637892 n.a. n.a.\n 2025-Apr-02 00:00 11 36 10.54 +14 20 23.2 16.900 5.656 1.70954973773331 8.7561493 154.4124 /T 9.3836 0.5316358 281.48707 38.552277 n.a. n.a.\n 2025-Apr-03 00:00 11 35 19.42 +14 22 49.2 16.922 5.670 1.71473097649879 9.1847470 153.3479 /T 9.7521 0.5199045 280.77700 40.438355 n.a. n.a.\n 2025-Apr-04 00:00 11 34 29.32 +14 25 02.7 16.943 5.684 1.72015777084575 9.6064495 152.2791 /T 10.1192 0.5077728 280.03867 42.295107 n.a. n.a.\n 2025-Apr-05 00:00 11 33 40.31 +14 27 03.6 16.965 5.698 1.72582610568377 10.0211520 151.2074 /T 10.4844 0.4952771 279.26937 44.121739 n.a. n.a.\n 2025-Apr-06 00:00 11 32 52.42 +14 28 52.0 16.987 5.712 1.73173191924475 10.4287941 150.1339 /T 10.8471 0.4824522 278.46598 45.917624 n.a. n.a.\n 2025-Apr-07 00:00 11 32 05.69 +14 30 27.7 17.009 5.725 1.73787112316987 10.8293418 149.0595 /T 11.2069 0.4693316 277.62495 47.682234 n.a. n.a.\n 2025-Apr-08 00:00 11 31 20.17 +14 31 50.8 17.030 5.739 1.74423961431082 11.2227766 147.9853 /T 11.5633 0.4559478 276.74226 49.415102 n.a. n.a.\n 2025-Apr-09 00:00 11 30 35.89 +14 33 01.4 17.052 5.751 1.75083328170381 11.6090894 146.9118 /T 11.9160 0.4423326 275.81335 51.115777 n.a. n.a.\n 2025-Apr-10 00:00 11 29 52.89 +14 33 59.4 17.073 5.764 1.75764801109354 11.9882775 145.8399 /T 12.2647 0.4285177 274.83304 52.783806 n.a. n.a.\n 2025-Apr-11 00:00 11 29 11.20 +14 34 44.9 17.095 5.776 1.76467968822470 12.3603433 144.7700 /T 12.6090 0.4145348 273.79546 54.418710 n.a. n.a.\n 2025-Apr-12 00:00 11 28 30.84 +14 35 17.9 17.116 5.788 1.77192420128468 12.7252928 143.7027 /T 12.9486 0.4004158 272.69391 56.019959 n.a. n.a.\n 2025-Apr-13 00:00 11 27 51.86 +14 35 38.6 17.138 5.799 1.77937744245755 13.0831336 142.6385 /T 13.2835 0.3861933 271.52076 57.586957 n.a. n.a.\n 2025-Apr-14 00:00 11 27 14.26 +14 35 47.0 17.159 5.810 1.78703530846502 13.4338734 141.5777 /T 13.6133 0.3719008 270.26726 59.119009 n.a. n.a.\n 2025-Apr-15 00:00 11 26 38.08 +14 35 43.2 17.180 5.821 1.79489370009400 13.7775184 140.5207 /T 13.9379 0.3575735 268.92342 60.615288 n.a. n.a.\n 2025-Apr-16 00:00 11 26 03.34 +14 35 27.3 17.201 5.832 1.80294852089333 14.1140719 139.4678 /T 14.2570 0.3432481 267.47777 62.074803 n.a. n.a.\n 2025-Apr-17 00:00 11 25 30.06 +14 34 59.3 17.222 5.842 1.81119567533226 14.4435337 138.4194 /T 14.5706 0.3289644 265.91716 63.496350 n.a. n.a.\n 2025-Apr-18 00:00 11 24 58.25 +14 34 19.4 17.243 5.852 1.81963106666170 14.7658998 137.3756 /T 14.8785 0.3147650 264.22648 64.878469 n.a. n.a.\n$$EOE\n******************************************************************************************************************************************************************************\nColumn meaning:\n \nTIME\n\n Times PRIOR to 1962 are UT1, a mean-solar time closely related to the\nprior but now-deprecated GMT. Times AFTER 1962 are in UTC, the current\ncivil or \"wall-clock\" time-scale. UTC is kept within 0.9 seconds of UT1\nusing integer leap-seconds for 1972 and later years.\n\n Conversion from the internal Barycentric Dynamical Time (TDB) of solar\nsystem dynamics to the non-uniform civil UT time-scale requested for output\nhas not been determined for UTC times after the next July or January 1st.\nTherefore, the last known leap-second is used as a constant over future\nintervals.\n\n Time tags refer to the UT time-scale conversion from TDB on Earth\nregardless of observer location within the solar system, although clock\nrates may differ due to the local gravity field and no analog to \"UT\"\nmay be defined for that location.\n\n Any 'b' symbol in the 1st-column denotes a B.C. date. First-column blank\n(\" \") denotes an A.D. date.\n \nCALENDAR SYSTEM\n\n Mixed calendar mode was active such that calendar dates after AD 1582-Oct-15\n(if any) are in the modern Gregorian system. Dates prior to 1582-Oct-5 (if any)\nare in the Julian calendar system, which is automatically extended for dates\nprior to its adoption on 45-Jan-1 BC. The Julian calendar is useful for\nmatching historical dates. The Gregorian calendar more accurately corresponds\nto the Earth's orbital motion and seasons. A \"Gregorian-only\" calendar mode is\navailable if such physical events are the primary interest.\n\n NOTE: \"n.a.\" in output means quantity \"not available\" at the print-time.\n \n 'R.A._____(ICRF)_____DEC' =\n Astrometric right ascension and declination of the target center with\nrespect to the observing site (coordinate origin) in the reference frame of\nthe planetary ephemeris (ICRF). Compensated for down-leg light-time delay\naberration.\n\n Units: RA in hours-minutes-seconds of time, HH MM SS.ff{ffff}\n DEC in degrees-minutes-seconds of arc, sDD MN SC.f{ffff}\n \n 'APmag S-brt' =\n The asteroids' approximate apparent airless visual magnitude and surface\nbrightness using the standard IAU H-G system magnitude model:\n\n APmag = H + 5*log10(delta) + 5*log10(r) - 2.5*log10((1-G)*phi_1 + G*phi_2)\n\n For solar phase angles >90 deg, the error could exceed 1 magnitude. For\nphase angles >120 degrees, output values are rounded to the nearest integer to\nindicate error could be large and unknown. For Earth-based observers, the\nestimated dimming due to atmospheric absorption (extinction) is available as\na separate, requestable quantity.\n\n Surface brightness is the average airless visual magnitude of a\nsquare-arcsecond of the illuminated portion of the apparent disk. It is\ncomputed only if the target radius is known.\n\n Units: MAGNITUDES & MAGNITUDES PER SQUARE ARCSECOND\n \n 'delta deldot' =\n Apparent range (\"delta\", light-time aberrated) and range-rate (\"delta-dot\")\nof the target center relative to the observer. A positive \"deldot\" means the\ntarget center is moving away from the observer, negative indicates movement\ntoward the observer. Units: AU and KM/S\n \n 'S-O-T /r' =\n Sun-Observer-Target apparent SOLAR ELONGATION ANGLE seen from the observers'\nlocation at print-time.\n\n The '/r' column provides a code indicating the targets' apparent position\nrelative to the Sun in the observers' sky, as described below:\n\n Case A: For an observing location on the surface of a rotating body, that\nbody rotational sense is considered:\n\n /T indicates target TRAILS Sun (evening sky: rises and sets AFTER Sun)\n /L indicates target LEADS Sun (morning sky: rises and sets BEFORE Sun)\n\n Case B: For an observing point that does not have a rotational model (such\nas a spacecraft), the \"leading\" and \"trailing\" condition is defined by the\nobservers' heliocentric ORBITAL motion:\n\n * If continuing in the observers' current direction of heliocentric\n motion would encounter the targets' apparent longitude first, followed\n by the Sun's, the target LEADS the Sun as seen by the observer.\n\n * If the Sun's apparent longitude would be encountered first, followed\n by the targets', the target TRAILS the Sun.\n\n Two other codes can be output:\n /* indicates observer is Sun-centered (undefined)\n /? Target is aligned with Sun center (no lead or trail)\n\n The S-O-T solar elongation angle is numerically the minimum separation\nangle of the Sun and target in the sky in any direction. It does NOT indicate\nthe amount of separation in the leading or trailing directions, which would\nbe defined along the equator of a spherical coordinate system.\n\n Units: DEGREES\n \n 'S-T-O' =\n The Sun-Target-Observer angle; the interior vertex angle at target center\nformed by a vector from the target to the apparent center of the Sun (at\nreflection time on the target) and the apparent vector from target to the\nobserver at print-time. Slightly different from true PHASE ANGLE (requestable\nseparately) at the few arcsecond level in that it includes stellar aberration\non the down-leg from target to observer. Units: DEGREES\n \n 'Sky_motion Sky_mot_PA RelVel-ANG' =\n Total apparent angular rate of the target in the plane-of-sky. \"Sky_mot_PA\"\nis the position angle of the target's direction of motion in the plane-of-sky,\nmeasured counter-clockwise from the apparent of-date north pole direction.\n\"RelVel-ANG\" is the flight path angle of the target's relative motion with\nrespect to the observer's line-of-sight, in the range [-90,+90], where positive\nvalues indicate motion away from the observer, negative values are toward the\nobserver:\n\n -90 = target is moving directly toward the observer\n 0 = target is moving at right angles to the observer's line-of-sight\n +90 = target is moving directly away from the observer\n\nUNITS: ARCSECONDS/MINUTE, DEGREES, DEGREES\n \n 'Lun_Sky_Brt sky_SNR' =\n Sky brightness due to moonlight scattered by Earth's atmosphere at the\ntarget's position in the sky. \"sky_SNR\" is the visual signal-to-noise ratio\n(SNR) of the target's surface brightness relative to background sky. Output\nonly for topocentric Earth observers when both the Moon and target are above\nthe local horizon and the Sun is in astronomical twilight (or further) below\nthe horizon, and the target is not the Moon or Sun. If all conditions are\nnot met, \"n.a.\" is output. Galactic brightness, local sky light-pollution\nand weather are NOT considered. Lunar opposition surge is considered. The\nvalue returned is accurate under ideal conditions at the approximately 8-23%\nlevel, so is a useful but not definitive value.\n\n If the target-body radius is also known, \"sky_SNR\" is output. This is the\napproximate visual signal-to-noise ratio of the target's brightness divided\nby lunar sky brightness. When sky_SNR < 1, the target is dimmer than the\nideal moonlight-scattering background sky, so unlikely to be detectable at\nvisual wavelengths. In practice, visibility requires sky_SNR > 1 and a\ndetector sensitive enough to reach the target's magnitude, even if it isn't\nwashed out by moonlight. When relating magnitudes and brightness values,\nkeep in mind their logarithmic relationship m2-m1 = -2.5*log_10(b2/b1).\n\n UNITS: VISUAL MAGNITUDES / ARCSECOND^2, and unitless ratio\n\nComputations by ...\n\n Solar System Dynamics Group, Horizons On-Line Ephemeris System\n 4800 Oak Grove Drive, Jet Propulsion Laboratory\n Pasadena, CA 91109 USA\n\n General site: https://ssd.jpl.nasa.gov/\n Mailing list: https://ssd.jpl.nasa.gov/email_list.html\n System news : https://ssd.jpl.nasa.gov/horizons/news.html\n User Guide : https://ssd.jpl.nasa.gov/horizons/manual.html\n Connect : browser https://ssd.jpl.nasa.gov/horizons/app.html#/x\n API https://ssd-api.jpl.nasa.gov/doc/horizons.html\n command-line telnet ssd.jpl.nasa.gov 6775\n e-mail/batch https://ssd.jpl.nasa.gov/ftp/ssd/horizons_batch.txt\n scripts https://ssd.jpl.nasa.gov/ftp/ssd/SCRIPTS\n Author : Jon.D.Giorgini@jpl.nasa.gov\n\n******************************************************************************************************************************************************************************\n", + "orbitalElements": "# DATE = 2025-01-31\n# PIPE_VER = 1.17.1\n# PMAP = jwst_1322.pmap\n# \n# Wavelength (microns), flux density (mJy), uncertainty (mJy)\n0.702500 0.008520 0.000057\n0.707500 0.008672 0.000152\n0.712500 0.008581 0.000158\n0.717500 0.008611 0.000035\n0.722500 0.008685 0.000236\n0.727500 0.008698 0.000281\n0.732500 0.008840 0.000337\n0.737500 0.008911 0.000223\n0.742500 0.009085 0.000068\n0.747500 0.009339 0.000077\n0.752500 0.009099 0.000283\n0.757500 0.009083 0.000080\n0.762500 0.009554 0.000182\n0.767500 0.009573 0.000239\n0.772500 0.009470 0.000204\n0.777500 0.009413 0.000156\n0.782500 0.009386 0.000248\n0.787500 0.009661 0.000188\n0.792500 0.009957 0.000121\n0.797500 0.010151 0.000271\n0.802500 0.010194 0.000289\n0.807500 0.010149 0.000218\n0.812500 0.010214 0.000287\n0.817500 0.010268 0.000247\n0.822500 0.010172 0.000156\n0.827500 0.010391 0.000091\n0.832500 0.010540 0.000113\n0.837500 0.010348 0.000326\n0.842500 0.010313 0.000324\n0.847500 0.010329 0.000305\n0.852500 0.010343 0.000298\n0.857500 0.010453 0.000187\n0.862500 0.010533 0.000145\n0.867500 0.010544 0.000206\n0.872500 0.010550 0.000187\n0.877500 0.010612 0.000189\n0.882500 0.010828 0.000269\n0.887500 0.010962 0.000234\n0.892500 0.011032 0.000191\n0.897500 0.010936 0.000179\n0.902500 0.010980 0.000215\n0.907500 0.010974 0.000122\n0.912500 0.011049 0.000140\n0.917500 0.011122 0.000180\n0.922500 0.011236 0.000132\n0.927500 0.011183 0.000190\n0.932500 0.011211 0.000261\n0.937500 0.011268 0.000246\n0.942500 0.011367 0.000210\n0.947500 0.011516 0.000254\n0.952500 0.011558 0.000200\n0.957500 0.011538 0.000356\n0.962500 0.011524 0.000370\n0.967500 0.011571 0.000301\n0.972500 0.011615 0.000255\n0.977500 0.011809 0.000207\n0.982500 0.011889 0.000250\n0.987500 0.011899 0.000256\n0.992500 0.011769 0.000309\n0.997500 0.011821 0.000263\n1.002500 0.011858 0.000268\n1.007500 0.011832 0.000252\n1.012500 0.011880 0.000242\n1.017500 0.011669 0.000305\n1.022500 0.011628 0.000220\n1.027500 0.011654 0.000287\n1.032500 0.011767 0.000232\n1.037500 0.011810 0.000213\n1.042500 0.011832 0.000221\n1.047500 0.011864 0.000279\n1.052500 0.011843 0.000261\n1.057500 0.011899 0.000269\n1.062500 0.011900 0.000254\n1.067500 0.011973 0.000136\n1.072500 0.011861 0.000092\n1.077500 0.011728 0.000177\n1.082500 0.011720 0.000192\n1.087500 0.011814 0.000249\n1.092500 0.011783 0.000275\n1.097500 0.011788 0.000306\n1.102500 0.011842 0.000332\n1.107500 0.011808 0.000383\n1.112500 0.011870 0.000310\n1.117500 0.011860 0.000261\n1.122500 0.011838 0.000171\n1.127500 0.011914 0.000280\n1.132500 0.011797 0.000253\n1.137500 0.011849 0.000321\n1.142500 0.011944 0.000403\n1.147500 0.011997 0.000345\n1.152500 0.011952 0.000260\n1.157500 0.011870 0.000237\n1.162500 0.011845 0.000260\n1.167500 0.011886 0.000257\n1.172500 0.011927 0.000342\n1.177500 0.011904 0.000309\n1.182500 0.011912 0.000231\n1.187500 0.011881 0.000215\n1.192500 0.011901 0.000177\n1.197500 0.011884 0.000265\n1.202500 0.011879 0.000267\n1.207500 0.011965 0.000265\n1.212500 0.011987 0.000283\n1.217500 0.011818 0.000346\n1.222500 0.011871 0.000381\n1.227500 0.012059 0.000326\n1.232500 0.012028 0.000288\n1.237500 0.012192 0.000135\n1.242500 0.012154 0.000236\n1.247500 0.012081 0.000172\n1.252500 0.012170 0.000301\n1.257500 0.012142 0.000325\n1.262500 0.012140 0.000302\n1.267500 0.012076 0.000297\n1.272500 0.011801 0.000269\n1.277500 0.011842 0.000319\n1.282500 0.011854 0.000314\n1.287500 0.011975 0.000356\n1.292500 0.012057 0.000295\n1.297500 0.012031 0.000291\n1.302500 0.011965 0.000353\n1.307500 0.011893 0.000373\n1.312500 0.011848 0.000400\n1.317500 0.011760 0.000309\n1.322500 0.011729 0.000268\n1.327500 0.011639 0.000274\n1.332500 0.011646 0.000321\n1.337500 0.011657 0.000333\n1.342500 0.011607 0.000319\n1.347500 0.011597 0.000363\n1.352500 0.011594 0.000363\n1.357500 0.011602 0.000290\n1.362500 0.011603 0.000307\n1.367500 0.011638 0.000251\n1.372500 0.011580 0.000254\n1.377500 0.011509 0.000182\n1.382500 0.011486 0.000218\n1.387500 0.011430 0.000191\n1.392500 0.011415 0.000186\n1.397500 0.011340 0.000144\n1.402500 0.011306 0.000123\n1.407500 0.011319 0.000163\n1.412500 0.011334 0.000161\n1.417500 0.011335 0.000162\n1.422500 0.011315 0.000230\n1.427500 0.011307 0.000233\n1.432500 0.011281 0.000213\n1.437500 0.011264 0.000188\n1.442500 0.011300 0.000244\n1.447500 0.011305 0.000231\n1.452500 0.011268 0.000215\n1.457500 0.011262 0.000153\n1.462500 0.011168 0.000212\n1.467500 0.011127 0.000239\n1.472500 0.011145 0.000197\n1.477500 0.011129 0.000250\n1.482500 0.010959 0.000401\n1.487500 0.010925 0.000450\n1.492500 0.010844 0.000355\n1.497500 0.010805 0.000211\n1.502500 0.010876 0.000190\n1.507500 0.010927 0.000172\n1.512500 0.010988 0.000180\n1.517500 0.011035 0.000230\n1.522500 0.010977 0.000280\n1.527500 0.010963 0.000284\n1.532500 0.010885 0.000327\n1.537500 0.010755 0.000287\n1.542500 0.010757 0.000331\n1.547500 0.010713 0.000374\n1.552500 0.010641 0.000295\n1.557500 0.010593 0.000251\n1.562500 0.010474 0.000212\n1.567500 0.010496 0.000225\n1.572500 0.010512 0.000210\n1.577500 0.010381 0.000111\n1.582500 0.010412 0.000125\n1.587500 0.010449 0.000128\n1.592500 0.010412 0.000037\n1.597500 0.010371 0.000057\n1.602500 0.010336 0.000098\n1.607500 0.010350 0.000090\n1.612500 0.010321 0.000085\n1.617500 0.010270 0.000197\n1.622500 0.010163 0.000173\n1.627500 0.010132 0.000172\n1.632500 0.010087 0.000128\n1.637500 0.010070 0.000122\n1.642500 0.010160 0.000234\n1.647500 0.010181 0.000232\n1.652500 0.010245 0.000131\n1.657500 0.010078 0.000211\n1.662500 0.009932 0.000397\n1.667500 0.009830 0.000477\n1.672500 0.009679 0.000390\n1.677500 0.009740 0.000126\n1.682500 0.009848 0.000190\n1.687500 0.009785 0.000126\n1.692500 0.009866 0.000119\n1.697500 0.009868 0.000140\n1.702500 0.009852 0.000169\n1.707500 0.009810 0.000185\n1.712500 0.009657 0.000201\n1.717500 0.009631 0.000217\n1.722500 0.009558 0.000182\n1.727500 0.009548 0.000159\n1.732500 0.009559 0.000179\n1.737500 0.009461 0.000125\n1.742500 0.009450 0.000199\n1.747500 0.009430 0.000194\n1.752500 0.009291 0.000067\n1.757500 0.009198 0.000207\n1.762500 0.009098 0.000209\n1.767500 0.009077 0.000172\n1.772500 0.009035 0.000142\n1.777500 0.008948 0.000241\n1.782500 0.008969 0.000231\n1.787500 0.008991 0.000206\n1.792500 0.008942 0.000244\n1.797500 0.008937 0.000248\n1.802500 0.008859 0.000340\n1.807500 0.008758 0.000281\n1.812500 0.008700 0.000181\n1.817500 0.008720 0.000252\n1.822500 0.008674 0.000272\n1.827500 0.008579 0.000310\n1.832500 0.008533 0.000268\n1.837500 0.008441 0.000274\n1.842500 0.008461 0.000263\n1.847500 0.008561 0.000169\n1.852500 0.008496 0.000233\n1.857500 0.008433 0.000321\n1.862500 0.008393 0.000354\n1.867500 0.008138 0.000235\n1.872500 0.008100 0.000248\n1.877500 0.008123 0.000291\n1.882500 0.008039 0.000210\n1.887500 0.008079 0.000299\n1.892500 0.008088 0.000250\n1.897500 0.008036 0.000250\n1.902500 0.008058 0.000209\n1.907500 0.008102 0.000150\n1.912500 0.007971 0.000241\n1.917500 0.007956 0.000245\n1.922500 0.007847 0.000130\n1.927500 0.007725 0.000161\n1.932500 0.007755 0.000176\n1.937500 0.007735 0.000139\n1.942500 0.007581 0.000256\n1.947500 0.007406 0.000199\n1.952500 0.007255 0.000141\n1.957500 0.007286 0.000205\n1.962500 0.007281 0.000214\n1.967500 0.007314 0.000147\n1.972500 0.007317 0.000152\n1.977500 0.007282 0.000175\n1.982500 0.007232 0.000196\n1.987500 0.007087 0.000323\n1.992500 0.007087 0.000332\n1.997500 0.007048 0.000252\n2.002500 0.007113 0.000175\n2.007500 0.007164 0.000090\n2.012500 0.007120 0.000047\n2.017500 0.007093 0.000069\n2.022500 0.007024 0.000128\n2.027500 0.007009 0.000133\n2.032500 0.007031 0.000129\n2.037500 0.006993 0.000077\n2.042500 0.006945 0.000145\n2.047500 0.006950 0.000156\n2.052500 0.006867 0.000034\n2.057500 0.006854 0.000078\n2.062500 0.006847 0.000111\n2.067500 0.006796 0.000107\n2.072500 0.006757 0.000157\n2.077500 0.006652 0.000189\n2.082500 0.006632 0.000177\n2.087500 0.006599 0.000137\n2.092500 0.006566 0.000175\n2.097500 0.006515 0.000207\n2.102500 0.006513 0.000203\n2.107500 0.006449 0.000086\n2.112500 0.006431 0.000064\n2.117500 0.006472 0.000030\n2.122500 0.006429 0.000051\n2.127500 0.006396 0.000045\n2.132500 0.006333 0.000066\n2.137500 0.006381 0.000096\n2.142500 0.006408 0.000175\n2.147500 0.006343 0.000253\n2.152500 0.006341 0.000230\n2.157500 0.006261 0.000134\n2.162500 0.006241 0.000132\n2.167500 0.006301 0.000077\n2.172500 0.006209 0.000113\n2.177500 0.006195 0.000129\n2.182500 0.006166 0.000138\n2.187500 0.006160 0.000150\n2.192500 0.006156 0.000139\n2.197500 0.006209 0.000060\n2.202500 0.006262 0.000029\n2.207500 0.006250 0.000052\n2.212500 0.006238 0.000146\n2.217500 0.006140 0.000263\n2.222500 0.006136 0.000258\n2.227500 0.005949 0.000100\n2.232500 0.005980 0.000120\n2.237500 0.005986 0.000136\n2.242500 0.005947 0.000119\n2.247500 0.005892 0.000066\n2.252500 0.005868 0.000077\n2.257500 0.005828 0.000089\n2.262500 0.005783 0.000107\n2.267500 0.005711 0.000157\n2.272500 0.005690 0.000159\n2.277500 0.005623 0.000158\n2.282500 0.005613 0.000163\n2.287500 0.005625 0.000183\n2.292500 0.005610 0.000155\n2.297500 0.005570 0.000096\n2.302500 0.005549 0.000050\n2.307500 0.005529 0.000056\n2.312500 0.005458 0.000091\n2.317500 0.005344 0.000084\n2.322500 0.005305 0.000107\n2.327500 0.005325 0.000150\n2.332500 0.005378 0.000127\n2.337500 0.005466 0.000027\n2.342500 0.005444 0.000124\n2.347500 0.005342 0.000175\n2.352500 0.005187 0.000118\n2.357500 0.005287 0.000198\n2.362500 0.005267 0.000093\n2.367500 0.005263 0.000093\n2.372500 0.005320 0.000056\n2.377500 0.005191 0.000126\n2.382500 0.005140 0.000121\n2.387500 0.005081 0.000156\n2.392500 0.005114 0.000198\n2.397500 0.005132 0.000233\n2.402500 0.005086 0.000260\n2.407500 0.005068 0.000241\n2.412500 0.005055 0.000130\n2.417500 0.005079 0.000150\n2.422500 0.005094 0.000087\n2.427500 0.005042 0.000067\n2.432500 0.005036 0.000102\n2.437500 0.004948 0.000076\n2.442500 0.004898 0.000058\n2.447500 0.004887 0.000094\n2.452500 0.004929 0.000078\n2.457500 0.004809 0.000047\n2.462500 0.004771 0.000074\n2.467500 0.004761 0.000062\n2.472500 0.004763 0.000183\n2.477500 0.004638 0.000262\n2.482500 0.004631 0.000250\n2.487500 0.004556 0.000139\n2.492500 0.004551 0.000151\n2.497500 0.004562 0.000182\n2.502500 0.004511 0.000173\n2.507500 0.004565 0.000125\n2.512500 0.004568 0.000129\n2.517500 0.004563 0.000111\n2.522500 0.004570 0.000116\n2.527500 0.004589 0.000103\n2.532500 0.004567 0.000076\n2.537500 0.004493 0.000061\n2.542500 0.004456 0.000084\n2.547500 0.004458 0.000110\n2.552500 0.004470 0.000105\n2.557500 0.004460 0.000091\n2.562500 0.004421 0.000058\n2.567500 0.004315 0.000038\n2.572500 0.004306 0.000112\n2.577500 0.004277 0.000134\n2.582500 0.004256 0.000120\n2.587500 0.004305 0.000088\n2.592500 0.004302 0.000070\n2.597500 0.004294 0.000039\n2.602500 0.004300 0.000079\n2.607500 0.004277 0.000097\n2.612500 0.004288 0.000065\n2.617500 0.004346 0.000048\n2.622500 0.004380 0.000024\n2.627500 0.004286 0.000108\n2.632500 0.004281 0.000094\n2.637500 0.004288 0.000072\n2.642500 0.004224 0.000040\n2.647500 0.004110 0.000118\n2.652500 0.004104 0.000150\n2.657500 0.004078 0.000190\n2.662500 0.004092 0.000239\n2.667500 0.004021 0.000189\n2.672500 0.004037 0.000088\n2.677500 0.003997 0.000092\n2.682500 0.003923 0.000044\n2.687500 0.003809 0.000068\n2.692500 0.003773 0.000082\n2.697500 0.003585 0.000087\n2.702500 0.003472 0.000041\n2.707500 0.003422 0.000025\n2.712500 0.003457 0.000087\n2.717500 0.003439 0.000140\n2.722500 0.003424 0.000087\n2.727500 0.003391 0.000081\n2.732500 0.003329 0.000055\n2.737500 0.003405 0.000086\n2.742500 0.003269 0.000060\n2.747500 0.003154 0.000029\n2.752500 0.003062 0.000037\n2.757500 0.002926 0.000034\n2.762500 0.002789 0.000081\n2.767500 0.002622 0.000050\n2.772500 0.002490 0.000021\n2.777500 0.002436 0.000038\n2.782500 0.002303 0.000023\n2.787500 0.002144 0.000032\n2.792500 0.002067 0.000079\n2.797500 0.001835 0.000039\n2.802500 0.001722 0.000010\n2.807500 0.001690 0.000052\n2.812500 0.001512 0.000026\n2.817500 0.001396 0.000046\n2.822500 0.001320 0.000004\n2.827500 0.001239 0.000051\n2.832500 0.001147 0.000071\n2.837500 0.001040 0.000026\n2.842500 0.000937 0.000025\n2.847500 0.000879 0.000049\n2.852500 0.000778 0.000016\n2.857500 0.000714 0.000003\n2.862500 0.000654 0.000007\n2.867500 0.000594 0.000033\n2.872500 0.000487 0.000008\n2.877500 0.000491 0.000026\n2.882500 0.000440 0.000028\n2.887500 0.000394 0.000017\n2.892500 0.000385 0.000029\n2.897500 0.000388 0.000028\n2.902500 0.000378 0.000027\n2.907500 0.000360 0.000037\n2.912500 0.000358 0.000038\n2.917500 0.000355 0.000079\n2.922500 0.000253 0.000032\n2.927500 0.000277 0.000054\n2.932500 0.000301 0.000024\n2.937500 0.000297 0.000023\n2.942500 0.000300 0.000038\n2.947500 0.000271 0.000011\n2.952500 0.000178 0.000085\n2.957500 0.000206 0.000020\n2.962500 0.000241 0.000019\n2.967500 0.000272 0.000043\n2.972500 0.000265 0.000047\n2.977500 0.000268 0.000051\n2.982500 0.000271 0.000044\n2.987500 0.000260 0.000044\n2.992500 0.000281 0.000033\n2.997500 0.000271 0.000039\n3.002500 0.000258 0.000032\n3.007500 0.000241 0.000012\n3.012500 0.000218 0.000051\n3.017500 0.000203 0.000038\n3.022500 0.000214 0.000034\n3.027500 0.000209 0.000001\n3.032500 0.000223 0.000012\n3.037500 0.000172 0.000015\n3.042500 0.000173 0.000019\n3.047500 0.000143 0.000022\n3.052500 0.000163 0.000040\n3.057500 0.000221 0.000029\n3.062500 0.000215 0.000050\n3.067500 0.000237 0.000053\n3.072500 0.000260 0.000023\n3.077500 0.000299 0.000009\n3.082500 0.000263 0.000017\n3.087500 0.000237 0.000033\n3.092500 0.000257 0.000043\n3.097500 0.000280 0.000050\n3.102500 0.000286 0.000045\n3.107500 0.000203 0.000043\n3.112500 0.000207 0.000041\n3.117500 0.000272 0.000028\n3.122500 0.000303 0.000034\n3.127500 0.000247 0.000011\n3.132500 0.000251 0.000029\n3.137500 0.000260 0.000027\n3.142500 0.000247 0.000041\n3.147500 0.000281 0.000037\n3.152500 0.000266 0.000060\n3.157500 0.000278 0.000025\n3.162500 0.000256 0.000021\n3.167500 0.000291 0.000032\n3.172500 0.000296 0.000020\n3.177500 0.000299 0.000019\n3.182500 0.000322 0.000010\n3.187500 0.000355 0.000032\n3.192500 0.000350 0.000025\n3.197500 0.000283 0.000018\n3.202500 0.000263 0.000050\n3.207500 0.000266 0.000059\n3.212500 0.000294 0.000026\n3.217500 0.000312 0.000027\n3.222500 0.000328 0.000023\n3.227500 0.000336 0.000051\n3.232500 0.000378 0.000022\n3.237500 0.000386 0.000027\n3.242500 0.000356 0.000024\n3.247500 0.000364 0.000028\n3.252500 0.000359 0.000029\n3.257500 0.000451 0.000033\n3.262500 0.000485 0.000013\n3.267500 0.000482 0.000012\n3.272500 0.000490 0.000017\n3.277500 0.000500 0.000009\n3.282500 0.000497 0.000043\n3.287500 0.000495 0.000050\n3.292500 0.000526 0.000021\n3.297500 0.000563 0.000044\n3.302500 0.000555 0.000086\n3.307500 0.000560 0.000068\n3.312500 0.000587 0.000038\n3.317500 0.000574 0.000079\n3.322500 0.000606 0.000084\n3.327500 0.000603 0.000101\n3.332500 0.000550 0.000052\n3.337500 0.000581 0.000023\n3.342500 0.000551 0.000025\n3.347500 0.000561 0.000054\n3.352500 0.000588 0.000069\n3.357500 0.000676 0.000044\n3.362500 0.000674 0.000017\n3.367500 0.000545 0.000044\n3.372500 0.000498 0.000063\n3.377500 0.000513 0.000099\n3.382500 0.000551 0.000109\n3.387500 0.000587 0.000126\n3.392500 0.000575 0.000117\n3.397500 0.000580 0.000081\n3.402500 0.000539 0.000078\n3.407500 0.000555 0.000075\n3.412500 0.000552 0.000023\n3.417500 0.000595 0.000044\n3.422500 0.000538 0.000021\n3.427500 0.000513 0.000044\n3.432500 0.000521 0.000014\n3.437500 0.000606 0.000110\n3.442500 0.000585 0.000113\n3.447500 0.000598 0.000072\n3.452500 0.000613 0.000079\n3.457500 0.000595 0.000066\n3.462500 0.000610 0.000045\n3.467500 0.000619 0.000043\n3.472500 0.000586 0.000041\n3.477500 0.000656 0.000055\n3.482500 0.000699 0.000035\n3.487500 0.000703 0.000056\n3.492500 0.000638 0.000036\n3.497500 0.000663 0.000033\n3.502500 0.000700 0.000012\n3.507500 0.000658 0.000073\n3.512500 0.000615 0.000074\n3.517500 0.000626 0.000019\n3.522500 0.000654 0.000016\n3.527500 0.000706 0.000066\n3.532500 0.000715 0.000037\n3.537500 0.000715 0.000013\n3.542500 0.000692 0.000071\n3.547500 0.000686 0.000077\n3.552500 0.000736 0.000040\n3.557500 0.000771 0.000074\n3.562500 0.000836 0.000113\n3.567500 0.000787 0.000076\n3.572500 0.000770 0.000091\n3.577500 0.000810 0.000135\n3.582500 0.000877 0.000058\n3.587500 0.000901 0.000060\n3.592500 0.000866 0.000036\n3.597500 0.000860 0.000089\n3.602500 0.000893 0.000103\n3.607500 0.000915 0.000049\n3.612500 0.000908 0.000090\n3.617500 0.000885 0.000119\n3.622500 0.000926 0.000150\n3.627500 0.001024 0.000063\n3.632500 0.001063 0.000056\n3.637500 0.001001 0.000082\n3.642500 0.001019 0.000141\n3.647500 0.001012 0.000164\n3.652500 0.001059 0.000152\n3.657500 0.001077 0.000123\n3.662500 0.001045 0.000101\n3.667500 0.000977 0.000028\n3.672500 0.001028 0.000020\n3.677500 0.001015 0.000040\n3.682500 0.000951 0.000038\n3.687500 0.000928 0.000028\n3.692500 0.001154 0.000060\n3.697500 0.001222 0.000066\n3.702500 0.001182 0.000025\n3.707500 0.001130 0.000020\n3.712500 0.001070 0.000054\n3.717500 0.001003 0.000025\n3.722500 0.001073 0.000096\n3.727500 0.001114 0.000086\n3.732500 0.001038 0.000055\n3.737500 0.001037 0.000064\n3.742500 0.001095 0.000107\n3.747500 0.001083 0.000104\n3.752500 0.001038 0.000054\n3.757500 0.001043 0.000147\n3.762500 0.001044 0.000158\n3.767500 0.000986 0.000158\n3.772500 0.001020 0.000131\n3.777500 0.001031 0.000107\n3.782500 0.001028 0.000035\n3.787500 0.001161 0.000073\n3.792500 0.001210 0.000019\n3.797500 0.001196 0.000092\n3.802500 0.001096 0.000113\n3.807500 0.000981 0.000132\n3.812500 0.000867 0.000115\n3.817500 0.001046 0.000221\n3.822500 0.001181 0.000131\n3.827500 0.001155 0.000086\n3.832500 0.001027 0.000106\n3.837500 0.001166 0.000068\n3.842500 0.001188 0.000119\n3.847500 0.001070 0.000041\n3.852500 0.000965 0.000053\n3.857500 0.000935 0.000053\n3.862500 0.001026 0.000103\n3.867500 0.000937 0.000023\n3.872500 0.000919 0.000022\n3.877500 0.000993 0.000108\n3.882500 0.001017 0.000073\n3.887500 0.000909 0.000026\n3.892500 0.000784 0.000125\n3.897500 0.000847 0.000088\n3.902500 0.000886 0.000075\n3.907500 0.000971 0.000034\n3.912500 0.000944 0.000056\n3.917500 0.000901 0.000028\n3.922500 0.000843 0.000046\n3.927500 0.000900 0.000060\n3.932500 0.001042 0.000040\n3.937500 0.001026 0.000060\n3.942500 0.000835 0.000120\n3.947500 0.000836 0.000114\n3.952500 0.000882 0.000047\n3.957500 0.000787 0.000010\n3.962500 0.000743 0.000027\n3.967500 0.000854 0.000042\n3.972500 0.000984 0.000134\n3.977500 0.000915 0.000153\n3.982500 0.000935 0.000149\n3.987500 0.000917 0.000127\n3.992500 0.000919 0.000131\n3.997500 0.001005 0.000164\n4.002500 0.001093 0.000099\n4.007500 0.001091 0.000026\n4.012500 0.001060 0.000103\n4.017500 0.001113 0.000097\n4.022500 0.000886 0.000038\n4.027500 0.000937 0.000077\n4.032500 0.000942 0.000122\n4.037500 0.000925 0.000009\n4.042500 0.000979 0.000124\n4.047500 0.000878 0.000059\n4.052500 0.000905 0.000027\n4.057500 0.000997 0.000074\n4.062500 0.000953 0.000072\n4.067500 0.000988 0.000109\n4.072500 0.001022 0.000086\n4.077500 0.001067 0.000020\n4.082500 0.001071 0.000051\n4.087500 0.001033 0.000141\n4.092500 0.000947 0.000113\n4.097500 0.000987 0.000099\n4.102500 0.000948 0.000083\n4.107500 0.000924 0.000139\n4.112500 0.000970 0.000043\n4.117500 0.001119 0.000076\n4.122500 0.001114 0.000022\n4.127500 0.001069 0.000140\n4.132500 0.001076 0.000098\n4.137500 0.001086 0.000088\n4.142500 0.001108 0.000073\n4.147500 0.001125 0.000092\n4.152500 0.001185 0.000053\n4.157500 0.001034 0.000078\n4.162500 0.000917 0.000069\n4.167500 0.000992 0.000095\n4.172500 0.001006 0.000046\n4.177500 0.000993 0.000061\n4.182500 0.000815 0.000056\n4.187500 0.000749 0.000092\n4.192500 0.000756 0.000038\n4.197500 0.000786 0.000046\n4.202500 0.000921 0.000054\n4.207500 0.000992 0.000072\n4.212500 0.000986 0.000014\n4.217500 0.000818 0.000045\n4.222500 0.000739 0.000150\n4.227500 0.000741 0.000085\n4.232500 0.000599 0.000008\n4.237500 0.000620 0.000017\n4.242500 0.000535 0.000062\n4.247500 0.000295 0.000026\n4.252500 0.000347 0.000109\n4.257500 0.000333 0.000096\n4.262500 0.000232 0.000051\n4.267500 0.000207 0.000028\n4.272500 0.000204 0.000026\n4.277500 0.000249 0.000088\n4.282500 0.000341 0.000027\n4.287500 0.000524 0.000114\n4.292500 0.000508 0.000091\n4.297500 0.000509 0.000121\n4.302500 0.000821 0.000108\n4.307500 0.000937 0.000076\n4.312500 0.000926 0.000166\n4.317500 0.000753 0.000063\n4.322500 0.000835 0.000100\n4.327500 0.000846 0.000060\n4.332500 0.000877 0.000076\n4.337500 0.000856 0.000136\n4.342500 0.000956 0.000167\n4.347500 0.000990 0.000097\n4.352500 0.000833 0.000030\n4.357500 0.000749 0.000083\n4.362500 0.000748 0.000092\n4.367500 0.000731 0.000053\n4.372500 0.000836 0.000142\n4.377500 0.000868 0.000151\n4.382500 0.000835 0.000073\n4.387500 0.000858 0.000021\n4.392500 0.000729 0.000066\n4.397500 0.000660 0.000129\n4.402500 0.000915 0.000037\n4.407500 0.000921 0.000070\n4.412500 0.000851 0.000070\n4.417500 0.000851 0.000055\n4.422500 0.000769 0.000061\n4.427500 0.000709 0.000167\n4.432500 0.000686 0.000052\n4.437500 0.000710 0.000049\n4.442500 0.000792 0.000081\n4.447500 0.000740 0.000074\n4.452500 0.000679 0.000086\n4.457500 0.000705 0.000088\n4.462500 0.000702 0.000015\n4.467500 0.000815 0.000085\n4.472500 0.000809 0.000104\n4.477500 0.000815 0.000147\n4.482500 0.000831 0.000115\n4.487500 0.000678 0.000045\n4.492500 0.000752 0.000130\n4.497500 0.000807 0.000121\n4.502500 0.000869 0.000175\n4.507500 0.000888 0.000235\n4.512500 0.000687 0.000161\n4.517500 0.000598 0.000145\n4.522500 0.000773 0.000121\n4.527500 0.000841 0.000132\n4.532500 0.000803 0.000134\n4.537500 0.000719 0.000116\n4.542500 0.000653 0.000141\n4.547500 0.000642 0.000130\n4.552500 0.000788 0.000095\n4.557500 0.000662 0.000046\n4.562500 0.000673 0.000124\n4.567500 0.000589 0.000077\n4.572500 0.000694 0.000149\n4.577500 0.000653 0.000024\n4.582500 0.000796 0.000063\n4.587500 0.000775 0.000085\n4.592500 0.000773 0.000026\n4.597500 0.000502 0.000068\n4.602500 0.000494 0.000080\n4.607500 0.000538 0.000088\n4.612500 0.000432 0.000084\n4.617500 0.000360 0.000015\n4.622500 0.000360 0.000071\n4.627500 0.000481 0.000037\n4.632500 0.000468 0.000072\n4.637500 0.000506 0.000126\n4.642500 0.000538 0.000036\n4.647500 0.000480 0.000101\n4.652500 0.000474 0.000121\n4.657500 0.000577 0.000153\n4.662500 0.000453 0.000138\n4.667500 0.000597 0.000224\n4.672500 0.000573 0.000012\n4.677500 0.000425 0.000032\n4.682500 0.000431 0.000063\n4.687500 0.000427 0.000075\n4.692500 0.000421 0.000131\n4.697500 0.000472 0.000051\n4.702500 0.000475 0.000115\n4.707500 0.000467 0.000095\n4.712500 0.000394 0.000049\n4.717500 0.000634 0.000140\n4.722500 0.000683 0.000024\n4.727500 0.000734 0.000053\n4.732500 0.000611 0.000096\n4.737500 0.000606 0.000108\n4.742500 0.000880 0.000102\n4.747500 0.000677 0.000124\n4.752500 0.000462 0.000160\n4.757500 0.000688 0.000108\n4.762500 0.000764 0.000117\n4.767500 0.000783 0.000111\n4.772500 0.000871 0.000011\n4.777500 0.000983 0.000082\n4.782500 0.000965 0.000151\n4.787500 0.000774 0.000078\n4.792500 0.000830 0.000036\n4.797500 0.000955 0.000052\n4.802500 0.000809 0.000098\n4.807500 0.000838 0.000088\n4.812500 0.001101 0.000050\n4.817500 0.001347 0.000087\n4.822500 0.001056 0.000101\n4.827500 0.000977 0.000312\n4.832500 0.001108 0.000379\n4.837500 0.001178 0.000150\n4.842500 0.001425 0.000090\n4.847500 0.001342 0.000080\n4.852500 0.001232 0.000326\n4.857500 0.001076 0.000208\n4.862500 0.001194 0.000212\n4.867500 0.000965 0.000257\n4.872500 0.000808 0.000204\n4.877500 0.000890 0.000139\n4.882500 0.001068 0.000047\n4.887500 0.001051 0.000072\n4.892500 0.001088 0.000172\n4.897500 0.001015 0.000122\n4.902500 0.000900 0.000182\n4.907500 0.001014 0.000129\n4.912500 0.000959 0.000110\n4.917500 0.000905 0.000148\n4.922500 0.001053 0.000253\n4.927500 0.000876 0.000164\n4.932500 0.000774 0.000147\n4.937500 0.000717 0.000075\n4.942500 0.000879 0.000182\n4.947500 0.000937 0.000153\n4.952500 0.000830 0.000152\n4.957500 0.000759 0.000102\n4.962500 0.000641 0.000071\n4.967500 0.000851 0.000103\n4.972500 0.000944 0.000080\n4.977500 0.000767 0.000120\n4.982500 0.000506 0.000147\n4.987500 0.000672 0.000075\n4.992500 0.000768 0.000023\n4.997500 0.000921 0.000024\n5.002500 0.000721 0.000169\n5.007500 0.000856 0.000069\n5.012500 0.000884 0.000102\n5.017500 0.000841 0.000055\n5.022500 0.000894 0.000046\n5.027500 0.000712 0.000062\n5.032500 0.000572 0.000076\n5.037500 0.000654 0.000015\n5.042500 0.000817 0.000128\n5.047500 0.000678 0.000100\n5.052500 0.000645 0.000038\n5.057500 0.000599 0.000096\n5.062500 0.000429 0.000109\n5.067500 0.000660 0.000076\n5.072500 0.000624 0.000049\n5.077500 0.000488 0.000093\n5.082500 0.000646 0.000151\n5.087500 0.000718 0.000346\n5.092500 0.000406 0.000174\n5.097500 0.000205 0.000080\n", + "mpcId": "123", + "alertId": "321", + "mjd": "63646", + "telescope": "test telescope" + }, + "miscInfo": { + "misc": [ + { + "miscKey": "Test Key", + "miscValue": "value" + } + ] + } +} diff --git a/rafts/frontend/src/types.d.ts b/rafts/frontend/src/types.d.ts new file mode 100644 index 0000000..ed0a89b --- /dev/null +++ b/rafts/frontend/src/types.d.ts @@ -0,0 +1,104 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { DefaultSession } from 'next-auth' + +declare module 'next-auth' { + interface Session { + accessToken?: string + role?: string + affiliation?: string + user?: { + id?: string + role?: string + groups?: string[] + affiliation?: string + } & DefaultSession['user'] + } + + interface User { + id?: string + name?: string | null + firstName?: string + lastName?: string + email?: string | null + accessToken?: string + role?: string + groups?: string[] + affiliation?: string + } +} + +declare module 'next-auth/jwt' { + interface JWT { + accessToken?: string + userId?: string + role?: string + groups?: string[] + affiliation?: string + } +} diff --git a/rafts/frontend/src/types/attachments.ts b/rafts/frontend/src/types/attachments.ts new file mode 100644 index 0000000..02c2d8c --- /dev/null +++ b/rafts/frontend/src/types/attachments.ts @@ -0,0 +1,448 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +/** + * Attachment types and utilities for VOSpace file storage + * + * This module provides type definitions and utility functions for managing + * file attachments in the RAFT form system. Attachments are stored in VOSpace + * and referenced in the RAFT.json via FileReference objects. + */ + +/** + * Represents a file stored in VOSpace + * Used instead of inline base64/text content + */ +export interface FileReference { + /** Discriminator for type detection */ + type: 'file-reference' + /** Original filename (e.g., "figure.png") */ + filename: string + /** MIME type (e.g., "image/png", "text/plain") */ + mimeType: string + /** File size in bytes */ + size: number + /** ISO timestamp when uploaded */ + uploadedAt: string +} + +/** + * Attachment field value - can be legacy content or new FileReference + */ +export type AttachmentValue = string | FileReference | undefined + +/** + * Supported attachment types in the form + */ +export type AttachmentType = 'figure' | 'ephemeris' | 'orbital' | 'spectrum' | 'astrometry' + +/** + * Configuration for each attachment type + */ +export interface AttachmentConfig { + /** Field name in the form data */ + fieldName: string + /** Default filename for this attachment type */ + defaultFilename: string + /** Allowed MIME types */ + allowedMimeTypes: string[] + /** Maximum file size in bytes */ + maxSize: number + /** Whether content is binary (true) or text (false) */ + isBinary: boolean +} + +/** + * Attachment configurations by type + */ +export const ATTACHMENT_CONFIGS: Record = { + figure: { + fieldName: 'figure', + defaultFilename: 'figure.png', + allowedMimeTypes: ['image/png', 'image/jpeg', 'image/jpg'], + maxSize: 5 * 1024 * 1024, // 5MB + isBinary: true, + }, + ephemeris: { + fieldName: 'ephemeris', + defaultFilename: 'ephemeris.txt', + allowedMimeTypes: ['text/plain'], + maxSize: 5 * 1024 * 1024, + isBinary: false, + }, + orbital: { + fieldName: 'orbitalElements', + defaultFilename: 'orbital.txt', + allowedMimeTypes: ['text/plain'], + maxSize: 5 * 1024 * 1024, + isBinary: false, + }, + spectrum: { + fieldName: 'spectroscopy', + defaultFilename: 'spectrum.txt', + allowedMimeTypes: ['text/plain'], + maxSize: 5 * 1024 * 1024, + isBinary: false, + }, + astrometry: { + fieldName: 'astrometry', + defaultFilename: 'astrometry.xml', + allowedMimeTypes: ['application/xml', 'text/xml', 'text/plain'], + maxSize: 5 * 1024 * 1024, + isBinary: false, + }, +} + +// ============================================================================ +// Type Guards and Detection Utilities +// ============================================================================ + +/** + * Check if a value is a FileReference object + */ +export function isFileReference(value: unknown): value is FileReference { + return ( + typeof value === 'object' && + value !== null && + 'type' in value && + (value as FileReference).type === 'file-reference' && + 'filename' in value && + 'mimeType' in value && + 'size' in value + ) +} + +/** + * Parse a stored attachment value (could be FileReference JSON string, base64, or text) + * Returns the appropriate AttachmentValue type + */ +export function parseStoredAttachment(value: unknown): AttachmentValue { + if (!value) return undefined + if (isFileReference(value)) return value + + // Try to parse as JSON string that might be a serialized FileReference + if (typeof value === 'string') { + // Check if it looks like a JSON object (starts with {) + if (value.startsWith('{')) { + try { + const parsed = JSON.parse(value) + if (isFileReference(parsed)) { + return parsed + } + } catch { + // Not valid JSON, treat as regular string + } + } + // Return as-is (base64 or text content) + return value + } + + return undefined +} + +/** + * Check if a string is a base64 data URL (for images) + */ +export function isBase64DataUrl(value: unknown): boolean { + return typeof value === 'string' && value.startsWith('data:') +} + +/** + * Check if a string is a base64 image data URL + */ +export function isBase64Image(value: unknown): boolean { + return typeof value === 'string' && value.startsWith('data:image/') +} + +/** + * Check if a value is inline text content (not base64, not FileReference) + */ +export function isInlineTextContent(value: unknown): boolean { + return typeof value === 'string' && value.length > 0 && !isBase64DataUrl(value) +} + +/** + * Check if a value has any attachment content + */ +export function hasAttachmentContent(value: unknown): boolean { + if (!value) return false + if (isFileReference(value)) return true + if (typeof value === 'string' && value.length > 0) return true + return false +} + +// ============================================================================ +// FileReference Utilities +// ============================================================================ + +/** + * Create a FileReference object + */ +export function createFileReference( + filename: string, + mimeType: string, + size: number, +): FileReference { + return { + type: 'file-reference', + filename, + mimeType, + size, + uploadedAt: new Date().toISOString(), + } +} + +/** + * Generate a unique filename with timestamp to avoid collisions + */ +export function generateUniqueFilename(originalFilename: string): string { + const timestamp = Date.now() + const ext = getFileExtension(originalFilename) + const baseName = getFileBaseName(originalFilename) + return `${baseName}-${timestamp}${ext}` +} + +/** + * Get file extension including the dot (e.g., ".png") + */ +export function getFileExtension(filename: string): string { + const lastDot = filename.lastIndexOf('.') + return lastDot >= 0 ? filename.slice(lastDot) : '' +} + +/** + * Get filename without extension + */ +export function getFileBaseName(filename: string): string { + const lastDot = filename.lastIndexOf('.') + return lastDot >= 0 ? filename.slice(0, lastDot) : filename +} + +/** + * Sanitize filename for safe storage + * Removes special characters, replaces spaces with underscores + */ +export function sanitizeFilename(filename: string): string { + return filename + .replace(/[^a-zA-Z0-9._-]/g, '_') // Replace special chars with underscore + .replace(/_+/g, '_') // Collapse multiple underscores + .replace(/^_|_$/g, '') // Remove leading/trailing underscores +} + +// ============================================================================ +// MIME Type Utilities +// ============================================================================ + +/** + * Get MIME type from file extension + */ +export function getMimeTypeFromExtension(filename: string): string { + const ext = getFileExtension(filename).toLowerCase() + const mimeTypes: Record = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.txt': 'text/plain', + '.xml': 'application/xml', + '.json': 'application/json', + '.psv': 'text/plain', + '.mpc': 'text/plain', + } + return mimeTypes[ext] || 'application/octet-stream' +} + +/** + * Get file extension from MIME type + */ +export function getExtensionFromMimeType(mimeType: string): string { + const extensions: Record = { + 'image/png': '.png', + 'image/jpeg': '.jpg', + 'image/jpg': '.jpg', + 'image/gif': '.gif', + 'text/plain': '.txt', + 'application/xml': '.xml', + 'text/xml': '.xml', + 'application/json': '.json', + } + return extensions[mimeType] || '' +} + +/** + * Check if MIME type is an image + */ +export function isImageMimeType(mimeType: string): boolean { + return mimeType.startsWith('image/') +} + +/** + * Check if MIME type is text-based + */ +export function isTextMimeType(mimeType: string): boolean { + return ( + mimeType.startsWith('text/') || + mimeType === 'application/xml' || + mimeType === 'application/json' + ) +} + +// ============================================================================ +// Validation Utilities +// ============================================================================ + +/** + * Validate file against attachment configuration + */ +export function validateAttachment( + file: File, + config: AttachmentConfig, +): { valid: boolean; error?: string } { + // Check file size + if (file.size > config.maxSize) { + const maxSizeMB = config.maxSize / (1024 * 1024) + return { + valid: false, + error: `File size exceeds maximum of ${maxSizeMB}MB`, + } + } + + // Check MIME type + const isAllowedType = config.allowedMimeTypes.some( + (allowed) => file.type === allowed || file.type.startsWith(allowed.replace('/*', '/')), + ) + + // If MIME type check fails, try extension-based validation for text files + // Browsers often report empty or generic MIME types for .psv, .mpc, etc. + if (!isAllowedType) { + const ext = getFileExtension(file.name).toLowerCase() + const textExtensions = ['.txt', '.psv', '.mpc', '.xml'] + const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif'] + + // For text-based configs, allow known text file extensions + if (!config.isBinary && textExtensions.includes(ext)) { + return { valid: true } + } + + // For binary (image) configs, allow known image extensions + if (config.isBinary && imageExtensions.includes(ext)) { + return { valid: true } + } + + return { + valid: false, + error: `File type ${file.type || 'unknown'} is not allowed. Allowed types: ${config.allowedMimeTypes.join(', ')}`, + } + } + + return { valid: true } +} + +/** + * Get attachment config by field name + */ +export function getConfigByFieldName(fieldName: string): AttachmentConfig | undefined { + return Object.values(ATTACHMENT_CONFIGS).find((config) => config.fieldName === fieldName) +} + +// ============================================================================ +// Base64 Conversion Utilities +// ============================================================================ + +/** + * Convert a Blob to base64 data URL + */ +export async function blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(new Error('Failed to convert blob to base64')) + reader.readAsDataURL(blob) + }) +} + +/** + * Convert base64 data URL to Blob + */ +export function base64ToBlob(base64: string): Blob { + const [header, data] = base64.split(',') + const mimeType = header.match(/data:([^;]+)/)?.[1] || 'application/octet-stream' + const binary = atob(data) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i) + } + return new Blob([bytes], { type: mimeType }) +} + +/** + * Extract MIME type from base64 data URL + */ +export function getMimeTypeFromBase64(base64: string): string { + const match = base64.match(/data:([^;]+)/) + return match ? match[1] : 'application/octet-stream' +} diff --git a/rafts/frontend/src/types/auth.ts b/rafts/frontend/src/types/auth.ts new file mode 100644 index 0000000..ae47d3e --- /dev/null +++ b/rafts/frontend/src/types/auth.ts @@ -0,0 +1,66 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ diff --git a/rafts/frontend/src/types/common.ts b/rafts/frontend/src/types/common.ts new file mode 100644 index 0000000..66a6569 --- /dev/null +++ b/rafts/frontend/src/types/common.ts @@ -0,0 +1,81 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { ReactNode } from 'react' + +export type Lang = 'en' | 'fr' + +export type Params = Promise<{ locale: string }> + +export interface RootLayoutProps { + children: ReactNode +} + +export interface LayoutProps { + children: ReactNode + params: Params +} diff --git a/rafts/frontend/src/types/doi.ts b/rafts/frontend/src/types/doi.ts new file mode 100644 index 0000000..2c9f7bc --- /dev/null +++ b/rafts/frontend/src/types/doi.ts @@ -0,0 +1,110 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { TRaftSubmission } from '@/shared/model' + +export interface DOIData { + identifier: string + identifierType: string + title: string + titleLang: string | null + status: string + dataDirectory: string + journalRef: string | null + /** Assigned reviewer username (only visible to publishers/reviewers) */ + reviewer: string | null +} + +export interface RaftStatusChange { + fromStatus: string + toStatus: string + changedBy: string + changedAt: string + reason?: string +} + +export interface RaftData extends TRaftSubmission { + _id: string + id?: string + relatedRafts: string[] + generateForumPost: boolean + createdBy: string + createdAt: string + updatedAt: string + updatedBy?: string + doi?: string + /** Data directory path for storage links (e.g., /rafts-test/RAFTS-xxx/data) */ + dataDirectory?: string + /** Assigned reviewer username (from DOI backend) */ + reviewer?: string | null + /** ISO timestamp of first submission for review */ + submittedAt?: string + /** Version number — increments on resubmit after revert */ + version?: number + /** History of status transitions */ + statusHistory?: RaftStatusChange[] +} diff --git a/rafts/frontend/src/types/reviews.ts b/rafts/frontend/src/types/reviews.ts new file mode 100644 index 0000000..2e8d1f2 --- /dev/null +++ b/rafts/frontend/src/types/reviews.ts @@ -0,0 +1,116 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { RaftData } from '@/types/doi' + +export interface ReviewUser { + _id: string + firstName: string + lastName: string +} + +export interface ReviewComment { + _id: string + content: string + createdBy: ReviewUser + createdAt: string + isResolved: boolean + location?: string + resolvedBy?: string + resolvedAt?: string +} + +export interface StatusChange { + fromStatus: string + toStatus: string + changedBy: ReviewUser + changedAt: string + reason?: string + _id: string +} + +export interface RaftVersion { + versionNumber: number + raftData: RaftData + createdAt: string + createdBy: ReviewUser + commitMessage?: string + _id: string +} + +export interface RaftReview { + _id: string + raftId: string + currentVersion: number + versions: RaftVersion[] + statusHistory: StatusChange[] + comments: ReviewComment[] + assignedReviewers: ReviewUser[] + isActive: boolean + createdAt: string + updatedAt: string +} diff --git a/rafts/frontend/src/utilities/__tests__/debounce.test.ts b/rafts/frontend/src/utilities/__tests__/debounce.test.ts new file mode 100644 index 0000000..84eb000 --- /dev/null +++ b/rafts/frontend/src/utilities/__tests__/debounce.test.ts @@ -0,0 +1,154 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { debounce } from '../debounce' + +describe('debounce', () => { + beforeEach(() => { + vi.useFakeTimers() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('delays function execution', () => { + const fn = vi.fn() + const debounced = debounce(fn, 100) + + debounced() + expect(fn).not.toHaveBeenCalled() + + vi.advanceTimersByTime(100) + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('resets timer on subsequent calls', () => { + const fn = vi.fn() + const debounced = debounce(fn, 100) + + debounced() + vi.advanceTimersByTime(50) + debounced() + vi.advanceTimersByTime(50) + + expect(fn).not.toHaveBeenCalled() + + vi.advanceTimersByTime(50) + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('only calls function once for rapid successive calls', () => { + const fn = vi.fn() + const debounced = debounce(fn, 100) + + debounced() + debounced() + debounced() + debounced() + + vi.advanceTimersByTime(100) + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('passes arguments to the debounced function', () => { + const fn = vi.fn() + const debounced = debounce(fn, 100) + + debounced('arg1', 'arg2') + vi.advanceTimersByTime(100) + + expect(fn).toHaveBeenCalledWith('arg1', 'arg2') + }) + + it('uses the latest arguments when called multiple times', () => { + const fn = vi.fn() + const debounced = debounce(fn, 100) + + debounced('first') + debounced('second') + debounced('third') + + vi.advanceTimersByTime(100) + expect(fn).toHaveBeenCalledWith('third') + expect(fn).toHaveBeenCalledTimes(1) + }) + + it('allows multiple separate debounced calls after wait time', () => { + const fn = vi.fn() + const debounced = debounce(fn, 100) + + debounced() + vi.advanceTimersByTime(100) + expect(fn).toHaveBeenCalledTimes(1) + + debounced() + vi.advanceTimersByTime(100) + expect(fn).toHaveBeenCalledTimes(2) + }) +}) diff --git a/rafts/frontend/src/utilities/__tests__/doiIdentifier.test.ts b/rafts/frontend/src/utilities/__tests__/doiIdentifier.test.ts new file mode 100644 index 0000000..a396b2b --- /dev/null +++ b/rafts/frontend/src/utilities/__tests__/doiIdentifier.test.ts @@ -0,0 +1,160 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { describe, it, expect } from 'vitest' +import { extractDOI, sortByIdentifierNumber, getCitationLink } from '../doiIdentifier' + +describe('extractDOI', () => { + it('extracts DOI from valid XML with identifierType attribute', () => { + const xml = '10.5281/zenodo.1234567' + expect(extractDOI(xml)).toBe('10.5281/zenodo.1234567') + }) + + it('extracts DOI from XML with whitespace', () => { + const xml = ' 25.0047 ' + expect(extractDOI(xml)).toBe('25.0047') + }) + + it('extracts DOI using fallback regex when identifierType is missing', () => { + const xml = '10.1234/test' + expect(extractDOI(xml)).toBe('10.1234/test') + }) + + it('returns null for invalid XML', () => { + const xml = 'test' + expect(extractDOI(xml)).toBeNull() + }) + + it('returns null for empty string', () => { + expect(extractDOI('')).toBeNull() + }) + + it('handles complex XML document', () => { + const xml = ` + + + 10.5281/example + Test Document + + ` + expect(extractDOI(xml)).toBe('10.5281/example') + }) +}) + +describe('sortByIdentifierNumber', () => { + it('sorts DOI records by identifier number in descending order', () => { + const records = [ + { identifier: '10.5281/1', title: 'First', status: 'draft' }, + { identifier: '10.5281/3', title: 'Third', status: 'draft' }, + { identifier: '10.5281/2', title: 'Second', status: 'draft' }, + ] + + const sorted = sortByIdentifierNumber(records as never) + expect(sorted[0].identifier).toBe('10.5281/3') + expect(sorted[1].identifier).toBe('10.5281/2') + expect(sorted[2].identifier).toBe('10.5281/1') + }) + + it('does not modify the original array', () => { + const records = [ + { identifier: '10.5281/2', title: 'Second', status: 'draft' }, + { identifier: '10.5281/1', title: 'First', status: 'draft' }, + ] + + sortByIdentifierNumber(records as never) + expect(records[0].identifier).toBe('10.5281/2') + }) + + it('handles decimal identifiers', () => { + const records = [ + { identifier: '10.5281/1.5', title: 'A', status: 'draft' }, + { identifier: '10.5281/2.5', title: 'B', status: 'draft' }, + { identifier: '10.5281/1.1', title: 'C', status: 'draft' }, + ] + + const sorted = sortByIdentifierNumber(records as never) + expect(sorted[0].identifier).toBe('10.5281/2.5') + expect(sorted[1].identifier).toBe('10.5281/1.5') + expect(sorted[2].identifier).toBe('10.5281/1.1') + }) + + it('returns empty array for empty input', () => { + expect(sortByIdentifierNumber([])).toEqual([]) + }) +}) + +describe('getCitationLink', () => { + it('generates correct citation link', () => { + const result = getCitationLink('10.5281/zenodo.1234567') + expect(result).toBe('https://www.canfar.net/citation/landing?doi=10.5281/zenodo.1234567') + }) + + it('handles simple DOI identifier', () => { + const result = getCitationLink('25.0047') + expect(result).toBe('https://www.canfar.net/citation/landing?doi=25.0047') + }) +}) diff --git a/rafts/frontend/src/utilities/__tests__/formatter.test.ts b/rafts/frontend/src/utilities/__tests__/formatter.test.ts new file mode 100644 index 0000000..ca5f309 --- /dev/null +++ b/rafts/frontend/src/utilities/__tests__/formatter.test.ts @@ -0,0 +1,149 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { describe, it, expect } from 'vitest' +import { formatDate, formatUserName, getUserInitials } from '../formatter' + +describe('formatDate', () => { + it('formats a valid date string correctly', () => { + const result = formatDate('2025-07-15T10:30:00Z') + // The exact output depends on timezone, but it should contain expected parts + expect(result).toMatch(/Jul/) + expect(result).toMatch(/15/) + expect(result).toMatch(/2025/) + }) + + it('handles ISO date strings', () => { + // Use a mid-day time to avoid timezone edge cases + const result = formatDate('2024-06-15T12:00:00.000Z') + expect(result).toMatch(/Jun/) + expect(result).toMatch(/15/) + expect(result).toMatch(/2024/) + }) +}) + +describe('formatUserName', () => { + it('returns full name when user has firstName and lastName', () => { + const user = { + firstName: 'John', + lastName: 'Doe', + id: '1', + email: 'john@example.com', + } + expect(formatUserName(user)).toBe('John Doe') + }) + + it('returns "Unknown User" when user is undefined', () => { + expect(formatUserName(undefined)).toBe('Unknown User') + }) + + it('handles empty strings in name', () => { + const user = { + firstName: '', + lastName: 'Doe', + id: '1', + email: 'test@example.com', + } + expect(formatUserName(user)).toBe(' Doe') + }) +}) + +describe('getUserInitials', () => { + it('returns uppercase initials for valid user', () => { + const user = { + firstName: 'John', + lastName: 'Doe', + id: '1', + email: 'john@example.com', + } + expect(getUserInitials(user)).toBe('JD') + }) + + it('returns "U" when user is undefined', () => { + expect(getUserInitials(undefined)).toBe('U') + }) + + it('handles lowercase names', () => { + const user = { + firstName: 'jane', + lastName: 'smith', + id: '1', + email: 'jane@example.com', + } + expect(getUserInitials(user)).toBe('JS') + }) + + it('handles single character names', () => { + const user = { + firstName: 'A', + lastName: 'B', + id: '1', + email: 'ab@example.com', + } + expect(getUserInitials(user)).toBe('AB') + }) +}) diff --git a/rafts/frontend/src/utilities/constants.ts b/rafts/frontend/src/utilities/constants.ts new file mode 100644 index 0000000..26b1ce9 --- /dev/null +++ b/rafts/frontend/src/utilities/constants.ts @@ -0,0 +1,70 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export const CITATION_PARTIAL_URL = 'https://www.canfar.net/citation/landing?doi=' +export const STORAGE_PARTIAL_URL = 'https://www.canfar.net/storage/list' +export const COOKIE_SSO_KEY = process.env.NEXT_COOKIE_SSO_KEY || 'CADC_SSO' diff --git a/rafts/frontend/src/utilities/dataCiteSample.json b/rafts/frontend/src/utilities/dataCiteSample.json new file mode 100644 index 0000000..6456de2 --- /dev/null +++ b/rafts/frontend/src/utilities/dataCiteSample.json @@ -0,0 +1,90 @@ +{ + "resource": { + "@xmlns": "http://datacite.org/schema/kernel-4", + "identifier": { + "@identifierType": "DOI", + "$": "110.5072/example-full" + }, + "creators": { + "$": [ + { + "creator": { + "creatorName": { + "@nameType": "Personal", + "$": "Miller, Elizabeth" + }, + "givenName": { "$": "Elizabeth" }, + "familyName": { "$": "Miller" }, + "nameIdentifier": { + "@nameIdentifierScheme": "ORCID", + "@schemeURI": "http://orcid.org/", + "$": "0000-0001-5000-0007" + }, + "affiliation": { "$": "DataCite" } + } + } + ] + }, + "titles": { + "$": [ + { + "title": { "$": "Full DataCite XML Example" } + } + ] + }, + "publisher": { "$": "CADC" }, + "publicationYear": { "$": 2025 }, + "resourceType": { + "@resourceTypeGeneral": "Dataset", + "$": "Dataset" + }, + "contributors": { + "$": [ + { + "contributor": { + "@contributorType": "ProjectLeader", + "contributorName": { "$": "Starr, Joan" }, + "givenName": { "$": "Joan" }, + "familyName": { "$": "Starr" }, + "nameIdentifier": { + "@nameIdentifierScheme": "ORCID", + "@schemeURI": "http://orcid.org/", + "$": "0000-0002-7285-027X" + }, + "affiliation": { "$": "California Digital Library" } + } + } + ] + }, + "dates": { + "$": [ + { + "date": { + "@dateType": "Created", + "@dateInformation": "The date the DOI was created", + "$": "2025-02-26" + } + } + ] + }, + "descriptions": { + "$": [ + { + "description": { + "@descriptionType": "Abstract", + "@xml:lang": "en-US", + "$": "XML example of all DataCite Metadata Schema v4.1 properties." + } + }, + { + "description": { + "@descriptionType": "TechnicalInfo", + "@xml:lang": "en-US", + "$": "keywords, keywords." + } + } + ] + }, + "language": { "$": "en-US" } + } +} diff --git a/rafts/frontend/src/utilities/dataCiteToRaft.ts b/rafts/frontend/src/utilities/dataCiteToRaft.ts new file mode 100644 index 0000000..643727d --- /dev/null +++ b/rafts/frontend/src/utilities/dataCiteToRaft.ts @@ -0,0 +1,355 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { TRaftContext } from '@/context/types' +import { + OPTION_DRAFT, + OPTION_REVIEW, + OPTION_UNDER_REVIEW, + OPTION_APPROVED, + OPTION_PUBLISHED, +} from '@/shared/constants' + +// Status type from constants +type RaftStatus = + | typeof OPTION_DRAFT + | typeof OPTION_REVIEW + | typeof OPTION_UNDER_REVIEW + | typeof OPTION_APPROVED + | typeof OPTION_PUBLISHED + +/** + * DataCite Resource structure (simplified) + * Based on DataCite Metadata Schema + */ +interface DataCiteCreator { + name?: string + givenName?: string + familyName?: string + affiliation?: string | { name?: string }[] + nameIdentifiers?: { nameIdentifier?: string; nameIdentifierScheme?: string }[] +} + +interface DataCiteTitle { + value?: string + lang?: string +} + +interface DataCiteResource { + identifier?: { value?: string; identifierType?: string } + creators?: DataCiteCreator[] + titles?: DataCiteTitle[] + publisher?: { value?: string } + publicationYear?: { value?: string } + resourceType?: { resourceTypeGeneral?: string; value?: string } + language?: string + dates?: { date?: string; dateType?: string }[] +} + +/** + * Convert DataCite JSON resource to RAFT form context + * This is used as a fallback when RAFT.json doesn't exist + */ +export function dataCiteToRaft( + dataCite: DataCiteResource, + doiStatus?: { title?: string; status?: string }, +): TRaftContext { + // Extract title + const title = dataCite.titles?.[0]?.value || doiStatus?.title || '' + + // Extract corresponding author from first creator + const firstCreator = dataCite.creators?.[0] + const correspondingAuthor = firstCreator + ? { + firstName: firstCreator.givenName || firstCreator.name?.split(' ')[0] || '', + lastName: firstCreator.familyName || firstCreator.name?.split(' ').slice(1).join(' ') || '', + affiliation: getAffiliation(firstCreator.affiliation), + authorORCID: getOrcid(firstCreator.nameIdentifiers), + email: '', // Not available in DataCite + } + : { + firstName: '', + lastName: '', + affiliation: '', + authorORCID: '', + email: '', + } + + // Extract contributing authors (rest of creators) + const contributingAuthors = + dataCite.creators?.slice(1).map((creator) => ({ + firstName: creator.givenName || creator.name?.split(' ')[0] || '', + lastName: creator.familyName || creator.name?.split(' ').slice(1).join(' ') || '', + affiliation: getAffiliation(creator.affiliation), + authorORCID: getOrcid(creator.nameIdentifiers), + email: '', // Not available in DataCite + })) || [] + + // Map status + const status = mapStatus(doiStatus?.status) + + const raftContext: TRaftContext = { + generalInfo: { + title, + postOptOut: false, + status, + }, + authorInfo: { + correspondingAuthor, + contributingAuthors: contributingAuthors.length > 0 ? contributingAuthors : undefined, + collaborations: undefined, + }, + observationInfo: { + topic: ['other'], // Default, not available in DataCite + objectName: '', // Not available in DataCite + abstract: '', // Not available in DataCite + figure: undefined, + acknowledgements: undefined, + relatedPublishedRafts: undefined, + }, + technical: { + photometry: undefined, + spectroscopy: undefined, + astrometry: undefined, + ephemeris: undefined, + orbitalElements: undefined, + mpcId: undefined, + alertId: undefined, + mjd: '', + telescope: undefined, + }, + measurementInfo: { + photometry: undefined, + spectroscopy: undefined, + astrometry: undefined, + }, + miscInfo: { + misc: undefined, + }, + } + + return raftContext +} + +function getAffiliation(affiliation: string | { name?: string }[] | undefined): string { + if (!affiliation) return '' + if (typeof affiliation === 'string') return affiliation + if (Array.isArray(affiliation) && affiliation.length > 0) { + return affiliation[0].name || '' + } + return '' +} + +function getOrcid( + nameIdentifiers: { nameIdentifier?: string; nameIdentifierScheme?: string }[] | undefined, +): string { + if (!nameIdentifiers) return '' + const orcid = nameIdentifiers.find((ni) => ni.nameIdentifierScheme?.toLowerCase() === 'orcid') + return orcid?.nameIdentifier || '' +} + +function mapStatus(status: string | undefined): RaftStatus { + if (!status) return OPTION_DRAFT + const lowerStatus = status.toLowerCase() + if (lowerStatus.includes('progress') || lowerStatus.includes('draft')) return OPTION_DRAFT + if (lowerStatus.includes('review') && lowerStatus.includes('under')) return OPTION_UNDER_REVIEW + if (lowerStatus.includes('review')) return OPTION_REVIEW + if (lowerStatus.includes('approved')) return OPTION_APPROVED + if ( + lowerStatus.includes('minted') || + lowerStatus.includes('published') || + lowerStatus.includes('completed') + ) + return OPTION_PUBLISHED + return OPTION_DRAFT +} + +/** + * Parse DataCite XML to JSON + */ +export async function parseDataCiteXml(xmlString: string): Promise { + const { parseStringPromise } = await import('xml2js') + + try { + const result = await parseStringPromise(xmlString, { + explicitArray: false, + attrkey: 'attr', + charkey: '_', + }) + + // DataCite XML has 'resource' as root + const resource = result.resource || result + + return { + identifier: parseIdentifier(resource.identifier), + creators: parseCreators(resource.creators), + titles: parseTitles(resource.titles), + publisher: parsePublisher(resource.publisher), + publicationYear: parsePublicationYear(resource.publicationYear), + resourceType: parseResourceType(resource.resourceType), + language: resource.language, + } + } catch (error) { + console.error('[parseDataCiteXml] Error parsing XML:', error) + throw new Error('Failed to parse DataCite XML') + } +} + +function parseIdentifier( + identifier: unknown, +): { value?: string; identifierType?: string } | undefined { + if (!identifier) return undefined + if (typeof identifier === 'string') return { value: identifier } + if (typeof identifier === 'object') { + const obj = identifier as { _?: string; attr?: { identifierType?: string } } + return { value: obj._ || '', identifierType: obj.attr?.identifierType } + } + return undefined +} + +function parseCreators(creators: unknown): DataCiteCreator[] { + if (!creators) return [] + const creatorList = (creators as { creator?: unknown }).creator + if (!creatorList) return [] + const arr = Array.isArray(creatorList) ? creatorList : [creatorList] + + return arr.map((c: unknown) => { + const creator = c as { + creatorName?: string | { _?: string } + givenName?: string + familyName?: string + affiliation?: string | { _?: string }[] + nameIdentifier?: { _?: string; attr?: { nameIdentifierScheme?: string } }[] + } + + return { + name: typeof creator.creatorName === 'string' ? creator.creatorName : creator.creatorName?._, + givenName: creator.givenName, + familyName: creator.familyName, + affiliation: parseAffiliation(creator.affiliation), + nameIdentifiers: parseNameIdentifiers(creator.nameIdentifier), + } + }) +} + +function parseAffiliation(affiliation: unknown): string | { name?: string }[] { + if (!affiliation) return '' + if (typeof affiliation === 'string') return affiliation + if (Array.isArray(affiliation)) { + return affiliation.map((a) => ({ + name: typeof a === 'string' ? a : a._, + })) + } + return '' +} + +function parseNameIdentifiers( + nameIdentifiers: unknown, +): { nameIdentifier?: string; nameIdentifierScheme?: string }[] { + if (!nameIdentifiers) return [] + const arr = Array.isArray(nameIdentifiers) ? nameIdentifiers : [nameIdentifiers] + return arr.map((ni: unknown) => { + const id = ni as { _?: string; attr?: { nameIdentifierScheme?: string } } + return { + nameIdentifier: id._, + nameIdentifierScheme: id.attr?.nameIdentifierScheme, + } + }) +} + +function parseTitles(titles: unknown): DataCiteTitle[] { + if (!titles) return [] + const titleList = (titles as { title?: unknown }).title + if (!titleList) return [] + const arr = Array.isArray(titleList) ? titleList : [titleList] + + return arr.map((t: unknown) => { + if (typeof t === 'string') return { value: t } + const title = t as { _?: string; attr?: { 'xml:lang'?: string } } + return { value: title._, lang: title.attr?.['xml:lang'] } + }) +} + +function parsePublisher(publisher: unknown): { value?: string } | undefined { + if (!publisher) return undefined + if (typeof publisher === 'string') return { value: publisher } + return { value: (publisher as { _?: string })._ } +} + +function parsePublicationYear(year: unknown): { value?: string } | undefined { + if (!year) return undefined + if (typeof year === 'string') return { value: year } + return { value: (year as { _?: string })._ } +} + +function parseResourceType( + resourceType: unknown, +): { resourceTypeGeneral?: string; value?: string } | undefined { + if (!resourceType) return undefined + const rt = resourceType as { _?: string; attr?: { resourceTypeGeneral?: string } } + return { value: rt._, resourceTypeGeneral: rt.attr?.resourceTypeGeneral } +} diff --git a/rafts/frontend/src/utilities/debounce.ts b/rafts/frontend/src/utilities/debounce.ts new file mode 100644 index 0000000..10c354f --- /dev/null +++ b/rafts/frontend/src/utilities/debounce.ts @@ -0,0 +1,83 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +export function debounce) => ReturnType>( + func: T, + wait: number, +): (...args: Parameters) => void { + let timeout: NodeJS.Timeout | null = null + + return function debounced(...args: Parameters) { + if (timeout) { + clearTimeout(timeout) + } + + timeout = setTimeout(() => { + func(...args) + }, wait) + } +} diff --git a/rafts/frontend/src/utilities/doiIdentifier.ts b/rafts/frontend/src/utilities/doiIdentifier.ts new file mode 100644 index 0000000..dfd989e --- /dev/null +++ b/rafts/frontend/src/utilities/doiIdentifier.ts @@ -0,0 +1,108 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { CITATION_PARTIAL_URL } from '@/utilities/constants' +import { DOIData } from '@/types/doi' + +export const extractDOI = (xmlString: string): string | null => { + try { + // Look for the identifier tag with identifierType="DOI" + const identifierRegex = /([^<]+)<\/identifier>/ + const match = xmlString.match(identifierRegex) + + // Return the captured value if found + if (match && match[1]) { + return match[1].trim() + } + + // Try a more forgiving approach if the above doesn't work + const fallbackRegex = /]*>([^<]+)<\/identifier>/ + const fallbackMatch = xmlString.match(fallbackRegex) + + if (fallbackMatch && fallbackMatch[1]) { + return fallbackMatch[1].trim() + } + + return null + } catch (error) { + console.error('Error extracting DOI:', error) + return null + } +} + +export const sortByIdentifierNumber = (records: DOIData[]) => { + return [...records].sort((a, b) => { + // Extract the part after the slash for both identifiers + const numA = a.identifier.split('/')[1] + const numB = b.identifier.split('/')[1] + + // Compare as numbers rather than strings for proper numerical sorting + return parseFloat(numB) - parseFloat(numA) + }) +} + +export const getCitationLink = (doiIdentifier: string) => `${CITATION_PARTIAL_URL}${doiIdentifier}` diff --git a/rafts/frontend/src/utilities/formatter.ts b/rafts/frontend/src/utilities/formatter.ts new file mode 100644 index 0000000..739cbd1 --- /dev/null +++ b/rafts/frontend/src/utilities/formatter.ts @@ -0,0 +1,92 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { User } from 'next-auth' + +// Format date for display +export const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) +} + +// Format user name +export const formatUserName = (user?: User) => { + if (!user) return 'Unknown User' + return `${user.firstName} ${user.lastName}` +} + +// Get user initials +export const getUserInitials = (user?: User) => { + if (!user) return 'U' + return `${user?.firstName?.[0]}${user?.lastName?.[0]}`.toUpperCase() +} diff --git a/rafts/frontend/src/utilities/jsonToDataCite.ts b/rafts/frontend/src/utilities/jsonToDataCite.ts new file mode 100644 index 0000000..0f979e6 --- /dev/null +++ b/rafts/frontend/src/utilities/jsonToDataCite.ts @@ -0,0 +1,167 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { RaftData } from '@/types/doi' + +// JSON format for OpenCADC JsonInputter (custom JSON-to-JDOM converter): +// - Text content: { "$": "value" } +// - Attributes: { "@attr": "value" } +// - Element with attrs + text: { "@attr": "value", "$": "text" } +// - Array of child elements: { "$": [ { "childName": { ...children } }, ... ] } +// - Regular keys create child elements (value MUST be JSONObject, not string) +interface DataCiteCreator { + creator: { + creatorName: { '@nameType': string; $: string } + givenName: { $: string } + familyName: { $: string } + affiliation?: { $: string } + } +} + +interface DataCiteContributor { + contributor: { + '@contributorType': string + contributorName: { $: string } + givenName: { $: string } + familyName: { $: string } + affiliation?: { $: string } + } +} + +interface DataCiteJSON { + resource: { + '@xmlns': string + identifier: { '@identifierType': string; $: string } + creators: { $: DataCiteCreator[] } + titles: { $: Array<{ title: { $: string } }> } + publisher: { $: string } + publicationYear: { $: string } + resourceType: { '@resourceTypeGeneral': string; $: string } + contributors?: { $: DataCiteContributor[] } + } +} + +const convertToDataCite = (input: Partial): DataCiteJSON => { + const publicationYear = new Date().getFullYear() + const identifier = '10.5072/example-full' // Example DOI + + const dataCite: DataCiteJSON = { + resource: { + '@xmlns': 'http://datacite.org/schema/kernel-4', + identifier: { + '@identifierType': 'DOI', + $: identifier, + }, + creators: { + $: [ + { + creator: { + creatorName: { + '@nameType': 'Personal', + $: `${input.authorInfo?.correspondingAuthor?.lastName || 'Unknown'}, ${input.authorInfo?.correspondingAuthor?.firstName || 'Unknown'}`, + }, + givenName: { $: input.authorInfo?.correspondingAuthor?.firstName || 'Unknown' }, + familyName: { $: input.authorInfo?.correspondingAuthor?.lastName || 'Unknown' }, + affiliation: { + $: input.authorInfo?.correspondingAuthor?.affiliation || 'Not specified', + }, + }, + }, + ], + }, + titles: { + $: [{ title: { $: input.generalInfo?.title || '' } }], + }, + publisher: { $: 'NRC CADC' }, + publicationYear: { $: String(publicationYear) }, + resourceType: { + '@resourceTypeGeneral': 'Dataset', + $: 'Dataset', + }, + }, + } + + if (input.authorInfo?.contributingAuthors && input.authorInfo.contributingAuthors.length > 0) { + dataCite.resource.contributors = { + $: input.authorInfo.contributingAuthors.map((author) => ({ + contributor: { + '@contributorType': 'Researcher', + contributorName: { + $: `${author.lastName || 'Unknown'}, ${author.firstName || 'Unknown'}`, + }, + givenName: { $: author.firstName || 'Unknown' }, + familyName: { $: author.lastName || 'Unknown' }, + affiliation: { $: author.affiliation || 'Not specified' }, + }, + })), + } + } + + return dataCite +} + +export default convertToDataCite diff --git a/rafts/frontend/src/utilities/localStorage.ts b/rafts/frontend/src/utilities/localStorage.ts new file mode 100644 index 0000000..efd8df9 --- /dev/null +++ b/rafts/frontend/src/utilities/localStorage.ts @@ -0,0 +1,115 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +/** + * Utility functions for managing form state persistence with localStorage + */ +import { TRaftContext } from '@/context/types' + +const STORAGE_KEY = 'raft_form_data' + +/** + * Save RAFT form data to localStorage + */ +export const saveRaftData = (data: TRaftContext): void => { + if (typeof window !== 'undefined') { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) + } catch (error) { + console.error('Error saving RAFT data to localStorage:', error) + } + } +} + +/** + * Load RAFT form data from localStorage + */ +export const loadRaftData = (): TRaftContext | null => { + if (typeof window !== 'undefined') { + try { + const savedData = localStorage.getItem(STORAGE_KEY) + return savedData ? JSON.parse(savedData) : null + } catch (error) { + console.error('Error loading RAFT data from localStorage:', error) + return null + } + } + return null +} + +/** + * Clear RAFT form data from localStorage + */ +export const clearRaftData = (): void => { + if (typeof window !== 'undefined') { + try { + localStorage.removeItem(STORAGE_KEY) + } catch (error) { + console.error('Error clearing RAFT data from localStorage:', error) + } + } +} diff --git a/rafts/frontend/src/utilities/validation.ts b/rafts/frontend/src/utilities/validation.ts new file mode 100644 index 0000000..1f10bac --- /dev/null +++ b/rafts/frontend/src/utilities/validation.ts @@ -0,0 +1,141 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { VALIDATION_SCHEMAS } from '@/context/constants' +import { ZodError } from 'zod' + +/** + * Validates an object against a Zod schema + * + * @param schema - The Zod schema to validate against + * @param data - The object to validate + * @returns True if valid, false otherwise + */ +export const validateWithSchema = ( + schema: (typeof VALIDATION_SCHEMAS)[T], + data: unknown, +): boolean => { + try { + // Attempt to parse the data with the schema + schema.parse(data) + return true + } catch { + // If Zod throws an error, the validation failed + return false + } +} + +/** + * Validates an object against a Zod schema and returns formatted errors + * + * @param schema - The Zod schema to validate against + * @param data - The object to validate + * @returns Object with field errors or undefined if valid + */ +interface ValidationErrorObject { + [key: string]: string | ValidationErrorObject +} + +type ValidationError = string | ValidationErrorObject + +export const getValidationErrors = ( + schema: (typeof VALIDATION_SCHEMAS)[T], + data: unknown, +): Record | undefined => { + try { + schema.parse(data) + return undefined + } catch (error) { + if (error instanceof ZodError) { + // Convert ZodError to a nested object structure matching the data structure + const formattedErrors: Record = {} + + error.issues.forEach((err) => { + let current = formattedErrors + const path = [...err.path] + const lastKey = path.pop() + + // Navigate to the correct nested level + path.forEach((key) => { + const keyStr = String(key) + if (!current[keyStr]) { + current[keyStr] = {} + } + current = current[keyStr] as Record + }) + + // Set the error message at the final key + if (lastKey !== undefined) { + current[String(lastKey)] = err.message + } + }) + + return formattedErrors + } + return undefined + } +} diff --git a/rafts/frontend/src/utilities/xmlParser.ts b/rafts/frontend/src/utilities/xmlParser.ts new file mode 100644 index 0000000..d738bd3 --- /dev/null +++ b/rafts/frontend/src/utilities/xmlParser.ts @@ -0,0 +1,265 @@ +/* + ************************************************************************ + ******************* CANADIAN ASTRONOMY DATA CENTRE ******************* + ************** CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES ************** + * + * (c) 2026. (c) 2026. + * Government of Canada Gouvernement du Canada + * National Research Council Conseil national de recherches + * Ottawa, Canada, K1A 0R6 Ottawa, Canada, K1A 0R6 + * All rights reserved Tous droits réservés + * + * NRC disclaims any warranties, Le CNRC dénie toute garantie + * expressed, implied, or énoncée, implicite ou légale, + * statutory, of any kind with de quelque nature que ce + * respect to the software, soit, concernant le logiciel, + * including without limitation y compris sans restriction + * any warranty of merchantability toute garantie de valeur + * or fitness for a particular marchande ou de pertinence + * purpose. NRC shall not be pour un usage particulier. + * liable in any event for any Le CNRC ne pourra en aucun cas + * damages, whether direct or être tenu responsable de tout + * indirect, special or general, dommage, direct ou indirect, + * consequential or incidental, particulier ou général, + * arising from the use of the accessoire ou fortuit, résultant + * software. Neither the name de l'utilisation du logiciel. Ni + * of the National Research le nom du Conseil National de + * Council of Canada nor the Recherches du Canada ni les noms + * names of its contributors may de ses participants ne peuvent + * be used to endorse or promote être utilisés pour approuver ou + * products derived from this promouvoir les produits dérivés + * software without specific prior de ce logiciel sans autorisation + * written permission. préalable et particulière + * par écrit. + * + * This file is part of the Ce fichier fait partie du projet + * OpenCADC project. OpenCADC. + * + * OpenCADC is free software: OpenCADC est un logiciel libre ; + * you can redistribute it and/or vous pouvez le redistribuer ou le + * modify it under the terms of modifier suivant les termes de + * the GNU Affero General Public la "GNU Affero General Public + * License as published by the License" telle que publiée + * Free Software Foundation, par la Free Software Foundation + * either version 3 of the : soit la version 3 de cette + * License, or (at your option) licence, soit (à votre gré) + * any later version. toute version ultérieure. + * + * OpenCADC is distributed in the OpenCADC est distribué + * hope that it will be useful, dans l'espoir qu'il vous + * but WITHOUT ANY WARRANTY; sera utile, mais SANS AUCUNE + * without even the implied GARANTIE : sans même la garantie + * warranty of MERCHANTABILITY implicite de COMMERCIALISABILITÉ + * or FITNESS FOR A PARTICULAR ni d'ADÉQUATION À UN OBJECTIF + * PURPOSE. See the GNU Affero PARTICULIER. Consultez la Licence + * General Public License for Générale Publique GNU Affero + * more details. pour plus de détails. + * + * You should have received Vous devriez avoir reçu une + * a copy of the GNU Affero copie de la Licence Générale + * General Public License along Publique GNU Affero avec + * with OpenCADC. If not, see OpenCADC ; si ce n'est + * . pas le cas, consultez : + * . + * + ************************************************************************ + */ + +import { DOIData } from '@/types/doi' + +/** + * Parse DOI status XML string into a JSON array + * Works in both client and server environments for Next.js + * + * @param {string} xmlString - XML string to parse + * @returns {Promise} Promise resolving to an array of DOI status objects + */ +export const parseXmlToJson = async (xmlString: string): Promise => { + // Check if we're running on the client or server + const isClient = typeof window !== 'undefined' + + return isClient ? parseWithDOMParser(xmlString) : await parseWithServerMethod(xmlString) +} + +/** + * Client-side parser using browser's DOMParser + */ +const parseWithDOMParser = (xmlString: string): DOIData[] => { + const parser = new DOMParser() + const xmlDoc = parser.parseFromString(xmlString, 'text/xml') + + // Check for parser errors + const parserError = xmlDoc.querySelector('parsererror') + if (parserError) { + console.error('XML parsing error:', parserError.textContent) + throw new Error('Invalid XML format') + } + + return extractDoiStatus(xmlDoc) +} + +interface DoiItemAttribute { + identifierType?: string + 'xml:lang'?: string +} + +interface DoiItemValue { + _?: string + attr?: DoiItemAttribute +} + +interface DoiStatusItem { + identifier?: string | DoiItemValue + title?: string | DoiItemValue + status?: string | DoiItemValue + dataDirectory?: string | DoiItemValue + journalRef?: string | DoiItemValue + reviewer?: string | DoiItemValue +} + +interface DoiStatusesResult { + doiStatuses?: { + doistatus?: DoiStatusItem | DoiStatusItem[] + } +} + +/** + * Server-side parser using dynamic import of xml2js + * This avoids including xml2js in client bundles + */ +const parseWithServerMethod = async (xmlString: string): Promise => { + // Dynamically import xml2js (only on server) + const { parseStringPromise } = await import('xml2js') + + try { + const result: DoiStatusesResult = await parseStringPromise(xmlString, { + explicitArray: false, + attrkey: 'attr', + }) + + if (!result.doiStatuses || !result.doiStatuses.doistatus) { + return [] + } + + // Ensure doistatus is always treated as an array + const doiStatuses: DoiStatusItem[] = Array.isArray(result.doiStatuses.doistatus) + ? result.doiStatuses.doistatus + : [result.doiStatuses.doistatus] + + return doiStatuses.map((item: DoiStatusItem): DOIData => { + // Handle identifier + const identifier: string = + typeof item.identifier === 'object' + ? item.identifier?._?.toString() || '' + : item.identifier?.toString() || '' + + const identifierType: string | null = + typeof item.identifier === 'object' ? item.identifier?.attr?.identifierType || '' : '' + + // Handle title + const title: string = + typeof item.title === 'object' + ? item.title?._?.toString() || '' + : item.title?.toString() || '' + + const titleLang: string | null = + typeof item.title === 'object' ? item.title?.attr?.['xml:lang'] || null : null + + // Handle other elements + const status: string = + typeof item.status === 'object' + ? item.status?._?.toString() || '' + : item.status?.toString() || '' + + const dataDirectory: string = + typeof item.dataDirectory === 'object' + ? item.dataDirectory?._?.toString() || '' + : item.dataDirectory?.toString() || '' + + const journalRef: string | null = + typeof item.journalRef === 'object' + ? item.journalRef?._?.toString() || null + : item.journalRef?.toString() || null + + const reviewer: string | null = + typeof item.reviewer === 'object' + ? item.reviewer?._?.toString() || null + : item.reviewer?.toString() || null + + return { + identifier, + identifierType, + title, + titleLang, + status, + dataDirectory, + journalRef: journalRef ? journalRef.trim() : null, + reviewer: reviewer ? reviewer.trim() : null, + } + }) + } catch (error) { + console.error('Error parsing XML:', error) + throw new Error('Failed to parse DOI status XML') + } +} + +/** + * Extract DOI status data from DOM document + */ +const extractDoiStatus = (xmlDoc: Document): DOIData[] => { + const doiStatusElements = xmlDoc.getElementsByTagName('doistatus') + const results: DOIData[] = [] + + for (let i = 0; i < doiStatusElements.length; i++) { + const item = doiStatusElements[i] + + // Extract the identifier + const identifierElement = item.getElementsByTagName('identifier')[0] + const identifier = identifierElement?.textContent || '' + const identifierType = identifierElement?.getAttribute('identifierType') || '' + + // Extract the title + const titleElement = item.getElementsByTagName('title')[0] + const title = titleElement?.textContent || '' + const titleLang = titleElement?.getAttribute('xml:lang') || '' + + // Extract other elements + const status = getElementText(item, 'status') + const dataDirectory = getElementText(item, 'dataDirectory') + const journalRef = getElementText(item, 'journalRef') + const reviewer = getElementText(item, 'reviewer') + + // Create result object + results.push({ + identifier, + identifierType, + title, + titleLang, + status, + dataDirectory, + journalRef: journalRef.trim() || null, + reviewer: reviewer.trim() || null, + }) + } + + return results +} + +/** + * Helper function to get text content from an element + */ +const getElementText = (parentElement: Element, tagName: string): string => { + const element = parentElement.getElementsByTagName(tagName)[0] + return element ? element.textContent || '' : '' +} + +/** + * Helper to modify dataDirectory in XML + */ +export const modifyDataDirectoryInXml = (xml: string, newDataDirectory: string): string => { + // Replace the dataDirectory element content + return xml.replace( + /([^<]*)<\/dataDirectory>/, + `${newDataDirectory}`, + ) +} diff --git a/rafts/frontend/src/version.json b/rafts/frontend/src/version.json new file mode 100644 index 0000000..acee13e --- /dev/null +++ b/rafts/frontend/src/version.json @@ -0,0 +1,5 @@ +{ + "version": "0.544", + "date": "February 12, 2026", + "timestamp": "2026-02-12T18:40:49.216Z" +} \ No newline at end of file diff --git a/rafts/frontend/tailwind.config.ts b/rafts/frontend/tailwind.config.ts new file mode 100644 index 0000000..e4e4336 --- /dev/null +++ b/rafts/frontend/tailwind.config.ts @@ -0,0 +1,48 @@ +const config = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + darkMode: 'class', // Enable dark mode with class strategy + theme: { + extend: { + colors: { + background: 'var(--background)', + foreground: 'var(--foreground)', + + // Form-specific colors + form: { + input: { + bg: 'var(--input-background)', + border: 'var(--input-border)', + text: 'var(--input-text)', + focus: { + border: 'var(--input-focus-border)', + ring: 'var(--input-focus-ring)', + }, + }, + label: 'var(--label-text)', + helper: 'var(--helper-text)', + heading: 'var(--header-text)', + fieldset: { + bg: 'var(--fieldset-background)', + border: 'var(--fieldset-border)', + legend: 'var(--legend-text)', + }, + button: { + bg: 'var(--button-background)', + hover: 'var(--button-hover)', + text: 'var(--button-text)', + }, + }, + }, + // Other theme extensions can go here + }, + }, + corePlugins: { + preflight: false, // Prevent Tailwind from resetting MUI styles + }, + plugins: [], +} +export default config diff --git a/rafts/frontend/tsconfig.json b/rafts/frontend/tsconfig.json new file mode 100644 index 0000000..fd8d7e7 --- /dev/null +++ b/rafts/frontend/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/rafts/frontend/vitest.config.ts b/rafts/frontend/vitest.config.ts new file mode 100644 index 0000000..5ee5112 --- /dev/null +++ b/rafts/frontend/vitest.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./src/__tests__/setup.ts'], + include: ['src/**/*.{test,spec}.{ts,tsx}'], + exclude: ['node_modules', '.next', 'src/tests/mock-*.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html', 'lcov'], + include: ['src/**/*.{ts,tsx}'], + exclude: [ + 'src/**/*.d.ts', + 'src/**/__tests__/**', + 'src/tests/**', + 'src/types/**', + 'src/**/index.ts', + ], + thresholds: { + statements: 60, + branches: 50, + functions: 60, + lines: 60, + }, + }, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}) diff --git a/rafts/nginx/conf.d/default.conf b/rafts/nginx/conf.d/default.conf new file mode 100644 index 0000000..ca3d216 --- /dev/null +++ b/rafts/nginx/conf.d/default.conf @@ -0,0 +1,101 @@ +# RAFTS Nginx Configuration +# Handles HTTP/HTTPS routing for the RAFTS application + +# Upstream definitions +upstream rafts_frontend { + server rafts-frontend:8080; +} + +upstream rafts_validator { + server ades-validator-api:8000; +} + +# Health check endpoint (for nginx itself) +server { + listen 80; + server_name localhost; + + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} + +# HTTP Server - Redirect to HTTPS (when SSL is enabled) +server { + listen 80; + server_name ${RAFTS_DOMAIN:-rafts.localhost}; + + # Let's Encrypt challenge + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect all HTTP to HTTPS (uncomment when SSL is configured) + # location / { + # return 301 https://$host$request_uri; + # } + + # Without SSL - proxy directly + location / { + proxy_pass http://rafts_frontend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 60s; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + } + + # Validator API (optional - if exposed directly) + location /validator/ { + rewrite ^/validator/(.*) /$1 break; + proxy_pass http://rafts_validator; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# HTTPS Server (uncomment and configure when SSL certificates are available) +# server { +# listen 443 ssl http2; +# server_name ${RAFTS_DOMAIN:-rafts.localhost}; +# +# ssl_certificate /etc/letsencrypt/live/${RAFTS_DOMAIN}/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/${RAFTS_DOMAIN}/privkey.pem; +# +# ssl_session_timeout 1d; +# ssl_session_cache shared:SSL:50m; +# ssl_session_tickets off; +# +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; +# ssl_prefer_server_ciphers off; +# +# # HSTS +# add_header Strict-Transport-Security "max-age=63072000" always; +# +# location / { +# proxy_pass http://rafts_frontend; +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection 'upgrade'; +# proxy_set_header Host $host; +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_cache_bypass $http_upgrade; +# proxy_read_timeout 60s; +# proxy_connect_timeout 60s; +# proxy_send_timeout 60s; +# } +# } diff --git a/rafts/nginx/nginx.conf b/rafts/nginx/nginx.conf new file mode 100644 index 0000000..5d26573 --- /dev/null +++ b/rafts/nginx/nginx.conf @@ -0,0 +1,35 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css text/xml application/json application/javascript application/xml; + + # Include additional configuration files + include /etc/nginx/conf.d/*.conf; +}