diff --git a/.github/workflows/landing-page-deploy.yml b/.github/workflows/landing-page-deploy.yml index f49b86331..2b0498bca 100644 --- a/.github/workflows/landing-page-deploy.yml +++ b/.github/workflows/landing-page-deploy.yml @@ -11,9 +11,9 @@ on: workflow_dispatch: inputs: source_branch: - description: 'Branch to use for build scripts (defaults to current branch)' + description: 'Branch to use for build scripts (defaults to deploy branch)' required: false - default: '' + default: 'deploy' permissions: contents: write @@ -28,9 +28,10 @@ jobs: deploy-landing-page: runs-on: ubuntu-latest steps: - - name: Checkout repository + - name: Checkout deploy branch (or fallback to main) uses: actions/checkout@v4 with: + ref: main # Use main branch since deploy branch doesn't exist yet fetch-depth: 0 # Fetch full history for gh-pages branch - name: Configure git user @@ -54,16 +55,55 @@ jobs: set -e echo "Building self-contained landing page..." - # Use current branch for build scripts unless specified - source_branch="${{ github.event.inputs.source_branch }}" - if [[ -z "$source_branch" ]]; then - source_branch="${{ github.ref_name }}" - fi - - echo "Using build scripts from branch: $source_branch" - # Build the landing page with self-contained assets - node scripts/build-multi-branch.js landing + # Check if we have landing page assets from deploy branch + if [[ -f "public/branch-listing.html" ]]; then + echo "Using landing page assets from deploy branch..." + npm run build:landing + else + echo "Deploy branch assets not available, creating basic landing page..." + + # Create a minimal landing page build + mkdir -p build + + cat > build/index.html << 'EOF' + + + + + + SGEX Workbench + + + +
+

🚀 SGEX Workbench

+

WHO SMART Guidelines Exchange

+ +
+

🌟 Main Application

+

Launch SGEX Workbench (Main Branch)

+
+ +
+

📝 Branch & PR Previews

+

Branch and pull request previews are automatically deployed to subdirectories.

+

Each branch is accessible at /branch-name/

+
+ +
+

â„šī¸ About

+

SGEX is an experimental collaborative project developing a workbench of tools to make it easier and faster to develop high fidelity SMART Guidelines Digital Adaptation Kits (DAKs).

+
+
+ + +EOF + fi # Verify index.html exists if [[ ! -f "build/index.html" ]]; then @@ -73,7 +113,7 @@ jobs: echo "✅ Landing page build completed" - - name: Deploy to gh-pages root + - name: Deploy to gh-pages root (preserving existing branches) shell: bash run: | set -e @@ -99,43 +139,43 @@ jobs: git push origin gh-pages fi - # Preserve all existing branch directories (exclude problematic cache directories) - echo "Preserving existing branch directories..." - if ls -d */ > /dev/null 2>&1; then - mkdir -p /tmp/branch-backup - for dir in */; do - if [[ "$dir" != "node_modules/" && "$dir" != ".cache/" ]]; then - echo "Backing up directory: $dir" - # Exclude cache subdirectories when copying - rsync -av --exclude='*/node_modules/.cache' --exclude='*/.cache' "$dir" /tmp/branch-backup/ - fi - done + # CRITICAL: Only remove files, NEVER remove directories (to preserve branch builds) + echo "Removing only root-level files (preserving all branch directories)..." + + # List what directories exist for safety verification + echo "Existing directories before cleanup:" + ls -la */ 2>/dev/null || echo "No directories found" + + # Remove only specific files that could conflict with landing page + rm -f index.html + rm -f manifest.json + rm -f asset-manifest.json + rm -f service-worker.js + rm -f robots.txt + rm -f favicon.ico + rm -f logo*.png + rm -f README.md + rm -f package.json + rm -f package-lock.json + + # Remove static directory only if it exists at root (not in branch subdirs) + if [[ -d "static" ]]; then + echo "Removing root static directory..." + rm -rf static fi - # Clear root (except .git and branch directories) - echo "Clearing root directory for landing page deployment..." - find . -maxdepth 1 -not -name '.' -not -name '.git' -not -name '.github' -exec rm -rf {} + - - # Also clean up any cache directories that might exist - echo "Cleaning up cache directories..." - find . -name "node_modules/.cache" -type d -exec rm -rf {} + 2>/dev/null || true - find . -name ".cache" -type d -exec rm -rf {} + 2>/dev/null || true - - # Deploy landing page to root + # Deploy landing page to root (this will not affect any subdirectories) echo "Deploying landing page to root..." cp -a /tmp/landing-build/. . - # Restore all branch directories - if [[ -d /tmp/branch-backup ]]; then - echo "Restoring branch directories..." - cp -r /tmp/branch-backup/* . - rm -rf /tmp/branch-backup - fi + # Verify existing directories are still there + echo "Directories after deployment:" + ls -la */ 2>/dev/null || echo "No directories found" # Clean up temporary build rm -rf /tmp/landing-build - echo "✅ Landing page deployed to gh-pages root" + echo "✅ Landing page deployed to gh-pages root while preserving all branch directories" - name: Commit and push changes shell: bash @@ -161,7 +201,7 @@ jobs: # Determine deployment type for commit message if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then deployment_type="manual" - trigger_info="Triggered from branch: ${{ github.ref_name }}" + trigger_info="Triggered from deploy branch" else deployment_type="automatic" trigger_info="Auto-triggered by push to main" @@ -169,7 +209,7 @@ jobs: git commit -m "🏠 Deploy landing page (${deployment_type}) - - Updated landing page with self-contained assets + - Updated landing page with self-contained assets from deploy branch - ${trigger_info} - Deployed at $(date -u '+%Y-%m-%d %H:%M:%S UTC') - Commit: ${{ github.sha }}" @@ -189,7 +229,7 @@ jobs: # Determine deployment type for output message if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then deployment_type="Manual" - trigger_info="Triggered from branch: ${{ github.ref_name }}" + trigger_info="Triggered from deploy branch" else deployment_type="Automatic" trigger_info="Auto-triggered by push to main" @@ -200,4 +240,5 @@ jobs: echo "- Deployment Type: $deployment_type" echo "- $trigger_info" echo "- Deployed at: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" + echo "- Source Branch: ${{ github.event.inputs.source_branch || 'deploy' }}" echo "- Commit: ${{ github.sha }}" \ No newline at end of file diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 9de6edc78..d52228024 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -73,6 +73,44 @@ jobs: export BASE_PATH="/${safe_branch_name}/" fi echo "Building with BASE_PATH: $BASE_PATH" + + # Get build script from deploy branch (fallback to local if deploy branch doesn't exist) + echo "Fetching build script from deploy branch..." + if git fetch origin deploy 2>/dev/null; then + echo "Deploy branch found, using build script from deploy branch" + git checkout origin/deploy -- scripts/build-multi-branch.js + else + echo "Deploy branch not found, checking if build script exists locally..." + if [[ ! -f "scripts/build-multi-branch.js" ]]; then + echo "ERROR: Build script not found and deploy branch doesn't exist" + echo "Creating minimal build script as fallback..." + mkdir -p scripts + cat > scripts/build-multi-branch.js << 'EOF' +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +console.log('Building React app for deployment...'); + +// Simplified build process +try { + // Set environment variables + process.env.NODE_ENV = 'production'; + process.env.PUBLIC_URL = process.env.BASE_PATH || '/'; + + // Run the build + execSync('npm run build', { stdio: 'inherit' }); + + console.log('✅ Build completed successfully'); +} catch (error) { + console.error('❌ Build failed:', error.message); + process.exit(1); +} +EOF + fi + fi + + # Run the build script node scripts/build-multi-branch.js branch env: CI: false @@ -224,10 +262,12 @@ jobs: branch_dir="${{ steps.validate_branch.outputs.branch_dir }}" repo_root="$(pwd)" + target_subdir="${{ steps.validate_branch.outputs.target_subdir }}" echo "Cleaning old deployment" echo "Branch directory: $branch_dir" echo "Repository root: $repo_root" + echo "Target subdirectory: $target_subdir" # Validate before any destructive operations if [[ "$branch_dir" != "$repo_root"* ]] || [[ ${#branch_dir} -le ${#repo_root} ]]; then @@ -235,10 +275,22 @@ jobs: exit 1 fi - # Use git to safely remove the branch directory + # Additional safety: ensure we're only removing the specific branch directory + if [[ "$target_subdir" == "." || "$target_subdir" == ".." || -z "$target_subdir" ]]; then + echo "ERROR: Invalid target subdirectory: '$target_subdir'" + exit 1 + fi + + # Use git to safely remove the specific branch directory only if [[ -d "$branch_dir" ]]; then echo "Removing existing deployment: $branch_dir" - git rm -rf "$branch_dir" || echo "Directory didn't exist in git" + # Additional safety: only remove if it matches our expected pattern + if [[ "$branch_dir" == "$repo_root/$target_subdir" ]]; then + git rm -rf "$target_subdir" 2>/dev/null || echo "Directory didn't exist in git" + else + echo "ERROR: Branch directory path doesn't match expected pattern" + exit 1 + fi else echo "No existing deployment to clean" fi diff --git a/.gitignore b/.gitignore index 28842349b..77730a93f 100644 --- a/.gitignore +++ b/.gitignore @@ -95,4 +95,7 @@ temp_serve/ # Generated QA reports (auto-generated by CI, should not be committed) docs/qa-report.html public/docs/qa-report.html -docs/github-issues-analysis.md \ No newline at end of file +docs/github-issues-analysis.md + +# Deployment structure (generated by CI/CD) +sgex/ \ No newline at end of file diff --git a/package.json b/package.json index 79b890f96..3ffe2f430 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "scripts": { "start": "craco start", "build": "react-scripts build", - "build:multi-branch": "node scripts/build-multi-branch.js", "test": "react-scripts test", "eject": "react-scripts eject", "serve": "npm run build && cd build && python3 -m http.server 3000", diff --git a/public/branch-listing.html b/public/branch-listing.html deleted file mode 100644 index 640ab1bae..000000000 --- a/public/branch-listing.html +++ /dev/null @@ -1,2021 +0,0 @@ - - - - - - SGEX Branch & PR Previews - - - - - - -
- - - - \ No newline at end of file diff --git a/scripts/build-multi-branch.js b/scripts/build-multi-branch.js deleted file mode 100755 index 085d523fd..000000000 --- a/scripts/build-multi-branch.js +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env node - -/** - * Multi-branch build script for SGEX - * - * This script handles two different build scenarios: - * 1. Branch-specific builds (for deployment to subdirectories) - * 2. Root landing page build (for branch listing at gh-pages root) - */ - -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); - -// Get build mode from environment or command line -const buildMode = process.env.BUILD_MODE || process.argv[2] || 'branch'; -const branchName = process.env.GITHUB_REF_NAME || 'main'; -const basePath = process.env.BASE_PATH || process.argv[3] || null; - -console.log(`🚀 Starting ${buildMode} build for branch: ${branchName}`); - -function ensureDependencies() { - const nodeModulesPath = path.join(__dirname, '..', 'node_modules'); - if (!fs.existsSync(nodeModulesPath)) { - console.log('đŸ“Ļ Installing dependencies...'); - execSync('npm ci', { stdio: 'inherit' }); - } -} - -function createBranchSpecificBuild() { - console.log('đŸ“Ļ Building branch-specific React app...'); - - // Ensure dependencies are available - ensureDependencies(); - - // Update package.json homepage for branch-specific deployment - const packageJsonPath = path.join(__dirname, '..', 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - const originalHomepage = packageJson.homepage; - - try { - // Set homepage based on branch and base path - let deploymentPath; - if (basePath) { - // For GitHub Pages, ensure the repository name is included in the path - // basePath comes in format like "/copilot-fix-418/" but needs to be "/sgex/copilot-fix-418/" - if (basePath.startsWith('/') && !basePath.startsWith('/sgex/')) { - deploymentPath = `/sgex${basePath}`; - } else { - deploymentPath = basePath; - } - packageJson.homepage = deploymentPath; - console.log(`🔧 Setting homepage to: ${deploymentPath}`); - } else { - // Default path structure: /sgex/main/ for main, /sgex/safe-branch-name/ for others - const safeBranchName = branchName === 'main' ? 'main' : branchName.replace(/[^a-zA-Z0-9._-]/g, '-'); - deploymentPath = `/sgex/${safeBranchName}/`; - packageJson.homepage = deploymentPath; - console.log(`🔧 Setting homepage to: ${deploymentPath}`); - } - - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - - // Update manifest.json for subdirectory deployment - const manifestPath = path.join(__dirname, '..', 'public', 'manifest.json'); - const manifestBackupPath = path.join(__dirname, '..', 'public', 'manifest.json.backup'); - - if (fs.existsSync(manifestPath)) { - // Backup original manifest.json - fs.copyFileSync(manifestPath, manifestBackupPath); - - // Update manifest.json paths for subdirectory deployment - const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - const originalManifest = { ...manifest }; - - // Update start_url for subdirectory deployment - manifest.start_url = deploymentPath; - - // Update icon paths for subdirectory deployment - if (manifest.icons && Array.isArray(manifest.icons)) { - manifest.icons = manifest.icons.map(icon => ({ - ...icon, - src: icon.src.startsWith('/') ? icon.src : `${deploymentPath}${icon.src}` - })); - } - - fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); - console.log(`🔧 Updated manifest.json for subdirectory deployment`); - } - - // Standard React build for the branch - execSync('npm run build', { stdio: 'inherit' }); - - console.log(`✅ Branch-specific build completed for: ${branchName}`); - - } finally { - // Always restore original package.json - packageJson.homepage = originalHomepage; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - - // Restore original manifest.json if backup exists - const manifestPath = path.join(__dirname, '..', 'public', 'manifest.json'); - const manifestBackupPath = path.join(__dirname, '..', 'public', 'manifest.json.backup'); - - if (fs.existsSync(manifestBackupPath)) { - fs.copyFileSync(manifestBackupPath, manifestPath); - fs.unlinkSync(manifestBackupPath); - console.log(`🔧 Restored original manifest.json`); - } - } -} - -function createRootLandingPageApp() { - console.log('🏠 Creating self-contained root landing page application...'); - - // Ensure dependencies are available - ensureDependencies(); - - // Create a temporary React app that only renders BranchListing with self-contained assets - const tempAppContent = ` -import React from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import BranchListing from './components/BranchListing'; -import './App.css'; - -function App() { - return ( - -
- -
-
- ); -} - -export default App; -`; - - // Backup original App.js - const appJsPath = path.join(__dirname, '..', 'src', 'App.js'); - const appJsBackupPath = path.join(__dirname, '..', 'src', 'App.js.backup'); - - if (fs.existsSync(appJsPath)) { - fs.copyFileSync(appJsPath, appJsBackupPath); - } - - try { - // Write temporary App.js for landing page - fs.writeFileSync(appJsPath, tempAppContent.trim()); - - // Update package.json homepage for root deployment - const packageJsonPath = path.join(__dirname, '..', 'package.json'); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - const originalHomepage = packageJson.homepage; - - // For root landing page, we want it to be at the GitHub Pages root for this repository - packageJson.homepage = '/sgex/'; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - - // Update manifest.json for root deployment - const manifestPath = path.join(__dirname, '..', 'public', 'manifest.json'); - const manifestBackupPath = path.join(__dirname, '..', 'public', 'manifest.json.backup-landing'); - - if (fs.existsSync(manifestPath)) { - // Backup original manifest.json - fs.copyFileSync(manifestPath, manifestBackupPath); - - // Update manifest.json for root deployment - const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); - - // Update start_url for root deployment - manifest.start_url = '/sgex/'; - - // Update icon paths for root deployment - ensure they're self-contained - if (manifest.icons && Array.isArray(manifest.icons)) { - manifest.icons = manifest.icons.map(icon => ({ - ...icon, - src: icon.src.startsWith('/') ? `/sgex${icon.src}` : `/sgex/${icon.src}` - })); - } - - fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); - console.log(`🔧 Updated manifest.json for self-contained root deployment`); - } - - // Build the landing page app - execSync('npm run build', { stdio: 'inherit' }); - - // Restore original package.json - packageJson.homepage = originalHomepage; - fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - - console.log('✅ Self-contained root landing page build completed'); - - } finally { - // Always restore original App.js - if (fs.existsSync(appJsBackupPath)) { - fs.copyFileSync(appJsBackupPath, appJsPath); - fs.unlinkSync(appJsBackupPath); - } - - // Restore original manifest.json if backup exists - const manifestPath = path.join(__dirname, '..', 'public', 'manifest.json'); - const manifestBackupPath = path.join(__dirname, '..', 'public', 'manifest.json.backup-landing'); - - if (fs.existsSync(manifestBackupPath)) { - fs.copyFileSync(manifestBackupPath, manifestPath); - fs.unlinkSync(manifestBackupPath); - console.log(`🔧 Restored original manifest.json`); - } - } -} - -// Execute based on build mode -if (buildMode === 'root' || buildMode === 'landing') { - createRootLandingPageApp(); -} else { - createBranchSpecificBuild(); -} - -console.log(`🎉 Build process completed successfully!`); \ No newline at end of file diff --git a/scripts/test-deployment.sh b/scripts/test-deployment.sh index 15e66d253..a7d925bfc 100755 --- a/scripts/test-deployment.sh +++ b/scripts/test-deployment.sh @@ -69,6 +69,14 @@ echo "-------------------------" echo "Testing branch-specific build..." export GITHUB_REF_NAME="test-branch" + +# Fetch build script from deploy branch (if available) +if git show-ref --verify --quiet refs/remotes/origin/deploy; then + echo "Fetching build script from deploy branch..." + git fetch origin deploy + git checkout origin/deploy -- scripts/build-multi-branch.js +fi + if node scripts/build-multi-branch.js branch > /dev/null 2>&1; then echo "✅ Branch-specific build: Success" if [[ -f "build/index.html" ]]; then @@ -82,6 +90,12 @@ fi echo "" echo "Testing root landing page build..." + +# Fetch landing page from deploy branch (if available) +if git show-ref --verify --quiet refs/remotes/origin/deploy; then + git checkout origin/deploy -- public/branch-listing.html +fi + if node scripts/build-multi-branch.js root > /dev/null 2>&1; then echo "✅ Root landing page build: Success" if [[ -f "build/index.html" ]]; then @@ -123,24 +137,24 @@ echo "" echo "đŸ§Ē Test 4: Component validation" echo "------------------------------" -echo "Testing BranchListing component..." -if [[ -f "src/components/BranchListing.js" ]] && [[ -f "src/components/BranchListing.css" ]]; then - echo "✅ BranchListing component files exist" +echo "Testing deployment components..." +if [[ -f "public/branch-listing.html" ]] && [[ -f "public/sgex-mascot.png" ]]; then + echo "✅ Landing page assets exist" - # Check for key elements in component - if grep -q "GitHub API" src/components/BranchListing.js; then - echo "✅ Component includes GitHub API integration" + # Check for key elements in landing page + if grep -q "GitHub API" public/branch-listing.html; then + echo "✅ Landing page includes GitHub API integration" fi - if grep -q "branch-card" src/components/BranchListing.css; then - echo "✅ Component includes card styling" + if grep -q "branch-card" public/branch-listing.html; then + echo "✅ Landing page includes card styling" fi - if grep -q "safeName" src/components/BranchListing.js; then - echo "✅ Component handles safe branch names" + if grep -q "safeBranchName" public/branch-listing.html; then + echo "✅ Landing page handles safe branch names" fi else - echo "❌ BranchListing component files missing" + echo "âš ī¸ Landing page assets may need to be fetched from deploy branch" fi # Test 5: Workflow file validation diff --git a/src/App.js b/src/App.js index a1f81ec84..bbc21000e 100644 --- a/src/App.js +++ b/src/App.js @@ -27,7 +27,6 @@ import DAKDashboardWithFramework from './components/DAKDashboardWithFramework'; import DashboardRedirect from './components/DashboardRedirect'; import TestDocumentationPage from './components/TestDocumentationPage'; import AssetEditorTest from './components/AssetEditorTest'; -import BranchListing from './components/BranchListing'; import logger from './utils/logger'; import './App.css'; @@ -112,7 +111,6 @@ function App() { } /> } /> } /> - } /> } /> diff --git a/src/components/BranchListing.css b/src/components/BranchListing.css deleted file mode 100644 index 18d11cb07..000000000 --- a/src/components/BranchListing.css +++ /dev/null @@ -1,1315 +0,0 @@ -.branch-listing { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; -} - -.branch-listing-header { - text-align: center; - margin-bottom: 2rem; -} - -.branch-listing-header h1 { - font-size: 2.5rem; - color: #2c3e50; - margin-bottom: 0.5rem; - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; -} - -.sgex-icon { - width: 48px; - height: 48px; - object-fit: contain; -} - -.branch-listing-header .subtitle { - font-size: 1.2rem; - color: #666; - margin: 0 0 1rem 0; - font-style: italic; -} - -.prominent-info { - background: var(--who-secondary-bg, #f8f9fa); - border: 2px solid var(--who-blue, #0366d6); - border-radius: 12px; - padding: 1.5rem; - margin: 1.5rem 0; - text-align: center; -} - -.info-text { - font-size: 1.1rem; - font-weight: 600; - color: var(--who-text-primary, #333); - margin: 0; -} - -.cache-status { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid var(--who-border, #e1e4e8); -} - -.cache-info { - font-size: 0.9rem; - color: var(--who-text-secondary, #666); - display: flex; - align-items: center; - gap: 0.5rem; -} - -.refresh-btn { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - background: var(--who-blue, #0366d6); - color: white; - border: none; - border-radius: 6px; - font-size: 0.9rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} - -.refresh-btn:hover:not(:disabled) { - background: var(--who-blue-dark, #0256c2); - transform: translateY(-1px); -} - -.refresh-btn:disabled { - opacity: 0.7; - cursor: not-allowed; - transform: none; -} - -.refresh-btn:disabled:hover { - background: var(--who-blue, #0366d6); -} - -/* Authentication section */ -.auth-section { - background: var(--who-tertiary-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 12px; - padding: 1.5rem; - margin: 1.5rem 0; - text-align: center; -} - -.login-section h3 { - color: var(--who-text-primary, #333); - margin-bottom: 1rem; -} - -.login-section p { - color: var(--who-text-secondary, #586069); - margin-bottom: 1rem; -} - -.authenticated-section { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - flex-wrap: wrap; -} - -.authenticated-section p { - color: #28a745; - font-weight: 500; - margin: 0; -} - -.logout-btn { - background: #dc3545; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.logout-btn:hover { - background: #c82333; -} - -/* PR Filter section */ -.pr-filter-section { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.pr-filter-section label { - font-weight: 500; - color: var(--who-text-primary, #333); -} - -.filter-select { - padding: 0.5rem; - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 6px; - background: var(--who-card-bg, #ffffff); - color: var(--who-text-primary, #333); - cursor: pointer; -} - -.filter-select:focus { - outline: none; - border-color: var(--who-blue, #0366d6); - box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1); -} - -/* Comments section */ -.pr-comments-section { - margin-top: 1.5rem; - padding-top: 1rem; - border-top: 1px solid var(--who-border-color, #e1e5e9); -} - -.pr-comments-section h4 { - margin: 0 0 1rem 0; - color: var(--who-text-primary, #333); - font-size: 1rem; -} - -.comments-loading { - color: var(--who-text-secondary, #586069); - font-style: italic; - padding: 0.5rem; -} - -.comments-list { - margin-bottom: 1rem; -} - -.comment-item { - background: var(--who-secondary-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - padding: 0.75rem; - margin-bottom: 0.5rem; -} - -.comment-header { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.5rem; -} - -.comment-avatar { - width: 20px; - height: 20px; - border-radius: 50%; -} - -.comment-author { - font-weight: 600; - color: var(--who-blue, #0366d6); - font-size: 0.9rem; -} - -.comment-date { - color: var(--who-text-secondary, #586069); - font-size: 0.8rem; - margin-left: auto; -} - -.comment-body { - color: var(--who-text-primary, #333); - font-size: 0.9rem; - line-height: 1.4; - white-space: pre-wrap; - word-break: break-word; -} - -.no-comments { - color: var(--who-text-secondary, #586069); - font-style: italic; - padding: 0.5rem; - text-align: center; -} - -.comment-input-section { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.comment-input { - width: 100%; - padding: 0.75rem; - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 6px; - font-family: inherit; - font-size: 0.9rem; - resize: vertical; - min-height: 60px; -} - -.comment-input:focus { - outline: none; - border-color: var(--who-blue, #0366d6); - box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1); -} - -.submit-comment-btn { - align-self: flex-start; - background: var(--who-blue, #0366d6); - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: background-color 0.2s ease; -} - -.submit-comment-btn:hover:not(:disabled) { - background: #0256cc; -} - -.submit-comment-btn:disabled { - background: var(--who-text-muted, #6a737d); - cursor: not-allowed; -} - -.comment-error-message { - background: #fff5f5; - border: 1px solid #fed7d7; - border-radius: 6px; - padding: 0.75rem; - color: #c53030; - font-size: 0.9rem; - margin: 0.25rem 0; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.comment-auth-message { - background: var(--who-tertiary-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - padding: 1rem; - margin-bottom: 1rem; - text-align: center; -} - -.comment-auth-message p { - margin: 0; - color: var(--who-text-secondary, #586069); -} - -.comment-auth-message a { - color: var(--who-blue, #0366d6); - text-decoration: none; - font-weight: 500; -} - -.comment-auth-message a:hover { - text-decoration: underline; -} - -/* Main action buttons */ -.main-actions { - display: flex; - justify-content: center; - gap: 1rem; - margin-bottom: 2rem; - flex-wrap: wrap; -} - -.contribute-btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1.5rem; - border-radius: 8px; - font-weight: 500; - text-decoration: none; - border: none; - cursor: pointer; - transition: all 0.2s ease; - font-size: 1rem; -} - -.contribute-btn.primary { - background: #28a745; - color: white; -} - -.contribute-btn.primary:hover { - background: #218838; - color: white; - text-decoration: none; -} - -.contribute-btn.secondary { - background: #0366d6; - color: white; -} - -.contribute-btn.secondary:hover { - background: #0256cc; - color: white; - text-decoration: none; -} - -.contribute-btn.tertiary { - background: #6f42c1; - color: white; -} - -.contribute-btn.tertiary:hover { - background: #5a32a3; - color: white; - text-decoration: none; -} - -/* Top section with two cards */ -.top-section { - display: grid; - grid-template-columns: 2fr 1fr; - gap: 2rem; - margin-bottom: 2rem; -} - -.mascot-card, .main-branch-card { - background: var(--who-card-bg, #ffffff); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 12px; - padding: 2rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; -} - -.mascot-card:hover, .main-branch-card:hover { - transform: translateY(-2px); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); -} - -.mascot-content { - display: flex; - align-items: flex-start; - gap: 2rem; -} - -.large-mascot { - width: 150px; - height: 150px; - object-fit: contain; - flex-shrink: 0; -} - -.explainer-content h2, .main-branch-content h2 { - color: var(--who-blue, #0366d6); - margin: 0 0 1rem 0; - font-size: 1.5rem; -} - -.explainer-content p { - color: var(--who-text-secondary, #586069); - line-height: 1.6; - margin-bottom: 1rem; -} - -.source-code-link { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: var(--who-blue, #0366d6); - text-decoration: none; - font-weight: 500; - padding: 0.5rem 1rem; - border: 1px solid var(--who-blue, #0366d6); - border-radius: 6px; - transition: all 0.2s ease; -} - -.source-code-link:hover { - background: var(--who-blue, #0366d6); - color: white; - text-decoration: none; -} - -.main-branch-content { - text-align: center; -} - -.main-branch-content p { - color: var(--who-text-secondary, #586069); - line-height: 1.6; - margin-bottom: 1.5rem; -} - -.main-branch-actions { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.main-branch-link { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - background: var(--who-blue, #0366d6); - color: white; - text-decoration: none; - padding: 1rem 1.5rem; - border-radius: 8px; - font-weight: 500; - font-size: 1.1rem; - transition: background-color 0.2s ease; -} - -.main-branch-link:hover { - background: #0256cc; - color: white; - text-decoration: none; -} - -.docs-link { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - color: var(--who-blue, #0366d6); - text-decoration: none; - padding: 0.75rem 1rem; - border: 1px solid var(--who-blue, #0366d6); - border-radius: 6px; - font-weight: 500; - transition: all 0.2s ease; -} - -.docs-link:hover { - background: var(--who-blue, #0366d6); - color: white; - text-decoration: none; -} - -/* PR section header */ -.pr-section-header { - text-align: center; - margin: 3rem 0 2rem 0; - padding: 2rem; - background: var(--who-secondary-bg, #f8f9fa); - border-radius: 12px; - border: 1px solid var(--who-border-color, #e1e5e9); -} - -.pr-header-content { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 1rem; -} - -.pr-header-text { - flex: 1; - text-align: left; -} - -.pr-header-actions { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 0.5rem; -} - -.refresh-btn { - background: var(--who-blue, #0366d6); - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 6px; - cursor: pointer; - font-size: 0.9rem; - font-weight: 500; - transition: background-color 0.2s ease; - min-width: 120px; -} - -.refresh-btn:hover:not(:disabled) { - background: var(--who-blue-dark, #0256cc); -} - -.refresh-btn:disabled { - background: #6c757d; - cursor: not-allowed; -} - -.cache-info { - font-size: 0.8rem; - color: var(--who-text-secondary, #586069); -} - -@media (max-width: 768px) { - .pr-header-content { - flex-direction: column; - text-align: center; - } - - .pr-header-text { - text-align: center; - } - - .pr-header-actions { - align-items: center; - } -} - -.pr-section-header h2 { - color: var(--who-blue, #0366d6); - margin: 0 0 0.5rem 0; - font-size: 2rem; -} - -.pr-section-header p { - color: var(--who-text-secondary, #586069); - font-size: 1.1rem; - margin: 0; -} - -/* Responsive adjustments for top section */ -@media (max-width: 768px) { - .top-section { - grid-template-columns: 1fr; - gap: 1rem; - } - - .mascot-content { - flex-direction: column; - text-align: center; - gap: 1rem; - } - - .large-mascot { - width: 120px; - height: 120px; - align-self: center; - } - - .main-branch-actions { - gap: 0.75rem; - } - - .pr-section-header { - padding: 1.5rem; - } - - .pr-section-header h2 { - font-size: 1.5rem; - } -} - -/* Cards styling - unified for PRs */ -.pr-cards { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); - gap: 1.5rem; - margin-bottom: 3rem; -} - -/* Branch and PR section specific styles */ -.pr-section { - margin-bottom: 3rem; -} - -.pr-controls { - margin-bottom: 2rem; - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; - flex-wrap: wrap; -} - -.pr-search { - width: 100%; - max-width: 500px; - padding: 0.75rem 1rem; - border: 1px solid #e1e5e9; - border-radius: 8px; - font-size: 1rem; - background: #ffffff; -} - -.pr-search:focus { - outline: none; - border-color: #0366d6; - box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1); -} - -.sort-select { - padding: 0.75rem 1rem; - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - font-size: 1rem; - background: var(--who-card-bg, #ffffff); - color: var(--who-text-primary, #333); - cursor: pointer; - min-width: 200px; -} - -.sort-select:focus { - outline: none; - border-color: var(--who-blue, #0366d6); - box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1); -} - -.status-checking { - color: #0366d6; - font-size: 0.9rem; - font-weight: 500; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.preview-card { - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 12px; - padding: 1.5rem; - background: var(--who-card-bg, #ffffff); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - transition: all 0.3s ease; - position: relative; -} - -.preview-card:hover { - transform: translateY(-4px); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); - border-color: var(--who-blue, #0366d6); -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 1rem; - gap: 1rem; -} - -.card-badges { - display: flex; - flex-direction: column; - gap: 0.5rem; - align-items: flex-end; - flex-shrink: 0; -} - -.item-name { - margin: 0; - font-size: 1.4rem; - color: var(--who-blue, #0366d6); - font-weight: 600; - word-break: break-word; - flex: 1; -} - -.commit-badge, .state-badge, .status-badge { - background: var(--who-hover-bg, #f1f3f4); - color: var(--who-text-secondary, #586069); - padding: 0.25rem 0.5rem; - border-radius: 6px; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; - font-size: 0.875rem; - font-weight: 500; - flex-shrink: 0; - white-space: nowrap; -} - -.status-badge.active { - background: #dcfce7; - color: #166534; -} - -.status-badge.not-found { - background: #fef3c7; - color: #92400e; -} - -.status-badge.errored { - background: #fee2e2; - color: #991b1b; -} - -.state-badge.open { - background: #dcfce7; - color: #166534; -} - -.state-badge.closed { - background: #fee2e2; - color: #991b1b; -} - -.card-body { - margin-bottom: 1rem; -} - -.item-date, .pr-meta { - color: var(--who-text-secondary, #586069); - font-size: 0.9rem; - margin: 0 0 1rem 0; -} - -.pr-actions { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.pr-top-actions { - display: flex; - gap: 0.5rem; - margin: 0.75rem 0; - padding: 0.5rem 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); -} - -.view-files-btn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: #60a5fa; - text-decoration: none; - padding: 0.5rem 1rem; - border-radius: 6px; - font-weight: 500; - font-size: 0.875rem; - background: rgba(96, 165, 250, 0.1); - border: 1px solid rgba(96, 165, 250, 0.3); - transition: all 0.2s ease; -} - -.view-files-btn:hover { - background: rgba(96, 165, 250, 0.2); - border-color: rgba(96, 165, 250, 0.5); - color: #93c5fd; - text-decoration: none; - transform: translateY(-1px); -} - -.preview-link, .pr-link, .copy-btn, .actions-link { - display: inline-flex; - align-items: center; - gap: 0.5rem; - color: white; - text-decoration: none; - padding: 0.75rem 1.5rem; - border-radius: 8px; - font-weight: 500; - transition: background-color 0.2s ease; - font-size: 0.9rem; - border: none; - cursor: pointer; -} - -.preview-link { - background: #0366d6; -} - -.preview-link:hover { - background: #0256cc; - text-decoration: none; - color: white; -} - -.copy-btn { - background: #28a745; -} - -.copy-btn:hover { - background: #218838; - color: white; -} - -.pr-link { - background: #6f42c1; -} - -.pr-link:hover { - background: #5a32a3; -} - -.actions-link { - background: #dc3545; - font-size: 0.8rem; - padding: 0.5rem 1rem; -} - -.actions-link:hover { - background: #c82333; - color: white; - text-decoration: none; -} - -.deployment-message { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding: 0.75rem; - border-radius: 8px; - font-size: 0.9rem; -} - -.building-message { - color: #92400e; - background: #fef3c7; - padding: 0.5rem; - border-radius: 6px; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.error-message { - color: #991b1b; - background: #fee2e2; - padding: 0.5rem; - border-radius: 6px; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.card-footer { - border-top: 1px solid #e1e5e9; - padding-top: 1rem; - margin-top: 1rem; -} - -.preview-path { - color: #6a737d; - font-size: 0.8rem; - word-break: break-all; -} - -.preview-url-link { - color: #0366d6; - text-decoration: none; - font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; -} - -.preview-url-link:hover { - text-decoration: underline; -} - -/* PR section specific styles moved to branch-section, pr-section above */ - -.pr-card { - border-left: 4px solid #0366d6; -} - -.pr-card .item-name { - font-size: 1.2rem; - line-height: 1.4; -} - -/* Pagination */ -.pagination { - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; - margin-top: 2rem; -} - -.pagination-btn { - background: var(--who-hover-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - color: var(--who-blue, #0366d6); - padding: 0.5rem 1rem; - border-radius: 6px; - cursor: pointer; - font-weight: 500; - transition: all 0.2s ease; -} - -.pagination-btn:hover:not(:disabled) { - background: var(--who-selected-bg, #e1e7fd); - border-color: var(--who-blue, #0366d6); -} - -.pagination-btn:disabled { - color: var(--who-text-muted, #6a737d); - cursor: not-allowed; - opacity: 0.6; -} - -.pagination-info { - color: var(--who-text-secondary, #586069); - font-size: 0.9rem; -} - -.no-items { - grid-column: 1 / -1; - text-align: center; - padding: 3rem; - color: #6a737d; -} - -.no-items p { - margin: 0.5rem 0; -} - -.loading, .error { - text-align: center; - padding: 3rem; - color: #6a737d; -} - -.error { - color: #d73a49; -} - -/* Footer */ -.branch-listing-footer { - border-top: 1px solid var(--who-border-color, #e1e5e9); - padding: 2rem 0; - margin-top: 3rem; -} - -.footer-content { - display: flex; - justify-content: space-between; - align-items: center; - gap: 2rem; -} - -.footer-left { - flex-shrink: 0; -} - -.footer-center { - text-align: center; - color: var(--who-text-secondary, #6a737d); -} - -.footer-center p { - margin: 0.5rem 0; -} - -.source-link, .footer-center a { - color: var(--who-blue, #0366d6); - text-decoration: none; - font-weight: 500; - display: inline-flex; - align-items: center; - gap: 0.5rem; -} - -.source-link:hover, .footer-center a:hover { - text-decoration: underline; -} - -/* Discussion Summary Status Bar */ -.discussion-summary-bar { - background: var(--who-secondary-bg, #f8f9fa); - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - padding: 0.75rem 1rem; - margin-top: 1rem; - cursor: pointer; - transition: all 0.2s ease; - display: flex; - justify-content: space-between; - align-items: center; -} - -.discussion-summary-bar:hover { - background: var(--who-hover-bg, #f1f3f4); - border-color: var(--who-blue, #0366d6); -} - -.discussion-summary-text { - color: var(--who-text-primary, #333); - font-size: 0.9rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.discussion-summary-icon { - color: var(--who-blue, #0366d6); - font-weight: bold; -} - -.discussion-expand-icon { - color: var(--who-text-secondary, #586069); - transition: transform 0.2s ease; -} - -.discussion-expand-icon.expanded { - transform: rotate(90deg); -} - -/* Expanded Discussion Section */ -.discussion-expanded-section { - margin-top: 0.5rem; - border: 1px solid var(--who-border-color, #e1e5e9); - border-radius: 8px; - background: var(--who-card-bg, #ffffff); - overflow: hidden; -} - -.discussion-header { - background: var(--who-secondary-bg, #f8f9fa); - padding: 1rem; - border-bottom: 1px solid var(--who-border-color, #e1e5e9); - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 0.5rem; -} - -.discussion-title { - font-weight: 600; - color: var(--who-text-primary, #333); - margin: 0; -} - -.discussion-actions { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; -} - -.discussion-action-btn { - background: var(--who-blue, #0366d6); - color: white; - border: none; - padding: 0.375rem 0.75rem; - border-radius: 4px; - font-size: 0.8rem; - text-decoration: none; - cursor: pointer; - transition: background-color 0.2s ease; - display: inline-flex; - align-items: center; - gap: 0.25rem; -} - -.discussion-action-btn:hover { - background: #0256cc; - color: white; - text-decoration: none; -} - -.discussion-action-btn.secondary { - background: #6f42c1; -} - -.discussion-action-btn.secondary:hover { - background: #5a32a3; -} - -.comment-input-section { - padding: 1rem; - border-bottom: 1px solid var(--who-border-color, #e1e5e9); - background: var(--who-tertiary-bg, #f8f9fa); -} - -.discussion-scroll-area { - max-height: 400px; - overflow-y: auto; - padding: 1rem; -} - -/* Contribute modal styles */ -.contribute-slide { - text-align: center; - padding: 1rem; -} - -.mascot-container { - margin-bottom: 1.5rem; - position: relative; - display: flex; - justify-content: center; - align-items: center; -} - -.contribute-mascot { - width: 120px; - height: 120px; - object-fit: contain; - margin: 0 0.5rem; -} - -.contribute-mascot.bug-report { - filter: sepia(1) hue-rotate(320deg) saturate(2); -} - -.contribute-mascot.coding-agent { - filter: sepia(1) hue-rotate(180deg) saturate(1.5); -} - -.contribute-mascot.community { - width: 80px; - height: 80px; -} - -.contribute-mascot.celebrate { - animation: bounce 2s infinite; -} - -@keyframes bounce { - 0%, 20%, 50%, 80%, 100% { - transform: translateY(0); - } - 40% { - transform: translateY(-10px); - } - 60% { - transform: translateY(-5px); - } -} - -.mascot-group { - display: flex; - justify-content: center; - gap: 1rem; - margin-bottom: 1rem; -} - -.thought-bubble { - position: absolute; - top: -20px; - right: 20%; - background: white; - border: 2px solid #0366d6; - border-radius: 50%; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.5rem; - animation: float 3s ease-in-out infinite; -} - -@keyframes float { - 0%, 100% { - transform: translateY(0px); - } - 50% { - transform: translateY(-10px); - } -} - -.action-buttons { - display: flex; - flex-direction: column; - gap: 1rem; - margin: 2rem 0; - align-items: center; -} - -.contribute-footer { - background: #f8f9fa; - padding: 1.5rem; - border-radius: 8px; - border-left: 4px solid #28a745; - margin-top: 2rem; -} - -/* Responsive design */ -@media (max-width: 768px) { - .branch-listing { - padding: 1rem; - } - - .pr-cards { - grid-template-columns: 1fr; - gap: 1rem; - } - - .preview-card { - padding: 1rem; - } - - .card-header { - flex-direction: column; - gap: 0.5rem; - align-items: flex-start; - } - - .card-badges { - flex-direction: row; - align-items: flex-start; - gap: 0.5rem; - } - - .main-actions { - flex-direction: column; - align-items: center; - } - - .contribute-btn { - width: 100%; - max-width: 300px; - justify-content: center; - } - - .pr-actions { - justify-content: center; - } - - .pr-controls { - flex-direction: column; - align-items: stretch; - } - - .pr-filter-section { - justify-content: center; - } - - .authenticated-section { - flex-direction: column; - gap: 0.5rem; - } - - .copy-btn, .actions-link { - width: 100%; - justify-content: center; - } - - .pagination { - flex-direction: column; - gap: 0.5rem; - } - - .mascot-group { - flex-direction: column; - align-items: center; - } - - .action-buttons { - width: 100%; - } - - .contribute-btn { - width: 100%; - max-width: none; - } -} \ No newline at end of file diff --git a/src/components/BranchListing.js b/src/components/BranchListing.js deleted file mode 100644 index 57e31bd09..000000000 --- a/src/components/BranchListing.js +++ /dev/null @@ -1,1142 +0,0 @@ -import React, { useState, useEffect, useCallback } from 'react'; -import { PageLayout } from './framework'; -import HelpModal from './HelpModal'; -import PATLogin from './PATLogin'; -import WorkflowStatus from './WorkflowStatus'; -import githubActionsService from '../services/githubActionsService'; -import branchListingCacheService from '../services/branchListingCacheService'; -import useThemeImage from '../hooks/useThemeImage'; -import './BranchListing.css'; - -const BranchListing = () => { - const [pullRequests, setPullRequests] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [prPage, setPrPage] = useState(1); - const [prSearchTerm, setPrSearchTerm] = useState(''); - - const [prSortBy, setPrSortBy] = useState('updated'); // updated, number, alphabetical - const [showContributeModal, setShowContributeModal] = useState(false); - const [deploymentStatuses, setDeploymentStatuses] = useState({}); - const [prFilter, setPrFilter] = useState('open'); // 'open', 'closed', 'all' - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [githubToken, setGithubToken] = useState(null); - const [prComments, setPrComments] = useState({}); - const [commentInputs, setCommentInputs] = useState({}); - const [submittingComments, setSubmittingComments] = useState({}); - const [expandedDiscussions, setExpandedDiscussions] = useState({}); - const [discussionSummaries, setDiscussionSummaries] = useState({}); - const [loadingSummaries, setLoadingSummaries] = useState(false); - const [workflowStatuses, setWorkflowStatuses] = useState({}); - const [loadingWorkflowStatuses, setLoadingWorkflowStatuses] = useState(false); - const [isRefreshing, setIsRefreshing] = useState(false); - const [cacheInfo, setCacheInfo] = useState(null); - - // Theme-aware mascot image - const mascotImage = useThemeImage('sgex-mascot.png'); - - const ITEMS_PER_PAGE = 10; - const GITHUB_OWNER = 'litlfred'; - const GITHUB_REPO = 'sgex'; - - // Function to manually refresh cache and reload data - const handleManualRefresh = useCallback(async () => { - setIsRefreshing(true); - - // Clear the cache to force fresh data - branchListingCacheService.forceRefresh(GITHUB_OWNER, GITHUB_REPO); - - // The fetchData function will be called by the useEffect when isRefreshing changes - }, []); - - // GitHub authentication functions - const handleAuthSuccess = (token, octokitInstance) => { - setGithubToken(token); - setIsAuthenticated(true); - // Store token for session - sessionStorage.setItem('github_token', token); - // Set token for GitHub Actions service - githubActionsService.setToken(token); - }; - - const handleLogout = () => { - setGithubToken(null); - setIsAuthenticated(false); - sessionStorage.removeItem('github_token'); - // Clear token from GitHub Actions service - githubActionsService.setToken(null); - }; - - // Function to fetch PR comments summary - const fetchPRCommentsSummary = useCallback(async (prNumber) => { - // Allow fetching comments even without authentication for read-only access - - try { - const headers = { - 'Accept': 'application/vnd.github.v3+json' - }; - - // Add authorization header only if token is available - if (githubToken) { - headers['Authorization'] = `token ${githubToken}`; - } - - const response = await fetch( - `https://api.github.com/repos/litlfred/sgex/issues/${prNumber}/comments`, - { headers } - ); - - if (!response.ok) { - throw new Error(`Failed to fetch comments: ${response.status}`); - } - - const comments = await response.json(); - if (comments.length === 0) { - return { count: 0, lastComment: null }; - } - - const lastComment = comments[comments.length - 1]; - return { - count: comments.length, - lastComment: { - author: lastComment.user.login, - created_at: new Date(lastComment.created_at), - avatar_url: lastComment.user.avatar_url - } - }; - } catch (error) { - console.error(`Error fetching comment summary for PR ${prNumber}:`, error); - return null; - } - }, [githubToken]); - - // Function to fetch all PR comments (for expanded view) - const fetchAllPRComments = useCallback(async (prNumber) => { - // Allow fetching comments even without authentication for read-only access - - try { - const headers = { - 'Accept': 'application/vnd.github.v3+json' - }; - - // Add authorization header only if token is available - if (githubToken) { - headers['Authorization'] = `token ${githubToken}`; - } - - const response = await fetch( - `https://api.github.com/repos/litlfred/sgex/issues/${prNumber}/comments`, - { headers } - ); - - if (!response.ok) { - throw new Error(`Failed to fetch comments: ${response.status}`); - } - - const comments = await response.json(); - return comments.map(comment => ({ - id: comment.id, - author: comment.user.login, - body: comment.body, - created_at: new Date(comment.created_at).toLocaleDateString(), - avatar_url: comment.user.avatar_url - })); - } catch (error) { - console.error(`Error fetching all comments for PR ${prNumber}:`, error); - return []; - } - }, [githubToken]); - - // Function to load discussion summaries for visible PRs - const loadDiscussionSummaries = useCallback(async (prs) => { - if (prs.length === 0) return; - - setLoadingSummaries(true); - const summaries = {}; - - for (const pr of prs) { - summaries[pr.number] = await fetchPRCommentsSummary(pr.number); - } - - setDiscussionSummaries(summaries); - setLoadingSummaries(false); - }, [fetchPRCommentsSummary]); - - // Function to toggle discussion expansion - const toggleDiscussion = async (prNumber) => { - const isExpanded = expandedDiscussions[prNumber]; - - if (!isExpanded) { - // Load all comments when expanding - const comments = await fetchAllPRComments(prNumber); - setPrComments(prev => ({ ...prev, [prNumber]: comments })); - } - - setExpandedDiscussions(prev => ({ - ...prev, - [prNumber]: !isExpanded - })); - }; - - // Function to get discussion summary text - const getDiscussionSummaryText = (prNumber) => { - const summary = discussionSummaries[prNumber]; - - if (loadingSummaries) { - return "Loading discussion..."; - } - - if (!summary || summary.count === 0) { - return "No comments yet"; - } - - const { count, lastComment } = summary; - const timeAgo = lastComment ? getTimeAgo(lastComment.created_at) : ''; - - return `${count} comment${count > 1 ? 's' : ''}, last by ${lastComment.author} ${timeAgo}`; - }; - - // Helper function to get relative time - const getTimeAgo = (date) => { - const now = new Date(); - const diffMs = now - date; - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) return 'today'; - if (diffDays === 1) return '1 day ago'; - if (diffDays < 7) return `${diffDays} days ago`; - if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`; - return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`; - }; - - // Function to submit a comment - const submitComment = async (prNumber, commentText) => { - if (!githubToken || !commentText.trim()) return false; - - setSubmittingComments(prev => ({ ...prev, [prNumber]: true })); - - try { - const response = await fetch( - `https://api.github.com/repos/litlfred/sgex/issues/${prNumber}/comments`, - { - method: 'POST', - headers: { - 'Authorization': `token ${githubToken}`, - 'Accept': 'application/vnd.github.v3+json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - body: commentText - }) - } - ); - - if (!response.ok) { - throw new Error(`Failed to submit comment: ${response.status}`); - } - - // Clear the comment input - setCommentInputs(prev => ({ ...prev, [prNumber]: '' })); - - // Refresh both full comments (if expanded) and summary - if (expandedDiscussions[prNumber]) { - const updatedComments = await fetchAllPRComments(prNumber); - setPrComments(prev => ({ ...prev, [prNumber]: updatedComments })); - } - - // Refresh the discussion summary - const updatedSummary = await fetchPRCommentsSummary(prNumber); - setDiscussionSummaries(prev => ({ ...prev, [prNumber]: updatedSummary })); - - return true; - } catch (error) { - console.error(`Error submitting comment for PR ${prNumber}:`, error); - return false; - } finally { - setSubmittingComments(prev => ({ ...prev, [prNumber]: false })); - } - }; - - // Function to load workflow statuses for PR branches - const loadWorkflowStatuses = useCallback(async (prData) => { - if (!githubToken) return; - - setLoadingWorkflowStatuses(true); - - try { - // Get all PR branch names - const branchNames = prData.map(pr => pr.branchName); - - const uniqueBranchNames = [...new Set(branchNames)]; - const statuses = await githubActionsService.getWorkflowStatusForBranches(uniqueBranchNames); - - setWorkflowStatuses(statuses); - } catch (error) { - console.error('Error loading workflow statuses:', error); - } finally { - setLoadingWorkflowStatuses(false); - } - }, [githubToken]); - - // Function to trigger workflow for a branch - const triggerWorkflow = useCallback(async (branchName) => { - if (!githubToken) { - alert('Please authenticate to trigger workflows'); - return; - } - - try { - const success = await githubActionsService.triggerWorkflow(branchName); - if (success) { - alert(`Workflow triggered for branch: ${branchName}`); - // Refresh workflow statuses after a short delay - setTimeout(() => { - const currentPRs = pullRequests.length > 0 ? pullRequests : []; - loadWorkflowStatuses(currentPRs); - }, 2000); - } else { - alert(`Failed to trigger workflow for branch: ${branchName}`); - } - } catch (error) { - console.error('Error triggering workflow:', error); - alert(`Error triggering workflow: ${error.message}`); - } - }, [githubToken, pullRequests, loadWorkflowStatuses]); - - // Check for existing authentication on component mount - useEffect(() => { - const token = sessionStorage.getItem('github_token'); - if (token) { - setGithubToken(token); - setIsAuthenticated(true); - // Set token for GitHub Actions service - githubActionsService.setToken(token); - } - }, []); - - // Function to check deployment status - const checkDeploymentStatus = async (url) => { - try { - const response = await fetch(url, { method: 'HEAD' }); - if (response.ok) { - return 'active'; - } else if (response.status === 404) { - return 'not-found'; - } else { - return 'errored'; - } - } catch (error) { - return 'errored'; - } - }; - - // Function to check deployment statuses for PRs only - const checkAllDeploymentStatuses = useCallback(async (prData) => { - const statuses = {}; - - // Check PRs only - for (const pr of prData) { - const status = await checkDeploymentStatus(pr.url); - statuses[`pr-${pr.id}`] = status; - } - - setDeploymentStatuses(statuses); - }, []); - - // Function to copy URL to clipboard - const copyToClipboard = async (url, type, name) => { - try { - await navigator.clipboard.writeText(url); - // You could add a toast notification here - console.log(`Copied ${type} URL for ${name} to clipboard`); - } catch (error) { - // Fallback for browsers that don't support clipboard API - const textArea = document.createElement('textarea'); - textArea.value = url; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); - console.log(`Copied ${type} URL for ${name} to clipboard (fallback)`); - } - }; - - // Sorting function for PRs - const sortPRs = (prs, sortBy) => { - return [...prs].sort((a, b) => { - switch (sortBy) { - case 'number': - return b.number - a.number; // Highest number first - case 'alphabetical': - return a.title.localeCompare(b.title); - case 'updated': - default: - const dateA = new Date(a.updatedAt); - const dateB = new Date(b.updatedAt); - return dateB - dateA; // Most recent first - } - }); - }; - - // "How to contribute" slideshow content - const contributeHelpTopic = { - id: 'how-to-contribute', - title: 'How to Contribute to SGEX', - type: 'slideshow', - content: [ - { - title: 'Welcome to SGEX - A Collaborative Workbench', - content: ` -
-
- SGEX Mascot -
-

What is SGEX?

-

SGEX is an experimental collaborative project developing a workbench of tools to make it easier and faster to develop high fidelity SMART Guidelines Digital Adaptation Kits.

-

Our mission is to empower healthcare organizations worldwide to create and maintain standards-compliant digital health implementations.

-
- ` - }, - { - title: 'Step 1: Report a Bug or Make a Feature Request', - content: ` -
-
- SGEX Mascot examining a bug -
-

🐛 Found something that needs fixing?

-

Every great contribution starts with identifying what can be improved:

-
    -
  • Bug reports: Help us identify and fix issues
  • -
  • Feature requests: Share ideas for new functionality
  • -
  • Documentation improvements: Make our guides clearer
  • -
  • User experience feedback: Tell us what's confusing
  • -
-

Click the mascot's help button on any page to quickly report issues!

-
- ` - }, - { - title: 'Step 2: Assignment to a Coding Agent', - content: ` -
-
- Robotic SGEX Mascot -
-

🤖 AI-Powered Development

-

Once your issue is triaged, it may be assigned to one of our coding agents:

-
    -
  • Automated analysis: AI agents analyze the requirements
  • -
  • Code generation: Initial implementations are created
  • -
  • Testing integration: Automated tests validate changes
  • -
  • Documentation updates: Keep documentation in sync
  • -
-

This hybrid approach combines human insight with AI efficiency.

-
- ` - }, - { - title: 'Step 3: Community Collaboration', - content: ` -
-
-
- SGEX Mascot 1 - SGEX Mascot 2 - SGEX Mascot 3 -
-
đŸ’Ģ
-
-

🌟 Real-time Evolution

-

The community drives continuous improvement through collaborative discussion:

-
    -
  • Code reviews: Community members review and suggest improvements
  • -
  • Testing feedback: Real-world testing by healthcare professionals
  • -
  • Knowledge sharing: Best practices emerge from collective experience
  • -
  • Iterative refinement: The workbench evolves based on actual usage
  • -
-

Together, we're building the future of digital health tooling!

-
- ` - }, - { - title: 'Get Started Today!', - content: ` -
-
- SGEX Mascot celebrating -
-

🚀 Ready to Contribute?

- - -
- ` - } - ] - }; - - useEffect(() => { - const fetchData = async () => { - try { - setLoading(true); - - // First, try to get cached data - const cachedData = branchListingCacheService.getCachedData(GITHUB_OWNER, GITHUB_REPO); - - if (cachedData && !isRefreshing) { - console.log('Using cached data for PR listing'); - - // Filter PRs based on current filter state - const filteredCachedPRs = cachedData.pullRequests.filter(pr => { - if (prFilter === 'all') return true; - return pr.state === prFilter; - }); - - setPullRequests(filteredCachedPRs); - setCacheInfo(branchListingCacheService.getCacheInfo(GITHUB_OWNER, GITHUB_REPO)); - - // Still need to check deployment statuses as these change frequently - await checkAllDeploymentStatuses(filteredCachedPRs); - - // Load workflow statuses if authenticated - if (githubToken) { - await loadWorkflowStatuses(filteredCachedPRs); - } - - // Load discussion summaries for first page - await loadDiscussionSummaries(filteredCachedPRs.slice(0, ITEMS_PER_PAGE)); - return; - } - - // If no cached data or refreshing, fetch fresh data - console.log('Fetching fresh data from GitHub API'); - - // Fetch pull requests based on filter - const prState = prFilter === 'all' ? 'all' : prFilter; - console.log(`Fetching PRs with state: ${prState} from GitHub API`); - - const prResponse = await fetch(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/pulls?state=${prState}&sort=updated&per_page=100`); - if (!prResponse.ok) { - const errorText = await prResponse.text(); - console.error(`GitHub API error: ${prResponse.status} - ${errorText}`); - throw new Error(`Failed to fetch pull requests: ${prResponse.status} - ${prResponse.statusText}`); - } - const prData = await prResponse.json(); - console.log(`Fetched ${prData.length} PRs from GitHub API`); - - // Format PR data - const formattedPRs = prData.map(pr => { - const safeBranchName = pr.head.ref.replace(/\//g, '-'); - return { - id: pr.id, - number: pr.number, - title: pr.title, - state: pr.state, - author: pr.user.login, - branchName: pr.head.ref, - safeBranchName: safeBranchName, - url: `./${safeBranchName}/`, - prUrl: pr.html_url, - updatedAt: new Date(pr.updated_at).toLocaleDateString(), - createdAt: new Date(pr.created_at).toLocaleDateString() - }; - }); - - // Cache the fresh data (we only cache PRs since branches were removed) - branchListingCacheService.setCachedData(GITHUB_OWNER, GITHUB_REPO, [], formattedPRs); - setCacheInfo(branchListingCacheService.getCacheInfo(GITHUB_OWNER, GITHUB_REPO)); - - setPullRequests(formattedPRs); - - // Check deployment statuses for PRs only - await checkAllDeploymentStatuses(formattedPRs); - - // Load workflow statuses if authenticated - if (githubToken) { - await loadWorkflowStatuses(formattedPRs); - } - - // Load discussion summaries for PRs - available for all users - await loadDiscussionSummaries(formattedPRs.slice(0, ITEMS_PER_PAGE)); - } catch (err) { - console.error('Error fetching data:', err); - setError(err.message); - - // Check if this is a network/CORS issue - if (err.message.includes('Failed to fetch') || err.message.includes('CORS')) { - console.log('Network/CORS error detected, checking if we can use cache or fallback data...'); - - // Try to use any existing cached data even if stale - const cachedDataRaw = localStorage.getItem(branchListingCacheService.getCacheKey(GITHUB_OWNER, GITHUB_REPO)); - if (cachedDataRaw) { - try { - const parsed = JSON.parse(cachedDataRaw); - console.log('Using stale cached data due to network error'); - setPullRequests(parsed.pullRequests.filter(pr => { - if (prFilter === 'all') return true; - return pr.state === prFilter; - })); - setCacheInfo({ - exists: true, - stale: true, - ageMinutes: Math.round((Date.now() - parsed.timestamp) / (60 * 1000)), - prCount: parsed.pullRequests?.length || 0 - }); - setError('Using cached data (network error occurred)'); - return; - } catch (parseError) { - console.error('Error parsing stale cache:', parseError); - } - } - } - - // Only use fallback data in development or when GitHub API is blocked - if (process.env.NODE_ENV === 'development' || err.message.includes('Failed to fetch')) { - console.log('Using fallback mock data for demonstration...'); - const mockPRs = [ - { - id: 1, - number: 123, - title: 'Improve multi-page selector landing page for GitHub deployment', - state: 'open', - author: 'copilot', - branchName: 'copilot/fix-459', - safeBranchName: 'copilot-fix-459', - url: './copilot-fix-459/', - prUrl: 'https://github.com/litlfred/sgex/pull/123', - updatedAt: new Date().toLocaleDateString(), - createdAt: new Date(Date.now() - 86400000).toLocaleDateString() - }, - { - id: 2, - number: 122, - title: 'Add dark mode support', - state: 'closed', - author: 'developer', - branchName: 'feature/dark-mode', - safeBranchName: 'feature-dark-mode', - url: './feature-dark-mode/', - prUrl: 'https://github.com/litlfred/sgex/pull/122', - updatedAt: new Date(Date.now() - 172800000).toLocaleDateString(), - createdAt: new Date(Date.now() - 345600000).toLocaleDateString() - }, - { - id: 3, - number: 121, - title: 'Fix authentication flow', - state: 'open', - author: 'contributor', - branchName: 'fix/auth-flow', - safeBranchName: 'fix-auth-flow', - url: './fix-auth-flow/', - prUrl: 'https://github.com/litlfred/sgex/pull/121', - updatedAt: new Date(Date.now() - 259200000).toLocaleDateString(), - createdAt: new Date(Date.now() - 432000000).toLocaleDateString() - } - ]; - - setPullRequests(mockPRs); - setError(null); // Clear error since we have fallback data - } - } finally { - setLoading(false); - setIsRefreshing(false); // Reset refresh state - } - }; - - fetchData(); - }, [checkAllDeploymentStatuses, prFilter, githubToken, loadWorkflowStatuses, loadDiscussionSummaries, isRefreshing]); - - // Load summaries for visible PRs when page changes - useEffect(() => { - if (pullRequests.length > 0) { - const filtered = pullRequests.filter(pr => - pr.title.toLowerCase().includes(prSearchTerm.toLowerCase()) || - pr.author.toLowerCase().includes(prSearchTerm.toLowerCase()) - ); - const sorted = sortPRs(filtered, prSortBy); - const paginated = sorted.slice((prPage - 1) * ITEMS_PER_PAGE, prPage * ITEMS_PER_PAGE); - loadDiscussionSummaries(paginated); - } - }, [prPage, prSearchTerm, prSortBy, pullRequests, loadDiscussionSummaries]); - - // Filter and sort PRs based on search and sorting - const filteredPRs = pullRequests.filter(pr => - pr.title.toLowerCase().includes(prSearchTerm.toLowerCase()) || - pr.author.toLowerCase().includes(prSearchTerm.toLowerCase()) || - pr.branchName.toLowerCase().includes(prSearchTerm.toLowerCase()) - ); - const sortedPRs = sortPRs(filteredPRs, prSortBy); - const paginatedPRs = sortedPRs.slice((prPage - 1) * ITEMS_PER_PAGE, prPage * ITEMS_PER_PAGE); - const totalPRPages = Math.ceil(sortedPRs.length / ITEMS_PER_PAGE); - - if (loading) { - return ( - -
-

SGEX Icon SGEX

-

a collaborative workbench for WHO SMART Guidelines

-
Loading previews...
-
-
- ); - } - - if (error) { - return ( - -
-

SGEX Icon SGEX

-

a collaborative workbench for WHO SMART Guidelines

-
-

Failed to load previews: {error}

-

Please try refreshing the page or check back later.

-
-
-
- ); - } - - return ( - -
- {/* Top Section: Two cards side by side */} -
- {/* Mascot and Explainer Card */} -
-
- SGEX Mascot -
-

SGEX Deployment Selection

-

Welcome to the SGEX deployment selection page. Here you can browse and access all available pull request previews for the WHO SMART Guidelines Exchange collaborative workbench.

-

Each pull request is automatically deployed to its own preview environment for testing and collaboration.

- - đŸ“Ļ View Source Code - -
-
-
- - {/* Main Branch Access Card */} -
-
-

🚀 Main Branch

-

Access the stable main branch of the SGEX workbench with the latest published features.

- -
-
-
- - {/* Authentication Section */} -
- {!isAuthenticated ? ( -
-

🔐 GitHub Authentication

-

Login with your GitHub Personal Access Token to view and add comments to pull requests:

- -
- ) : ( -
-

✅ Authenticated - You can now view and add comments to pull requests

- -
- )} -
- - {/* Main Actions */} -
- - - 🐛 Report a Bug - -
- - {/* PR Section Header */} -
-
-
-

🔄 Pull Request Previews ({sortedPRs.length})

-

Browse and test pull request changes in isolated preview environments

-
-
- - {cacheInfo && ( -
- - {cacheInfo.exists - ? `📊 Data cached (${cacheInfo.ageMinutes}m old)` - : 'Fresh data' - } - -
- )} -
-
-
- - {/* PR Section */} -
-
-
- - -
- { - setPrSearchTerm(e.target.value); - setPrPage(1); // Reset to first page when searching - }} - className="pr-search" - /> - -
- -
- {paginatedPRs.length === 0 ? ( -
- {prSearchTerm ? ( -

No pull requests match your search "{prSearchTerm}".

- ) : ( -

No pull request previews available at the moment.

- )} -
- ) : ( - paginatedPRs.map((pr) => { - const statusKey = `pr-${pr.id}`; - const deploymentStatus = deploymentStatuses[statusKey]; - - return ( -
-
-

#{pr.number}: {pr.title}

-
- - {pr.state === 'open' ? 'đŸŸĸ' : '🔴'} {pr.state} - - {deploymentStatus && ( - - {deploymentStatus === 'active' && 'đŸŸĸ Active'} - {deploymentStatus === 'not-found' && '🟡 Building'} - {deploymentStatus === 'errored' && '🔴 Error'} - - )} -
-
- -
-

- Branch: {pr.branchName} â€ĸ Author: {pr.author} -

-

- Created: {pr.createdAt} â€ĸ Updated: {pr.updatedAt} -

- - {/* PR Actions - View Files button at the top */} - - - {/* Discussion Summary Section - Show for all users */} -
- {/* Discussion Summary Status Bar */} -
toggleDiscussion(pr.number)} - > -
- đŸ’Ŧ - {getDiscussionSummaryText(pr.number)} -
- - â–ļ - -
- - {/* Expanded Discussion Section */} - {expandedDiscussions[pr.number] && ( -
-
-

Discussion

- -
- - {/* Comment Input at Top - Only show for authenticated users */} - {isAuthenticated ? ( -
-