From fddbddfdbd1f7413d01ace916b67eef250d4c957 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:12:48 -0400 Subject: [PATCH 1/7] ci(website): run support contracts and stop duplicate verify runs - ci(website): add the test:support step to verify - ci(website): skip the pull_request run for dev-headed promotion PRs, since the dev push run already reports verify on that commit - ci(website): drop the unused feature/** push trigger - ci(website): cancel superseded PR runs with a concurrency group --- .github/workflows/website.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 8fd5b40..6187bea 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -5,7 +5,6 @@ on: branches: - main - dev - - "feature/**" pull_request: branches: - main @@ -14,8 +13,14 @@ on: permissions: contents: read +concurrency: + group: website-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: verify: + # The dev push run already reports verify on the promotion PR's head commit. + if: github.event_name != 'pull_request' || github.head_ref != 'dev' runs-on: ubuntu-latest timeout-minutes: 15 defaults: @@ -40,6 +45,8 @@ jobs: run: npm run test:scene - name: Run PostHog contracts run: npm run test:posthog + - name: Run support contracts + run: npm run test:support - name: Build production app run: npm run build - name: Install browser for production checks From 30bcb355d005607af0fdcedf38a64e5255faf0ce Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:56:22 -0400 Subject: [PATCH 2/7] ci(website): stop persisting the checkout credential --- .github/workflows/website.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 6187bea..2c90246 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -29,6 +29,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@v4 with: From 25f4f742169431c88f9ab7b8ae2eefb82808d84a Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 25 Oct 2026 08:52:39 -0400 Subject: [PATCH 3/7] ci(website): verify fork branches named dev --- .github/workflows/website.yml | 6 ++-- frontend/test/workflow.test.mjs | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 frontend/test/workflow.test.mjs diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 2c90246..a3e8319 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -19,8 +19,8 @@ concurrency: jobs: verify: - # The dev push run already reports verify on the promotion PR's head commit. - if: github.event_name != 'pull_request' || github.head_ref != 'dev' + # Only this repository's dev-to-main promotion has a matching dev push run. + if: github.event_name != 'pull_request' || github.head_ref != 'dev' || github.base_ref != 'main' || github.event.pull_request.head.repo.id != github.event.repository.id runs-on: ubuntu-latest timeout-minutes: 15 defaults: @@ -43,6 +43,8 @@ jobs: run: npm run lint - name: Type-check run: npm run type-check + - name: Verify workflow event boundaries + run: node --test test/workflow.test.mjs - name: Run scene tests run: npm run test:scene - name: Run PostHog contracts diff --git a/frontend/test/workflow.test.mjs b/frontend/test/workflow.test.mjs new file mode 100644 index 0000000..6ce9b13 --- /dev/null +++ b/frontend/test/workflow.test.mjs @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { runInNewContext } from "node:vm"; + +const workflow = readFileSync( + new URL("../../.github/workflows/website.yml", import.meta.url), + "utf8", +); +const condition = workflow.match(/^ {4}if: (.+)$/m)?.[1]; +assert.ok(condition, "verify must have an explicit duplicate-run condition"); + +// This condition uses only boolean operators and same-type comparisons, shared +// by JavaScript and Actions for these lowercase string/numeric fixtures. +function runsVerify(github) { + return runInNewContext(condition, { github }, { timeout: 100 }); +} + +function pullRequest(head, base, headRepository = 1) { + return { + event_name: "pull_request", + head_ref: head, + base_ref: base, + event: { + repository: { id: 1 }, + pull_request: { head: { repo: { id: headRepository } } }, + }, + }; +} + +for (const branch of ["main", "dev"]) { + test(`a push to ${branch} runs verify without a pull request payload`, () => { + assert.equal(runsVerify({ event_name: "push", ref: `refs/heads/${branch}` }), true); + }); +} + +test("only the internal dev-to-main promotion skips duplicate verification", () => { + assert.equal(runsVerify(pullRequest("dev", "main")), false); +}); + +for (const base of ["main", "dev"]) { + test(`a fork branch named dev targeting ${base} still runs verify`, () => { + assert.equal(runsVerify(pullRequest("dev", base, 2)), true); + }); + + test(`an ordinary feature PR targeting ${base} runs verify`, () => { + assert.equal(runsVerify(pullRequest("fix/example", base)), true); + }); +} + +test("a dev PR targeting another integration branch is not a production promotion", () => { + assert.equal(runsVerify(pullRequest("dev", "dev")), true); +}); From 5188234d833829b81e7cc0ab738a4bd6e4cbd7f9 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 25 Oct 2026 09:04:01 -0400 Subject: [PATCH 4/7] ci(website): pin workflow actions to immutable revisions --- .github/workflows/website.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index a3e8319..35e1359 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -28,11 +28,11 @@ jobs: working-directory: frontend steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: 24.x cache: npm From f4660616386a6ff49b69ef8a758be352819d9955 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:46:43 -0400 Subject: [PATCH 5/7] refactor(repo): remove the stray portfolio app, old launcher and log junk - refactor(repo): delete visual-schedule-manager-portfolio/, an unrelated app last touched 2025-08-20 that nothing in frontend/ or CI references - refactor(repo): delete root start.sh, the pre-Vercel dev launcher - chore(repo): drop two tracked scratch files under logs/ --- ...3cef3872faf6ff0dd90aa7b611e2105-audit.json | 15 - logs/mcp-puppeteer-2025-12-10.log | 2 - start.sh | 260 ------ visual-schedule-manager-portfolio/.gitignore | 33 - visual-schedule-manager-portfolio/README.md | 169 ---- .../assets/integrations/fyle_favicon.png | Bin 688 -> 0 bytes .../assets/integrations/onelogin_favicon.png | Bin 1107 -> 0 bytes .../assets/integrations/sage_favicon.png | Bin 5938 -> 0 bytes .../schedules/AutoScheduleDialog.tsx | 351 --------- .../schedules/BusinessHoursModal.tsx | 291 ------- .../schedules/BusinessHoursOverlay.tsx | 71 -- .../schedules/CurrentTimeIndicator.tsx | 79 -- .../components/schedules/DragPreview.tsx | 158 ---- .../schedules/DraggableJobBadge.tsx | 207 ----- .../schedules/DroppableTimeSlot.tsx | 104 --- .../components/schedules/README.md | 259 ------ .../schedules/ScheduleDetailsModal.tsx | 281 ------- .../schedules/SchedulerErrorBoundary.tsx | 79 -- .../components/schedules/TimelineGrid.tsx | 125 --- .../schedules/TimelineScheduler.tsx | 743 ------------------ .../components/schedules/TimelineTrack.tsx | 102 --- .../components/schedules/TimezoneSelector.tsx | 144 ---- .../components/schedules/index.ts | 17 - .../components/ui/alert.tsx | 59 -- .../components/ui/badge.tsx | 46 -- .../components/ui/button.tsx | 59 -- .../components/ui/card.tsx | 92 --- .../components/ui/dialog.tsx | 143 ---- .../components/ui/label.tsx | 26 - .../components/ui/scroll-area.tsx | 58 -- .../components/ui/select.tsx | 185 ----- .../components/ui/separator.tsx | 28 - .../components/ui/switch.tsx | 29 - .../components/ui/tooltip.tsx | 61 -- visual-schedule-manager-portfolio/demo.tsx | 84 -- .../hooks/useDragAndDrop.ts | 147 ---- .../hooks/useScheduleTimezone.ts | 223 ------ .../lib/api/client.ts | 181 ----- .../lib/utils.ts | 51 -- .../package.json | 45 -- .../stores/schedule-store.ts | 247 ------ .../tailwind.config.js | 77 -- .../tsconfig.json | 22 - .../utils/auto-schedule.ts | 220 ------ .../utils/collision-detection.ts | 110 --- .../utils/date.ts | 109 --- .../utils/timeline-helpers.ts | 74 -- .../utils/timezone-conversion.ts | 148 ---- 48 files changed, 6014 deletions(-) delete mode 100644 logs/.d4add09e83cef3872faf6ff0dd90aa7b611e2105-audit.json delete mode 100644 logs/mcp-puppeteer-2025-12-10.log delete mode 100755 start.sh delete mode 100644 visual-schedule-manager-portfolio/.gitignore delete mode 100644 visual-schedule-manager-portfolio/README.md delete mode 100644 visual-schedule-manager-portfolio/assets/integrations/fyle_favicon.png delete mode 100644 visual-schedule-manager-portfolio/assets/integrations/onelogin_favicon.png delete mode 100644 visual-schedule-manager-portfolio/assets/integrations/sage_favicon.png delete mode 100644 visual-schedule-manager-portfolio/components/schedules/AutoScheduleDialog.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/BusinessHoursModal.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/BusinessHoursOverlay.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/CurrentTimeIndicator.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/DragPreview.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/DraggableJobBadge.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/DroppableTimeSlot.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/README.md delete mode 100644 visual-schedule-manager-portfolio/components/schedules/ScheduleDetailsModal.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/SchedulerErrorBoundary.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/TimelineGrid.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/TimelineScheduler.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/TimelineTrack.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/TimezoneSelector.tsx delete mode 100644 visual-schedule-manager-portfolio/components/schedules/index.ts delete mode 100644 visual-schedule-manager-portfolio/components/ui/alert.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/badge.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/button.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/card.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/dialog.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/label.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/scroll-area.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/select.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/separator.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/switch.tsx delete mode 100644 visual-schedule-manager-portfolio/components/ui/tooltip.tsx delete mode 100644 visual-schedule-manager-portfolio/demo.tsx delete mode 100644 visual-schedule-manager-portfolio/hooks/useDragAndDrop.ts delete mode 100644 visual-schedule-manager-portfolio/hooks/useScheduleTimezone.ts delete mode 100644 visual-schedule-manager-portfolio/lib/api/client.ts delete mode 100644 visual-schedule-manager-portfolio/lib/utils.ts delete mode 100644 visual-schedule-manager-portfolio/package.json delete mode 100644 visual-schedule-manager-portfolio/stores/schedule-store.ts delete mode 100644 visual-schedule-manager-portfolio/tailwind.config.js delete mode 100644 visual-schedule-manager-portfolio/tsconfig.json delete mode 100644 visual-schedule-manager-portfolio/utils/auto-schedule.ts delete mode 100644 visual-schedule-manager-portfolio/utils/collision-detection.ts delete mode 100644 visual-schedule-manager-portfolio/utils/date.ts delete mode 100644 visual-schedule-manager-portfolio/utils/timeline-helpers.ts delete mode 100644 visual-schedule-manager-portfolio/utils/timezone-conversion.ts diff --git a/logs/.d4add09e83cef3872faf6ff0dd90aa7b611e2105-audit.json b/logs/.d4add09e83cef3872faf6ff0dd90aa7b611e2105-audit.json deleted file mode 100644 index cd692d5..0000000 --- a/logs/.d4add09e83cef3872faf6ff0dd90aa7b611e2105-audit.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "keep": { - "days": true, - "amount": 14 - }, - "auditLog": "/Users/sbenson/Documents/GitHub/codeswhat-website/logs/.d4add09e83cef3872faf6ff0dd90aa7b611e2105-audit.json", - "files": [ - { - "date": 1765398359218, - "name": "/Users/sbenson/Documents/GitHub/codeswhat-website/logs/mcp-puppeteer-2025-12-10.log", - "hash": "ccd5a8481ee3f00d001daac2efdaadc8dd19a519295b269b8a05ac7d6a00b207" - } - ], - "hashType": "sha256" -} \ No newline at end of file diff --git a/logs/mcp-puppeteer-2025-12-10.log b/logs/mcp-puppeteer-2025-12-10.log deleted file mode 100644 index 80d5595..0000000 --- a/logs/mcp-puppeteer-2025-12-10.log +++ /dev/null @@ -1,2 +0,0 @@ -{"level":"info","message":"Starting MCP server","service":"mcp-puppeteer","timestamp":"2025-12-10 15:25:59.282"} -{"level":"info","message":"MCP server started successfully","service":"mcp-puppeteer","timestamp":"2025-12-10 15:25:59.283"} diff --git a/start.sh b/start.sh deleted file mode 100755 index c2a90a6..0000000 --- a/start.sh +++ /dev/null @@ -1,260 +0,0 @@ -#!/bin/bash - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -MAGENTA='\033[0;35m' -NC='\033[0m' # No Color - -# Script configuration -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROJECT_DIR="$SCRIPT_DIR/frontend" -ENV_FILE="$PROJECT_DIR/.env.local" -ENV_EXAMPLE="$PROJECT_DIR/.env.local.example" - -# Process tracking -FRONTEND_PID="" - -# Display header -display_header() { - echo -e "${CYAN}╔═══════════════════════════════════════╗${NC}" - echo -e "${CYAN}║${NC} ${GREEN}🚀 Next.js Project Launcher${NC} ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${BLUE}Development Server${NC} ${CYAN}║${NC}" - echo -e "${CYAN}╚═══════════════════════════════════════╝${NC}" - echo "" -} - -# Check if a port is in use -check_port() { - if lsof -Pi :$1 -sTCP:LISTEN -t >/dev/null 2>&1; then - return 0 - else - return 1 - fi -} - -# Cleanup function -cleanup() { - echo -e "\n${YELLOW}🛑 Shutting down development server...${NC}" - - # Kill frontend process - if [ ! -z "$FRONTEND_PID" ]; then - kill -TERM $FRONTEND_PID 2>/dev/null || true - fi - - # Kill any remaining processes on port 3000 - if check_port 3000; then - lsof -ti:3000 | xargs kill -9 2>/dev/null || true - fi - - echo -e "${GREEN}✅ Development server stopped${NC}" - exit -} - -# Set up trap to cleanup on exit -trap cleanup EXIT INT TERM - -# Function to check Node.js -check_node() { - echo -e "${BLUE}📋 Checking Node.js version...${NC}" - - if ! command -v node &> /dev/null; then - echo -e "${RED}❌ Node.js is not installed${NC}" - echo -e "${YELLOW}💡 Install Node.js 20 or higher from https://nodejs.org${NC}" - echo -e "${YELLOW}💡 Or use nvm: nvm install 20.9.0 && nvm use 20.9.0${NC}" - exit 1 - fi - - NODE_VERSION=$(node -v) - NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1 | cut -dv -f2) - - if [ $NODE_MAJOR -lt 20 ]; then - echo -e "${RED}❌ Node.js 20 or higher is required (found $NODE_VERSION)${NC}" - echo -e "${YELLOW}💡 Update Node.js from https://nodejs.org${NC}" - echo -e "${YELLOW}💡 Or use nvm: nvm install 20.9.0 && nvm use 20.9.0${NC}" - exit 1 - fi - - echo -e "${GREEN}✅ Found Node.js $NODE_VERSION${NC}" -} - -# Function to check npm -check_npm() { - echo -e "${BLUE}📦 Checking npm...${NC}" - - if ! command -v npm &> /dev/null; then - echo -e "${RED}❌ npm is not installed${NC}" - exit 1 - fi - - NPM_VERSION=$(npm -v) - echo -e "${GREEN}✅ Found npm $NPM_VERSION${NC}" -} - -# Function to setup environment -setup_environment() { - echo -e "\n${BLUE}🔧 Setting up environment...${NC}" - - cd "$PROJECT_DIR" - - # Check if .env.local exists - if [ ! -f "$ENV_FILE" ]; then - if [ -f "$ENV_EXAMPLE" ]; then - echo -e "${YELLOW} Creating .env.local from example...${NC}" - cp "$ENV_EXAMPLE" "$ENV_FILE" - echo -e "${GREEN}✅ Created .env.local - please update with your values${NC}" - else - echo -e "${YELLOW}⚠️ No .env.local file found${NC}" - echo -e "${YELLOW} Creating basic .env.local...${NC}" - cat > "$ENV_FILE" << EOF -# Site Configuration -NEXT_PUBLIC_SITE_NAME=CodesWhat -NEXT_PUBLIC_SITE_DESCRIPTION=Digital Solutions & Software Consulting -NEXT_PUBLIC_SITE_URL=http://localhost:3000 -EOF - echo -e "${GREEN}✅ Created basic .env.local${NC}" - fi - else - echo -e "${GREEN}✅ Environment file exists${NC}" - fi -} - -# Function to install dependencies -install_dependencies() { - echo -e "\n${BLUE}📦 Installing dependencies...${NC}" - - cd "$PROJECT_DIR" - - # Check if node_modules exists - if [ ! -d "node_modules" ]; then - echo -e "${YELLOW} Installing npm packages...${NC}" - npm install - if [ $? -ne 0 ]; then - echo -e "${RED}❌ Failed to install dependencies${NC}" - exit 1 - fi - else - echo -e "${YELLOW} Checking for updates...${NC}" - npm install --silent - fi - - echo -e "${GREEN}✅ Dependencies installed${NC}" -} - -# Function to start frontend -start_frontend() { - echo -e "\n${BLUE}🎨 Starting Next.js development server...${NC}" - - cd "$PROJECT_DIR" - - # Check if already running - if check_port 3000; then - echo -e "${YELLOW}⚠️ Port 3000 is already in use${NC}" - echo -e "${YELLOW} Killing existing process...${NC}" - lsof -ti:3000 | xargs kill -9 2>/dev/null || true - sleep 2 - fi - - # Start frontend with Turbopack - echo -e "${CYAN} Starting with Turbopack for fast refresh...${NC}" - npm run dev & - FRONTEND_PID=$! - - # Wait for the server to start - echo -e "${YELLOW} Waiting for server to start...${NC}" - for i in {1..30}; do - if check_port 3000; then - echo -e "${GREEN}✅ Development server started${NC}" - return 0 - fi - sleep 1 - done - - echo -e "${RED}❌ Failed to start development server${NC}" - exit 1 -} - -# Function to display status -display_status() { - echo -e "\n${GREEN}╔════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║${NC} ✨ Development server is running! ${GREEN}║${NC}" - echo -e "${GREEN}╠════════════════════════════════════════════════╣${NC}" - echo -e "${GREEN}║${NC} ${BLUE}🌐 Local Server:${NC} http://localhost:3000 ${GREEN}║${NC}" - echo -e "${GREEN}║${NC} ${CYAN}⚡ Turbopack:${NC} Enabled for fast refresh ${GREEN}║${NC}" - echo -e "${GREEN}║${NC} ${MAGENTA}📱 Framework:${NC} Next.js 15.3.4 ${GREEN}║${NC}" - echo -e "${GREEN}║${NC} ${YELLOW}⚛️ React:${NC} Version 19 ${GREEN}║${NC}" - echo -e "${GREEN}╠════════════════════════════════════════════════╣${NC}" - echo -e "${GREEN}║${NC} ${YELLOW}Press Ctrl+C to stop the server${NC} ${GREEN}║${NC}" - echo -e "${GREEN}╚════════════════════════════════════════════════╝${NC}" -} - -# Function to open browser -open_browser() { - echo -e "\n${BLUE}🌐 Opening browser...${NC}" - - # Wait a moment for the server to be fully ready - sleep 2 - - # Detect OS and open browser - if [[ "$OSTYPE" == "darwin"* ]]; then - open http://localhost:3000 - elif [[ "$OSTYPE" == "linux-gnu"* ]]; then - xdg-open http://localhost:3000 2>/dev/null || echo -e "${YELLOW} Please open http://localhost:3000 manually${NC}" - else - echo -e "${YELLOW} Please open http://localhost:3000 in your browser${NC}" - fi -} - -# Function to show build info -show_build_info() { - echo -e "\n${CYAN}╔════════════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║${NC} ${YELLOW}📊 Deployment Info${NC} ${CYAN}║${NC}" - echo -e "${CYAN}╠════════════════════════════════════════════════╣${NC}" - echo -e "${CYAN}║${NC} ${BLUE}Build:${NC} npm run build ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${BLUE}Deploy:${NC} vercel --prod ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${BLUE}Preview:${NC} vercel ${CYAN}║${NC}" - echo -e "${CYAN}╚════════════════════════════════════════════════╝${NC}" -} - -# Function to monitor output -monitor_output() { - echo -e "\n${CYAN}📺 Live server output:${NC}" - echo -e "${CYAN}════════════════════════════════════════════════${NC}" - - # Wait for the process - wait $FRONTEND_PID -} - -# Main execution -main() { - display_header - - # Check prerequisites - check_node - check_npm - - # Setup - setup_environment - install_dependencies - - # Start server - start_frontend - - # Display status - display_status - - # Show deployment info - show_build_info - - # Open browser - open_browser - - # Monitor process - monitor_output -} - -# Run main function -main \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/.gitignore b/visual-schedule-manager-portfolio/.gitignore deleted file mode 100644 index 763d40f..0000000 --- a/visual-schedule-manager-portfolio/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/README.md b/visual-schedule-manager-portfolio/README.md deleted file mode 100644 index b238bce..0000000 --- a/visual-schedule-manager-portfolio/README.md +++ /dev/null @@ -1,169 +0,0 @@ -# Visual Schedule Manager - -A modern, drag-and-drop visual scheduler component for managing global sync schedules across multiple timezones. Built with React, TypeScript, and Tailwind CSS. - -![Visual Schedule Manager Demo](./demo-screenshot.png) - -## Features - -### 🌍 Multi-Region Support -- Manage schedules for USA, Europe, and Asia regions -- Automatic timezone conversion and display -- Region-specific business hours overlay - -### ✨ Intuitive Drag & Drop -- Drag to enable/disable sync jobs -- Drag to reschedule to different time slots -- Visual feedback during drag operations -- Collision detection for overlapping schedules - -### 🤖 Smart Auto-Scheduling -- Intelligent optimization algorithm -- Avoids business hours automatically -- Maintains 1-hour spacing between syncs -- Prioritizes user syncs before project syncs - -### 🎯 Additional Features -- Real-time timezone conversion -- Business hours visualization -- Stacking support for overlapping schedules -- Current time indicator -- Responsive design - -## Tech Stack - -- **React 18** - UI framework -- **TypeScript** - Type safety -- **Tailwind CSS** - Styling -- **Framer Motion** - Animations -- **Zustand** - State management -- **date-fns** - Date utilities -- **Radix UI** - Accessible UI components - -## Component Structure - -``` -visual-schedule-manager/ -├── components/ -│ ├── schedules/ -│ │ ├── TimelineScheduler.tsx # Main scheduler component -│ │ ├── TimelineGrid.tsx # 24-hour grid with time slots -│ │ ├── TimelineTrack.tsx # Container for draggable items -│ │ ├── DraggableJobBadge.tsx # Draggable sync job badges -│ │ ├── TimezoneSelector.tsx # Timezone picker -│ │ ├── AutoScheduleDialog.tsx # Auto-scheduling interface -│ │ ├── BusinessHoursModal.tsx # Business hours configuration -│ │ └── ...other components -│ └── ui/ # Reusable UI components -├── utils/ # Helper functions -├── stores/ # State management -├── hooks/ # Custom React hooks -└── assets/ # Icons and images -``` - -## Integration - -### Basic Usage - -```tsx -import { TimelineScheduler } from './components/schedules/TimelineScheduler'; -import { SchedulerErrorBoundary } from './components/schedules/SchedulerErrorBoundary'; - -function App() { - return ( - - - - ); -} -``` - -### Required Assets - -The component requires integration icons located in `/assets/integrations/`: -- `fyle_favicon.png` - Destination system icon -- `onelogin_favicon.png` - User sync source icon -- `sage_favicon.png` - Project sync source icon - -### Styling Requirements - -The component uses Tailwind CSS. Ensure your project includes: -1. Tailwind CSS configuration -2. The required color palette (especially gray scales) -3. Dark mode support (optional) - -### State Management - -The scheduler uses Zustand for state management. The store is self-contained and doesn't require external setup. - -## API Integration - -The portfolio version includes a mock API client that simulates backend responses. For production use, replace the mock API client with your actual implementation. - -### Required API Endpoints - -```typescript -interface API { - getVisualSchedules(params: { timezone: string }): Promise - updateVisualScheduleTime(id: number, data: { position: number; timezone: string }): Promise - toggleVisualSchedule(id: number, enabled: boolean): Promise - autoSchedule(params: { timezone: string }): Promise -} -``` - -## Schedule Data Structure - -```typescript -interface Schedule { - id: number; - title: string; - entity: 'usa' | 'europe' | 'asia'; - syncType: 'user_sync' | 'project_sync'; - position: number | null; // Grid position (0-95 for 24 hours) - enabled: boolean; - startTimeUTC: string | null; - duration: number; // In minutes -} -``` - -## Customization - -### Timeline Configuration -- Each hour = 4 grid blocks (15-minute intervals) -- Block width: 60px (configurable in `timeline-helpers.ts`) -- Track height adjusts based on stacked items - -### Business Hours -- Default: 8 AM - 6 PM local time -- Configurable per region -- Visual overlay on timeline - -### Styling -- Uses Tailwind utility classes -- Customizable through CSS variables -- Dark mode support included - -## Browser Support - -- Chrome (latest) -- Firefox (latest) -- Safari (latest) -- Edge (latest) - -## Performance Considerations - -- Efficient drag-and-drop with minimal re-renders -- Memoized timezone calculations -- Optimized for up to 50 schedules per region - -## License - -MIT License - feel free to use in your projects! - -## Demo - -View the live demo at: [your-portfolio-url.com/visual-schedule-manager](#) - ---- - -Built with ❤️ for managing complex scheduling requirements across global teams. \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/assets/integrations/fyle_favicon.png b/visual-schedule-manager-portfolio/assets/integrations/fyle_favicon.png deleted file mode 100644 index add5be36d058965ae1cd0e80c8644a2b2c141a81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 688 zcmV;h0#E&kP)73KPIVYVR2Y9C`q0h>!8U=(X+4Eclcm;}{ zJ+*ImYJFNrPHehB6|&$1L^yob37buD7>)ud-~=I6ACJDB|uIB(nM+$JSEjvq{;`*a=#KWwaxk< zP9)`*VGiT?5E8)DO6L`%nYgYXq>9vIK>_X3XGt%ZcS0>883iJoe@B&ku+{C z#u$HcEB#(!U<}|hFzqc;3N$SFLY}5PIRZclGH3N7#rqxNj*tNuw6;>DnuXuKp5=`t zmb${}3!-t0G;g|B3a}~i9hP{O>OHRB_^$%Q$Ybz)kp~K{{bg(jS2iE&_hJYYWC4() zHHoMdELZ_(SNbHNET9(^tXUg{j{qwp_{I>9_Yd9K5&nFy3)r`xJ!*jXk9_L=`_C^$ Wlv-&NAUNLu0000YncDnQ^?tW0|0Ym_AmW4@PcEHagXEhNiDVNRlVmR{%vRFvoDzYE zVT`V~mz(b{E!E!_V@z%Vma+$js|P={9q(Bq2FOX=%{?^V6Ah#0VC-P1of`Z2R*uk)_`?CY<-;g|Z~G)C_^`M)9r zN;wCKD2c-Mx4>!&lW@r99k|xnr27pp|;0&|X137j1dJ({W zxia`R0Fb3rsXB7_XG_^*`RZIE5!{p~B7lfJ5#jAdZ4N+0*XCyax?lNz-xJDq#&%wK z1QD~)s9k$pXK8bj2?Il;?-dIE3ps-TTWZ+#N+*=V8vkx|7HzhqNz!!P zQtsUn0=Qo31UruRaBmx(bS)DivbweZmjc@<995RG4*@w95EVVI{HQir(`Y@P#NlLb zq#Ni(z--;2095iSCQxNy=)hORj3`(-Uz?qLWw!zVfQVg%9&1R%0D}Cv-T@4tz00w; ztUyE();hc1>qfE{AR-u@&9Rv50{#E|00@~*e&smojPG&=Hm9KHl*D0Yf?LZA<65V) zyAYw)&H*9r*NFzP$Gc zW{=NIFhkg-}LjEXL7Sg(?9?FuLO?~$2Lt)td$pU~ZOPh}4mYqWJ zk-e(#?a3_!o#nI3OZBtBh8t0uMBn><-$`O?WDiQbD~Jdp`bx7gecEUp0UPdwX_|Bz zwW$|H%pBc@x?FiH&|;%DbvjMrg|+sZE)Y@F`K?jA_Kb+^sfd-xWpS}N^L!MxFKo1J zIYP56xt=89c+syMXI9l)T@%a}#AKJ7_37t2ou%(K+q<6OS(enIu=SC$?Xt4n2Z>hR zgS~)=!Ayw4_PJ(b`sFwZE_dy>lcQp>a;&m%=q+Uzo+h^XAhP;W+bU=^&oIWc;y4&@ zFE>93gVvU)dlIk)$8isr%Kc9jT>nX8>j<+o0>EE22gc~RG>tFnG@j^mmcEFi;Ez0h Z{{aG^ zd2pO(eaAn~yQ^z;%eG`&zGLTtgdE@|j?)AP=L~lXQ>F(@fI>@4Nrz;Z!Zft>U@}92 zq=i5arZk0e50s`f7jXzqnvi0gG?(K`wq^OyO4hAC-v0637pcOBq~~4jyU#wKnVopF zuzIxbcYphPf2Uz_1VArvC9oZM39td^2AYA?B45{2TNHpX;25wU_!;mp@C0zU^!$P_ z3;f?V051jJ3cM273}hDZIv&sI2!U~6FK{pL9pEuwGVWiL0KMKIzz-26w>wupp;1BT+ z|Cs&Q4P4I|IFny;9q=XKqBH#;J)R2D4P1kSy?^eLrxWyLcrkmD%`y}+-U%F?{lr{B zcLTShPk_ghWO{)0z&$9I4g7yW2f)q1yYX>lPjzq}ic#-C{x!j@C$9$HGv{X>Pm&3M z_X1bV{sAZh_)!BEP9dps_XuJi$_9s@K6&`}4SY3Qocqa_8I z6r_#PSNRa8i!fGz;Q|ciAe)1sJet=51n-q zciKRzBxF-6TnvFQgd!4mK9smm=HTcQ?3sXP$KbhfI6S52JPXFV9P_0u4KHejZ5?oB zD{O4i&(d=QJKuLRD$q4%=HJsrS?s+N@Y7T9$S^!JE(`9N7hce3BQUV80bbq(uk4aF zZb|E1D=t913&E6rZwQR!Ww9T~!h=}}z2Gm`+3-5xkP*> zB5@Z(=%1FTzjqv-9Q8i;8SsLh%jyRBwQhLR3g~Un&x(~j5-{VBpUlC1gYa(y@bs9s z*mLlLj%W;Ijo-BjE^L;7k|5-{7$oeYQ}FF$@bv*WI$h~gB?d2Ofc1^=zFv6Ua)sK8 zRq&QXAGqD4@P$M0(2yc0HBM~{+5p#dz=zipZb z-5=B#wIk@1fp@M{Sgy`dF-H_bSlb96->5Ww4N*ISe$6tu8jXS5VvZ<=u%QV)x>3bb zH9~C&`o%5qp|#LnS3WTt^lqkRSK6KJc~kxgg2hmO`%UXr30MQTe^c0*hQHnd*LRjD zG#3eTyZ|S1aBx!PpGT)uB{x|Lp)>|6JMF25mGyE7y6aUp9o1`;>q>;c7Z1Z7ebpVF zoCj{#LmS}NdvtRY_k~Oum3!_PSDEL|VR(LAwH6cShy&IK`k<|Cgv(mt>UMZ>bLktT z48jh8-{^r|Bk-ea%rBlS+_un{w!r5ukVT9u)*N-8>>7pd9)}%SI5utfE@o27RK9kZ z%2Zc2#4qfWfyYn5ho94V9VG?aTIoZSg~kB>XdPVH7NeLA^i8RD;xm0J!jGe~Bp1Sw zDR_7o9vxQk(Yi)xOvNYa5PeWi6?AS#aaYjSbg1B{K88E5=p`N;fb-uX8-Z~rPVtnfu{MCLq5c^Wo zU#5t%eUA>q@_INg6Hh!U1lrU3Fzy_V;RTb2n_;`lTVZRveHo}hHo1M%Do z6j3M64P7z3Sn_aB(CZo%1`93K92ixs-QK5W)k-o_Pz?E=lkr&Tlz~@tLWg@%TlWOL zwGCD^*jY0J4CUbShZKEC0z-L4R-PT#hs}m!2Es41vdtMa3Xm( zPWva|&OsKOa^7h>b5DqMO3aZiVmM5p>$X=8T=y3XE<7caO$)tg2|5MK>+Eg_L*Rw+>X;ihFs(t3 zHaCYMy60$iXMuw|g6^t=Cd&(xFT$aO#GTK@bW!!!HlMvPlpo#Xjtt!SOWM=+m30NG z!`+DJo3slphNu=h<1KXZXinL8a~H#zmZ@ss#I$`hCT+ZhP9$~4J|!GdT~n0CiuQsI zj54f_;Lcx?ilK`-Fwjs%cl?tDh3>4<;9;_3?hC{nLC4WG9Z+3&{MkGV=j|dL6Ggp- zBe)~zTxjnuqz&{m#B!V^;bac>O)TC*2SzuROWn9oM%)qfMA5#OC}q^gS1n8x)#td_ zxOp*zpA1*7d7%urBj~Zb;>flx@cEhQay#xHgr`RJVO$77X-etcC(3iK3gM2Rvw8d4 zhGM8WaMfg`k4>u);rR6YqdbApRHrW-TAHTcCBz*;59igYWSa`^s;j;+v^%o!sRPhI zb@phFLC+6Nsp>glPZhdGhy$VB^Y-E4e-oZJS*Ai0(fVSis<(wv~%T5OcrISca>_o-JV|f zOSt1hgW9G>3igqXVhEd>;P=*4f6rYFi5XlI^u7tX9kvampd`(!yWl;m^|kG(3f#I< zgE@_ijjJ3ZW#G40Y0;2F6=^QxR+IxI=2vvZGj$Y{4u46jq6bfnR#$mx4dR}lhw~cL zxW+PkH=DFk)L=uC+McrR6!W--dxD-S%01ZD5${qSgIfN#wrkeifk`LhxYdq(f(E65 zukKI-Xq=i0QPSd?4t)b0nR2R9%>~>P^k_kD!Zq#jL_B5_BLDp27Pz)UTdex0RU6@{ zIBp6W)V+O43#@M{Pb@Z?hX1m5xi^_W>m({Ydbh@LSJ0D1xdYof$`^8rJ}ArUm2$tf zLkljmd0A{95Lt5E6*Oo8W@7*^Z7c8A)=1c0_3*MbxV}pZn7MzAyLDO)eb}X*FPUsU*OJR`TRavV?}KV8dR6@@r_!TQz=nL%yvl*Pik|(J%g}& zL}jp^1yg&12Hhauyj~%_q`U)>OEQv&C&$!{{m_v51e0*SD#7h2$C((+!6T;>(%X{J zY`02_CM-Sb0}UbH-T~KkYUcG|X+lz^&sYT{#XePrh5+8M9NxD^A_`*|Cl)7>uiZZl z4-CS0kE>@eiQBmnLTy^;h(aiVJbp@P>~&4*CrpZ7qI0yRRK9wBC#-Bxiha;q?9)-( z7J4?#DR}KN_^nmynl>aab#0KJ-9H82KMvnKrfzP}FQ{z`J)2y~T0b-d<3+7kZcku? zY=|uO6>Tb!ErhDgp7wSLyoEkJ0g8mYts7pqTy@pNFx@n60;63kJF;-+VR&-1=C7u= z(5E9rm3lje;n87b@>bMqq(L-=C<%nWFb zOv@EXC#VZNs?ENtLm#}S$DA6GyAZs~u@DT3GF;oC8^}vrGzTwCh&vLM$UZWx;f>FX zRrENOzzce@L?xoH>{1eBO=%-*9IG*G35>-27yIC`Qx!hU(&7beOUl6N2KlhpcPd`o zRr0kfDehEhp!p~E%3{|DUeIw8rP8nMkkvjv11+V{Uqwn*OQQbR9(ZD8>3@F(s69a& zltk&M({97@g6bruit)Y^?P<8USyp>nhqeMV1WRwVQ%2#uTlQ4nB-KSwJAyW-bLI8R z;i`7UmeVLC*FPm+^S&(X9gp`0FF?O*YdhS~1y{7H;0UvHRy#1Nx&GJ-)wTM05!8mD z8v=Oa3b=8#EM-*m13+noLorl={a5jUNKB54TZhP04qciuyy~ONZf( zKBwcRz%!Cs2jKTbjb}W zqp&k6{f-?&&h|07E$BC{fDf%x^kE?)X$WM^AIO&f!cP`uH6O_8L)25R!8PTWBN75D z>y`bCR#rHMyMo@5fsb!m{J_SJw3hJfU!t$)$s#;es`i^K>ISwY9ly0s1$s{&9C9)} zo}hw+7^y2w-rv}?xJW_>G^XI9=Gc#RPL52$?S1f(=V!8faa!wQsA8rYx?+6Mr>M~T%s<9`u4fBC8ifl4($`8`DMj_Y=trYmF7)i-+Nb z3HuLih-$ho%*1lEq~MmIFKV{G84Qdvggdga9V=E|n1HVhKt6_qPhjA}3WO%hh*2wP9(T0?T05dhS%0ggWQhhl>O@@1a}186lmeR4F(B0sn$8-*fiWbXcuP( zeUtUnYr}vexFhJ+v@VXVyYcK;bxb3AcvxEiY>adm%8yRsK%GV6j-Wf!%6!>atq_d> zsD?_keG@7JwRJlh0;e*D?g+X)t(syR@?rJV3XkOFN81phx)<+qB#^eWeUW@2RB2^3 z5CR7#>_e1k15IAg$)hn)alQ?OP^so>;pnt|J5^xpBSQ(o9YLp!{a+JO`MoO(A&zae z275!0AlwnOG4?6pfa1{s1eFX-ekQjn#w@fgxZ%Y&+gwzR$fI~~dwcLa9?oz2Oov}H?1 zY4mEKx53`$pC~vrO{q$*+&svZcKI2L3tSB0ye3T(O*(;5?TSsN&=_qhaA<3-JAxj` z!;xvr+6FX_S3oX=fi z+0p5^PL>GV67)m?b`Hgp4$h?Dy}kBbFmZEPE4-!K{&fnW-m5r5oe9D%L4!Iuj$67N zpby1`8TiAs$ti$eR;O9lww*Z!wQ)V_-i9pq1pUIew!g;FKw1P|wG94Xty+~TNmBs7 zyC$BgP=SH}J{3e~({EhoHt555({oO*N_Jb&0Kd5sZr-38jky2P zNZ8q&((aE8YcEUR6b$9gZUqhMwq9N*SLUiVt(4o;Bq7J$7G=;l-oH8MRLj*|+!u6n z3jTDHtX{bqU=1_N>PHH4MfxV;=#>6FT2KNbFp5}oq~)@#s#nHuS)IgPt|r}-fuEj& zo1WL19L9Y?gG6)N7G>GWZJi>k9R*mzaAvt@^s~9om-FT_0FLG0mglv%#c|vW+s)%$SVu49}N~D5`PiA*cQ3+0=%hL2=hl?N`dT+@ekb27O@u z^(cJvSS@@H1GOY*fcpj|>JyHOI&vhxa~$qG>|6oT*+@v$%@Pr!ly4z~TQ zG#?CdW4?DBK7G)sj_&zT8#je>F(}@9%LZlRTnS@Eb2h$t3_g2M^P_8w+7xue#m#Wj zdf3)p{qICzU^EY3?uUOmqSSv4Qtg+~o)z^f{CQiqdi1K%1xXprwZ5ZIE|sHc?fKvZ z4W&-+E_JB9sI(QT8s3!viTB4t@VAGQt*mjppl7qXL1m(EUZG50F-+#W6D4o@CgHB5 z@SS5Cvg7fB{v}dIR{HHdvf9h)%Ad`fFHws9WKOHyzIs%1HT)I)1zyl+qRzl2E%1gF znt`#hex_byDc`Zk8WSpoy(0_XKBm>pHS8;&2`}h#kv7_geSN1M=Vx^DXegh!BTEcg zk+F9I9?Ys#)v-xOapMKO5Zcn}1mD^YSGK}=8Lg9xwr_>ZjOLo}yE?a+sZgre>zjn1 zjlfTaVb_SdWc-ynCtlEtr8%XW$n!IrGq*XT%wty?TGF!YDWjgC(^c0+#YvMzB|wf( z!@fyPe|mZh_D`y&+FzXo;RPKRb)|I{ooVQ()6HaCO2TglB=91TD@w#81vLf?=F~|t zl-Cx5B;0&fF<#IfrwGvM8IQ*W3IX6|Kf9+E84G}8l|F^X6B7dgu)or$@OWZke*o;N z^eH@^nAjZvKL&i6mZ#bo10D{5CxE>bK8(j>%O2pd0O$kmt?*$y9$W4O4h59@gMpKk zJ&(t;Xig#xhtU{=$AG&lcOH+&ivIu}hncw?Q^4ne{z{$5<5?vANV`*@TCj-2Kr3)9 z@ho%jc*;!?_zZ9trJAJKf)0WI1zrqnS@NTKJPYS;;7^zpcAmT1X$*J@*aq}0-MKuT z1+yFYAag>_r`uIFfGqWPU|IRkrn@Qdw@yc0u%-Dc$S)@z-NJva*i^M z`Rw~02OdI2O!dGTAY*wzk7qs%0^b5Yf_+!(kD)~0g8NUt|VE_OC07*qoM6N<$f{R~QF#rGn diff --git a/visual-schedule-manager-portfolio/components/schedules/AutoScheduleDialog.tsx b/visual-schedule-manager-portfolio/components/schedules/AutoScheduleDialog.tsx deleted file mode 100644 index 23348ab..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/AutoScheduleDialog.tsx +++ /dev/null @@ -1,351 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '../ui/dialog'; -import { Button } from '../ui/button'; -import { Badge } from '../ui/badge'; -import { Separator } from '../ui/separator'; -import { Clock, CheckCircle, AlertTriangle, Loader2, ArrowRight } from 'lucide-react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { toast } from 'sonner'; -import { api } from '../../lib/api/client'; -import Image from 'next/image'; - -interface AutoScheduleDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -interface ProposedSchedule { - id: string; - title: string; - emoji: string; - entity: 'usa' | 'europe' | 'asia'; - syncType: 'project' | 'user'; - currentTime?: string; - proposedTime: string; - localTime: string; - reason: string; -} - -export function AutoScheduleDialog({ open, onOpenChange }: AutoScheduleDialogProps) { - const [isLoading, setIsLoading] = useState(false); - const [proposedSchedules, setProposedSchedules] = useState([]); - const [step, setStep] = useState<'preview' | 'generating' | 'success'>('preview'); - const [hasGenerated, setHasGenerated] = useState(false); - - // Generate proposed schedules when dialog opens - React.useEffect(() => { - if (open && step === 'preview' && !hasGenerated && !isLoading) { - generateProposedSchedules(); - } - }, [open, step, hasGenerated, isLoading]); - - const generateProposedSchedules = async () => { - if (isLoading) return; // Prevent concurrent calls - - setIsLoading(true); - setStep('generating'); - setHasGenerated(true); - - // Simulate delay for mock - setTimeout(() => { - // Mock proposals for each region - const mockProposals: ProposedSchedule[] = [ - // North America - { - id: '-1001', - title: 'User Sync', - emoji: '👤', - entity: 'usa', - syncType: 'user', - proposedTime: '2:00 AM EST', - localTime: '2:00 AM', - reason: 'Scheduled during off-peak hours to avoid business operations' - }, - { - id: '-1002', - title: 'Project Sync', - emoji: '📁', - entity: 'usa', - syncType: 'project', - proposedTime: '3:00 AM EST', - localTime: '3:00 AM', - reason: 'Runs after user sync to ensure user data is current' - }, - // Europe - { - id: '-2001', - title: 'User Sync', - emoji: '👤', - entity: 'europe', - syncType: 'user', - proposedTime: '2:00 AM GMT', - localTime: '2:00 AM', - reason: 'Scheduled during off-peak hours to avoid business operations' - }, - { - id: '-2002', - title: 'Project Sync', - emoji: '📁', - entity: 'europe', - syncType: 'project', - proposedTime: '3:00 AM GMT', - localTime: '3:00 AM', - reason: 'Runs after user sync to ensure user data is current' - }, - // Asia - { - id: '-3001', - title: 'User Sync', - emoji: '👤', - entity: 'asia', - syncType: 'user', - proposedTime: '2:00 AM JST', - localTime: '2:00 AM', - reason: 'Scheduled during off-peak hours to avoid business operations' - }, - { - id: '-3002', - title: 'Project Sync', - emoji: '📁', - entity: 'asia', - syncType: 'project', - proposedTime: '3:00 AM JST', - localTime: '3:00 AM', - reason: 'Runs after user sync to ensure user data is current' - } - ]; - - setProposedSchedules(mockProposals); - setStep('preview'); - setIsLoading(false); - }, 1500); - }; - - const handleApplySchedule = async () => { - setIsLoading(true); - - // Mock apply - just show success - setTimeout(() => { - setStep('success'); - toast.success(`Applied auto-schedule for ${proposedSchedules.length} syncs`); - - // Close dialog after success - setTimeout(() => { - onOpenChange(false); - setStep('preview'); - setProposedSchedules([]); - setHasGenerated(false); - }, 2000); - - setIsLoading(false); - }, 1000); - }; - - const handleCancel = () => { - onOpenChange(false); - setStep('preview'); - setProposedSchedules([]); - setHasGenerated(false); - }; - - return ( - - - - - - Auto-Schedule Optimization - - - Automatically optimize your sync schedules to run during off-peak hours with minimal conflicts. - - - - - {step === 'generating' && ( - - -

Generating Optimal Schedule...

-

- Analyzing your sync requirements and optimizing timing to minimize conflicts - and avoid business hours. -

-
- )} - - {step === 'preview' && ( - -
-
- -
-

- Schedule Optimization Rules -

-
    -
  • • User syncs scheduled before project syncs
  • -
  • • All syncs start at 2:00 AM local time
  • -
  • • 1-hour spacing between syncs to prevent conflicts
  • -
  • • Avoids business hours
  • -
-
-
-
- -
-

Proposed Schedule Changes

- - {proposedSchedules.map((schedule, index) => { - const sourceIcon = schedule.syncType === 'user' - ? '../../assets/integrations/onelogin_favicon.png' - : '../../assets/integrations/sage_favicon.png'; - - return ( - -
-
- - {schedule.entity === 'usa' && '🇺🇸'} - {schedule.entity === 'europe' && '🇬🇧'} - {schedule.entity === 'asia' && '🇯🇵'} - - -
-
- Source { - e.currentTarget.style.display = 'none'; - }} - /> -
- - - -
- Fyle { - e.currentTarget.style.display = 'none'; - }} - /> -
- - {schedule.title} -
- - - {schedule.entity === 'usa' && 'North America'} - {schedule.entity === 'europe' && 'Europe'} - {schedule.entity === 'asia' && 'Asia'} - -
- -
-
- {schedule.localTime} -
-
- {schedule.proposedTime} -
-
-
- - {schedule.currentTime && ( -
- Current: {schedule.currentTime} - → - New: {schedule.proposedTime} -
- )} - -

{schedule.reason}

-
- ); - })} -
-
- )} - - {step === 'success' && ( - - -

Schedule Applied Successfully!

-

- Your sync schedules have been optimized and updated. -

-
- )} -
- - - {step === 'preview' && ( - <> - - - - )} - - {step === 'generating' && ( - - )} - -
-
- ); -} \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/schedules/BusinessHoursModal.tsx b/visual-schedule-manager-portfolio/components/schedules/BusinessHoursModal.tsx deleted file mode 100644 index f03bc66..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/BusinessHoursModal.tsx +++ /dev/null @@ -1,291 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog'; -import { Button } from '../ui/button'; -import { Label } from '../ui/label'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; -import { Switch } from '../ui/switch'; -import { Clock, Building2 } from 'lucide-react'; -import { toast } from 'sonner'; - -interface BusinessHoursModalProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -interface BusinessHoursConfig { - usa: { start: number; end: number; enabled: boolean }; - europe: { start: number; end: number; enabled: boolean }; - asia: { start: number; end: number; enabled: boolean }; -} - -const DEFAULT_BUSINESS_HOURS: BusinessHoursConfig = { - usa: { start: 8, end: 18, enabled: true }, - europe: { start: 8, end: 18, enabled: true }, - asia: { start: 9, end: 18, enabled: true } -}; - -export function BusinessHoursModal({ open, onOpenChange }: BusinessHoursModalProps) { - const [config, setConfig] = useState(DEFAULT_BUSINESS_HOURS); - const [isDirty, setIsDirty] = useState(false); - - const formatHour = (hour: number): string => { - if (hour === 0) return '12:00 AM'; - if (hour === 12) return '12:00 PM'; - if (hour < 12) return `${hour}:00 AM`; - return `${hour - 12}:00 PM`; - }; - - const handleSave = () => { - // In a real implementation, this would save to backend/store - toast.success('Business hours configuration saved'); - setIsDirty(false); - onOpenChange(false); - }; - - const handleCancel = () => { - if (isDirty) { - if (confirm('You have unsaved changes. Are you sure you want to cancel?')) { - setConfig(DEFAULT_BUSINESS_HOURS); - setIsDirty(false); - onOpenChange(false); - } - } else { - onOpenChange(false); - } - }; - - const updateConfig = (entity: 'usa' | 'europe' | 'asia', field: keyof typeof config.usa, value: any) => { - setConfig(prev => ({ - ...prev, - [entity]: { - ...prev[entity], - [field]: value - } - })); - setIsDirty(true); - }; - - return ( - - - - - - Business Hours Configuration - - - -
- {/* USA Business Hours */} -
-
-

- 🇺🇸 - North America Business Hours - (Eastern Time) -

- updateConfig('usa', 'enabled', checked)} - /> -
- - {config.usa.enabled && ( -
-
- - -
- -
- - -
-
- )} -
- - {/* Europe Business Hours */} -
-
-

- 🇬🇧 - Europe Business Hours - (London Time) -

- updateConfig('europe', 'enabled', checked)} - /> -
- - {config.europe.enabled && ( -
-
- - -
- -
- - -
-
- )} -
- - {/* Asia Business Hours */} -
-
-

- 🇯🇵 - Asia Business Hours - (Tokyo Time) -

- updateConfig('asia', 'enabled', checked)} - /> -
- - {config.asia.enabled && ( -
-
- - -
- -
- - -
-
- )} -
- -
-
- -
-

About Business Hours

-
    -
  • • Business hours are shown as a shaded area on the timeline
  • -
  • • Scheduling during business hours will show a warning
  • -
  • • Times are in each region's local timezone
  • -
  • • Sync jobs should ideally run outside business hours
  • -
-
-
-
-
- - - - - -
-
- ); -} \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/schedules/BusinessHoursOverlay.tsx b/visual-schedule-manager-portfolio/components/schedules/BusinessHoursOverlay.tsx deleted file mode 100644 index d69f94c..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/BusinessHoursOverlay.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client" - -import React from 'react'; -import { getTimelinePosition, DEFAULT_BUSINESS_HOURS, TIME_BLOCK_WIDTH } from '../../utils/timeline-helpers'; -import { cn } from '../../lib/utils'; - -interface BusinessHoursOverlayProps { - entity: 'usa' | 'europe' | 'asia'; - className?: string; -} - -interface BusinessHours { - start: number; // position 0-95 - end: number; // position 0-95 - label: string; -} - -const getBusinessHours = (entity: 'usa' | 'europe' | 'asia'): BusinessHours[] => { - const config = DEFAULT_BUSINESS_HOURS[entity]; - - if (!config) { - return []; - } - - const startPos = getTimelinePosition(config.start, 0); - const endPos = getTimelinePosition(config.end, 0); - - return [{ - start: startPos, - end: endPos, - label: 'Business hours - preferred scheduling window' - }]; -}; - -export const BusinessHoursOverlay: React.FC = ({ - entity, - className -}) => { - const businessHours = getBusinessHours(entity); - - if (businessHours.length === 0) { - return null; - } - - return ( - <> - {businessHours.map((hours, index) => { - const leftPosition = hours.start * TIME_BLOCK_WIDTH; - const width = (hours.end - hours.start) * TIME_BLOCK_WIDTH; - - return ( -
- {/* Optional: Add a subtle pattern or border */} -
-
- ); - })} - - ); -}; diff --git a/visual-schedule-manager-portfolio/components/schedules/CurrentTimeIndicator.tsx b/visual-schedule-manager-portfolio/components/schedules/CurrentTimeIndicator.tsx deleted file mode 100644 index 6b01331..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/CurrentTimeIndicator.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client" - -import React, { useEffect, useState } from 'react'; -import { getTimelinePosition, TIME_BLOCK_WIDTH } from '../../utils/timeline-helpers'; -import { cn } from '../../lib/utils'; - -interface CurrentTimeIndicatorProps { - timezone: string; - className?: string; -} - -export const CurrentTimeIndicator: React.FC = ({ - timezone, - className -}) => { - const [position, setPosition] = useState(null); - const [currentTime, setCurrentTime] = useState(''); - - useEffect(() => { - const updatePosition = () => { - // Get current time in the specified timezone - const now = new Date(); - const options: Intl.DateTimeFormatOptions = { - timeZone: timezone, - hour: 'numeric', - minute: 'numeric', - hour12: false - }; - - const timeString = now.toLocaleString('en-US', options); - const [hours, minutes] = timeString.split(':').map(Number); - - // Calculate position based on current time - const timelinePosition = getTimelinePosition(hours, minutes); - setPosition(timelinePosition); - - // Format time for display - setCurrentTime( - now.toLocaleString('en-US', { - timeZone: timezone, - hour: 'numeric', - minute: '2-digit', - hour12: true - }) - ); - }; - - // Update immediately and then every minute - updatePosition(); - const interval = setInterval(updatePosition, 60000); - - return () => clearInterval(interval); - }, [timezone]); - - if (position === null) { - return null; - } - - const leftPosition = position * TIME_BLOCK_WIDTH; - - return ( -
- {/* Time label */} -
- {currentTime} -
-
- ); -}; diff --git a/visual-schedule-manager-portfolio/components/schedules/DragPreview.tsx b/visual-schedule-manager-portfolio/components/schedules/DragPreview.tsx deleted file mode 100644 index 3ea91d8..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/DragPreview.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import React from 'react'; -import { cn } from '../../lib/utils'; -import { JobBadgePreview } from './DraggableJobBadge'; -import type { DragItem } from '../../hooks/useDragAndDrop'; - -interface DragPreviewProps { - item: DragItem; - showTimePreview?: boolean; - previewPosition?: number; // Position where it will be dropped -} - -export const DragPreview: React.FC = ({ - item, - showTimePreview = false, - previewPosition, -}) => { - const formatTimeFromPosition = (position: number): string => { - const hours = Math.floor(position / 4); - const minutes = (position % 4) * 15; - - const formattedHours = hours.toString().padStart(2, '0'); - const formattedMinutes = minutes.toString().padStart(2, '0'); - - return `${formattedHours}:${formattedMinutes}`; - }; - - return ( -
- {/* Main badge preview */} -
- -
- - {/* Time preview tooltip */} - {showTimePreview && previewPosition !== undefined && ( -
-
- Drop at {formatTimeFromPosition(previewPosition)} -
-
-
-
-
- )} -
- ); -}; - -// Enhanced drag preview with queue information -interface EnhancedDragPreviewProps extends DragPreviewProps { - willQueue?: boolean; - queuedAfter?: string[]; - estimatedStartTime?: number; -} - -export const EnhancedDragPreview: React.FC = ({ - item, - showTimePreview = false, - previewPosition, - willQueue = false, - queuedAfter = [], - estimatedStartTime, -}) => { - const formatTimeFromPosition = (position: number): string => { - const hours = Math.floor(position / 4); - const minutes = (position % 4) * 15; - - const formattedHours = hours.toString().padStart(2, '0'); - const formattedMinutes = minutes.toString().padStart(2, '0'); - - return `${formattedHours}:${formattedMinutes}`; - }; - - return ( -
- {/* Main badge preview */} -
- -
- - {/* Enhanced preview with queue information */} - {showTimePreview && previewPosition !== undefined && ( -
-
- {willQueue ? ( -
-
- Will be queued -
-
- Scheduled: {formatTimeFromPosition(previewPosition)} -
- {estimatedStartTime !== undefined && estimatedStartTime !== previewPosition && ( -
- Estimated start: {formatTimeFromPosition(estimatedStartTime)} -
- )} - {queuedAfter.length > 0 && ( -
- After {queuedAfter.length} job{queuedAfter.length > 1 ? 's' : ''} -
- )} -
- ) : ( -
- Drop at {formatTimeFromPosition(previewPosition)} -
- )} - - {/* Tooltip arrow */} -
-
-
-
-
- )} -
- ); -}; - -// Simple drag preview for use in DragOverlay -export const SimpleDragPreview: React.FC<{ item: DragItem }> = ({ item }) => { - return ( -
- -
- ); -}; \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/schedules/DraggableJobBadge.tsx b/visual-schedule-manager-portfolio/components/schedules/DraggableJobBadge.tsx deleted file mode 100644 index 2753417..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/DraggableJobBadge.tsx +++ /dev/null @@ -1,207 +0,0 @@ -"use client" - -import React from 'react'; -import { Badge } from '../ui/badge'; -import { AlertCircle, ArrowRight } from 'lucide-react'; -import { cn } from '../../lib/utils'; -import { TIME_BLOCK_WIDTH } from '../../utils/timeline-helpers'; -import Image from 'next/image'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "../ui/tooltip"; - -interface Schedule { - id: string; - title: string; - entity: string; // Now can be 'usa', 'europe', or 'asia' - syncType: string; // Can be 'user_sync' or 'project_sync' - emoji: string; - startTimeUTC: string; - duration: number; - enabled: boolean; - frequency: 'daily' | 'manual'; - position: number; - hasConflict?: boolean; - stackIndex?: number; // For handling overlapping schedules -} - -interface DraggableJobBadgeProps { - schedule: Schedule; - onDrag: (scheduleId: string) => void; - onDrop: (scheduleId: string, newPosition: number) => void; - isSelected?: boolean; - onSelect?: () => void; - isPositioned?: boolean; // Whether to use absolute positioning -} - -export const DraggableJobBadge: React.FC = ({ - schedule, - onDrag, - onDrop, - isSelected = false, - onSelect, - isPositioned = true -}) => { - const [isDragging, setIsDragging] = React.useState(false); - - const handleDragStart = (e: React.DragEvent) => { - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('scheduleId', schedule.id); - setIsDragging(true); - onDrag(schedule.id); - }; - - const handleDragEnd = () => { - setIsDragging(false); - }; - - // Calculate position and width based on TIME_BLOCK_WIDTH - const leftPosition = schedule.position * TIME_BLOCK_WIDTH; - const width = schedule.duration * TIME_BLOCK_WIDTH; - - // Vertical offset for stacked schedules - const topOffset = schedule.stackIndex ? schedule.stackIndex * 36 : 4; - - // Determine source and destination icons - const getSourceIcon = () => { - if (schedule.syncType === 'user_sync' || schedule.title === 'User Sync') { - return '../../assets/integrations/onelogin_favicon.png'; - } - return '../../assets/integrations/sage_favicon.png'; - }; - - const getDestinationIcon = () => { - return '../../assets/integrations/fyle_favicon.png'; - }; - - return ( - - - -
- -
- {/* Source icon */} -
- Source { - e.currentTarget.style.display = 'none'; - }} - /> -
- - {/* Arrow */} - - - {/* Destination icon */} -
- Fyle { - e.currentTarget.style.display = 'none'; - }} - /> -
- - {/* Title */} - {schedule.title} - - {schedule.hasConflict && ( - - )} -
-
-
-
- -

{schedule.entity.toUpperCase()} - {schedule.title}

-

- {schedule.syncType === 'user_sync' ? 'OneLogin → Fyle' : 'Sage → Fyle'} -

-
-
-
- ); -}; - -// Simplified badge preview for drag operations -interface JobBadgePreviewProps { - id: string; - title: string; - emoji: string; - color?: string; - enabled: boolean; - hasConflict?: boolean; -} - -export const JobBadgePreview: React.FC = ({ - id, - title, - emoji, - color, - enabled, - hasConflict -}) => { - return ( - - {emoji} - {title} - {hasConflict && ( - - )} - - ); -}; diff --git a/visual-schedule-manager-portfolio/components/schedules/DroppableTimeSlot.tsx b/visual-schedule-manager-portfolio/components/schedules/DroppableTimeSlot.tsx deleted file mode 100644 index 42c2c32..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/DroppableTimeSlot.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { useDroppable } from '@dnd-kit/core'; -import { cn } from '../../lib/utils'; - -interface DroppableTimeSlotProps { - slotId: string; - position: number; // 0-95 - isOccupied: boolean; -} - -export const DroppableTimeSlot: React.FC = ({ - slotId, - position, - isOccupied, -}) => { - const { - setNodeRef, - isOver, - } = useDroppable({ - id: slotId, - data: { - position, - isOccupied, - }, - }); - - return ( -
- {/* Visual indicator when hovering */} - {isOver && ( -
-
-
- )} - - {/* Accessibility - screen reader only content */} - - Drop zone for {formatTimeFromPosition(position)} - {isOccupied && ' (will be queued)'} - -
- ); -}; - -// Helper function to format time from position -const formatTimeFromPosition = (position: number): string => { - const hours = Math.floor(position / 4); - const minutes = (position % 4) * 15; - - const formattedHours = hours.toString().padStart(2, '0'); - const formattedMinutes = minutes.toString().padStart(2, '0'); - - return `${formattedHours}:${formattedMinutes}`; -}; - -// Component to render all drop zones for a timeline -interface TimelineDropZonesProps { - occupiedPositions: Set; - className?: string; -} - -export const TimelineDropZones: React.FC = ({ - occupiedPositions, - className, -}) => { - return ( -
- {Array.from({ length: 96 }, (_, index) => ( - - ))} -
- ); -}; \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/schedules/README.md b/visual-schedule-manager-portfolio/components/schedules/README.md deleted file mode 100644 index 5bfa770..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/README.md +++ /dev/null @@ -1,259 +0,0 @@ -# Drag & Drop System for Visual Scheduler - -This directory contains the drag-and-drop functionality for the visual scheduler, built by Agent B. The system provides horizontal-only dragging with grid snapping, collision detection, and queue warnings. - -## Components - -### `DraggableJobBadge.tsx` -The main draggable component representing sync jobs. - -```typescript -interface DraggableJobBadgeProps { - id: string; - title: string; - emoji: string; - color: string; // Tailwind color class (e.g., 'bg-blue-500') - enabled: boolean; - position: number; // 0-95 (15-minute blocks) - hasConflict?: boolean; -} -``` - -**Usage:** -```tsx - -``` - -**Visual States:** -- **Normal**: Full opacity with move cursor -- **Dragging**: 50% opacity with scale transformation -- **Disabled**: Gray background, no cursor interaction -- **Conflict**: Orange border with warning indicator - -### `DroppableTimeSlot.tsx` -Individual drop zones for 15-minute time blocks. - -```typescript -interface DroppableTimeSlotProps { - slotId: string; - position: number; // 0-95 - isOccupied: boolean; -} -``` - -**TimelineDropZones Component:** -Renders all 96 drop zones for a complete timeline. - -```tsx - -``` - -**Visual Feedback:** -- **Available slot**: Green background on drag over -- **Occupied slot**: Orange background on drag over (queue warning) - -### `DragPreview.tsx` -Custom drag preview components for enhanced UX. - -**SimpleDragPreview**: Basic preview for DragOverlay -```tsx - - {activeItem ? : null} - -``` - -**EnhancedDragPreview**: Advanced preview with queue information -```tsx - -``` - -## Hook: `useDragAndDrop` - -The main hook that provides drag-and-drop functionality using @dnd-kit. - -```typescript -const { - dndContextProps, - activeItem, - activeId, - isDragging, - dragOverPosition, - DndContext, - DragOverlay, -} = useDragAndDrop({ - onDragStart: (item) => console.log('Started dragging:', item), - onDragEnd: (result) => handleDragEnd(result), - snapToGrid: true, - gridSize: 20, // pixels -}); -``` - -**DragEndResult Interface:** -```typescript -interface DragEndResult { - itemId: string; - oldPosition: number; - newPosition: number; - willQueue: boolean; - queuedAfter?: string[]; -} -``` - -## Collision Detection Utilities - -### `checkCollision()` -```typescript -checkCollision( - newPosition: number, - duration: number, - existingSlots: TimeSlot[], - excludeId?: string -): boolean -``` - -### `getQueueInfo()` -```typescript -getQueueInfo( - position: number, - existingSlots: TimeSlot[] -): { willQueue: boolean; queuedAfter?: string[] } -``` - -### `findNextAvailableSlot()` -```typescript -findNextAvailableSlot( - startPosition: number, - duration: number, - existingSlots: TimeSlot[] -): number -``` - -### `calculateEstimatedStartTime()` -```typescript -calculateEstimatedStartTime( - position: number, - duration: number, - existingSlots: TimeSlot[] -): number -``` - -## Integration Example - -```tsx -'use client'; -import React, { useState } from 'react'; -import { - useDragAndDrop, - DraggableJobBadge, - TimelineDropZones, - SimpleDragPreview, - checkCollision, - getQueueInfo, - type DragEndResult, - type TimeSlot, -} from '@/components/settings/schedules'; - -export default function ScheduleTimeline() { - const [jobs, setJobs] = useState([ - { id: '1', title: 'USA Projects', emoji: '🇺🇸', position: 8, enabled: true }, - { id: '2', title: 'ADGM Users', emoji: '🇦🇪', position: 16, enabled: true }, - ]); - - const handleDragEnd = (result: DragEndResult) => { - setJobs(prev => prev.map(job => - job.id === result.itemId - ? { ...job, position: result.newPosition } - : job - )); - }; - - const { dndContextProps, activeItem, DndContext, DragOverlay } = useDragAndDrop({ - onDragEnd: handleDragEnd, - snapToGrid: true, - gridSize: 20, - }); - - const occupiedPositions = new Set(jobs.map(job => job.position)); - - return ( - -
- - - {jobs.map(job => ( - - ))} -
- - - {activeItem ? : null} - -
- ); -} -``` - -## Position System - -The system uses a 96-block timeline representing 24 hours: -- **Position 0**: 00:00 (midnight) -- **Position 4**: 01:00 -- **Position 8**: 02:00 -- **Position 95**: 23:45 - -Each position represents a 15-minute block. - -## Dependencies - -- `@dnd-kit/core`: Core drag-and-drop functionality -- `@dnd-kit/modifiers`: Horizontal axis restriction -- `@dnd-kit/utilities`: Additional utilities - -## Test Page - -Access the test harness at: `/settings/schedules/test-dnd` - -The test page includes: -- Sample draggable job badges -- Visual timeline with grid lines -- Drop zone feedback -- Collision detection logging -- Real-time position updates - -## Integration Points - -This system is designed to integrate with: -- **Agent A**: Timeline grid components -- **Agent C**: Timezone state management -- **Agent D**: Backend API for persistence -- **Agent E**: Main scheduler container - -The drag-and-drop system is framework-agnostic and can be easily integrated into any timeline layout provided by Agent A. \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/schedules/ScheduleDetailsModal.tsx b/visual-schedule-manager-portfolio/components/schedules/ScheduleDetailsModal.tsx deleted file mode 100644 index 8ee062e..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/ScheduleDetailsModal.tsx +++ /dev/null @@ -1,281 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '../ui/dialog'; -import { Button } from '../ui/button'; -import { Badge } from '../ui/badge'; -import { Switch } from '../ui/switch'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; -import { Separator } from '../ui/separator'; -import { Clock, Settings, Trash2, Calendar, AlertTriangle } from 'lucide-react'; -import { motion } from 'framer-motion'; -import { toast } from 'sonner'; - -interface Schedule { - id: string; - title: string; - entity: 'usa' | 'europe' | 'asia'; - syncType: 'project' | 'user'; - emoji: string; - startTimeUTC?: string; - duration: number; - enabled: boolean; - frequency: 'daily' | 'manual'; - position?: number; - hasConflict?: boolean; -} - -interface ScheduleDetailsModalProps { - schedule: Schedule | null; - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function ScheduleDetailsModal({ schedule, open, onOpenChange }: ScheduleDetailsModalProps) { - const [isEditing, setIsEditing] = useState(false); - const [editedSchedule, setEditedSchedule] = useState>({}); - - // Initialize edited schedule when modal opens - React.useEffect(() => { - if (schedule && open) { - setEditedSchedule(schedule); - setIsEditing(false); - } - }, [schedule, open]); - - if (!schedule) return null; - - const handleSave = async () => { - try { - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 500)); - - toast.success('Schedule updated successfully'); - setIsEditing(false); - onOpenChange(false); - } catch (error) { - toast.error('Failed to update schedule'); - } - }; - - const handleDelete = async () => { - try { - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 500)); - - toast.success('Schedule deleted successfully'); - onOpenChange(false); - } catch (error) { - toast.error('Failed to delete schedule'); - } - }; - - const formatTime = (timeUTC?: string) => { - if (!timeUTC) return 'Not scheduled'; - const [hours, minutes] = timeUTC.split(':').map(Number); - const date = new Date(); - date.setUTCHours(hours, minutes, 0, 0); - return date.toLocaleString('en-US', { - hour: 'numeric', - minute: '2-digit', - hour12: true, - timeZone: 'UTC' - }) + ' UTC'; - }; - - const getStatusBadge = () => { - if (!schedule.enabled) { - return Disabled; - } - if (schedule.hasConflict) { - return Conflict; - } - return Active; - }; - - return ( - - - - - {schedule.emoji} -
-
{schedule.title}
-
- {schedule.entity.toUpperCase()} • {schedule.syncType} sync -
-
-
-
- -
- {/* Status */} -
- Status - {getStatusBadge()} -
- - {/* Conflict Warning */} - {schedule.hasConflict && ( - -
- -
-
- Schedule Conflict -
-
- This sync will be queued after other jobs at the same time. -
-
-
-
- )} - - - - {/* Schedule Details */} -
-
-
- -
- - setEditedSchedule(prev => ({ ...prev, enabled: checked })) - } - disabled={!isEditing} - /> -
-
- -
- -
- {isEditing ? ( - - ) : ( -
- {schedule.frequency} -
- )} -
-
-
- -
- -
- {formatTime(schedule.startTimeUTC)} -
-
- -
- -
- ~{schedule.duration} minutes -
-
- -
- -
- Block {schedule.position || 0} of 96 (15-minute intervals) -
-
-
- - - - {/* Actions */} -
-
- - - Drag on timeline to reschedule - -
- -
- - - Use Auto-Schedule for optimization - -
-
-
- - - - -
- {isEditing ? ( - <> - - - - ) : ( - <> - - - - )} -
-
-
-
- ); -} \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/schedules/SchedulerErrorBoundary.tsx b/visual-schedule-manager-portfolio/components/schedules/SchedulerErrorBoundary.tsx deleted file mode 100644 index 6664255..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/SchedulerErrorBoundary.tsx +++ /dev/null @@ -1,79 +0,0 @@ -'use client'; - -import React from 'react'; -import { Alert, AlertDescription, AlertTitle } from '../ui/alert'; -import { Button } from '../ui/button'; -import { AlertTriangle, RefreshCw } from 'lucide-react'; - -interface SchedulerErrorBoundaryProps { - children: React.ReactNode; - fallback?: React.ReactNode; -} - -interface ErrorBoundaryState { - hasError: boolean; - error?: Error; -} - -export class SchedulerErrorBoundary extends React.Component< - SchedulerErrorBoundaryProps, - ErrorBoundaryState -> { - constructor(props: SchedulerErrorBoundaryProps) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { hasError: true, error }; - } - - componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - console.error('Scheduler Error Boundary caught an error:', error, errorInfo); - } - - handleRetry = () => { - this.setState({ hasError: false, error: undefined }); - }; - - render() { - if (this.state.hasError) { - if (this.props.fallback) { - return this.props.fallback; - } - - return ( -
- - - Error Loading Scheduler - -

- Failed to load the visual scheduler. This might be due to a temporary issue. -

-
- - -
-
-
-
- ); - } - - return this.props.children; - } -} \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/schedules/TimelineGrid.tsx b/visual-schedule-manager-portfolio/components/schedules/TimelineGrid.tsx deleted file mode 100644 index d6b162e..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/TimelineGrid.tsx +++ /dev/null @@ -1,125 +0,0 @@ -"use client" - -import React from 'react'; -import { useMemo } from 'react'; -import { ScrollArea, ScrollBar } from '../ui/scroll-area'; -import { CurrentTimeIndicator } from './CurrentTimeIndicator'; -import { BusinessHoursOverlay } from './BusinessHoursOverlay'; -import { getGridColumns, formatTimeForDisplay } from '../../utils/timeline-helpers'; -import { cn } from '../../lib/utils'; - -// Constants for timeline dimensions -const TRACK_LABEL_WIDTH = 96; // Reduced since we removed labels -const TIMELINE_MIN_WIDTH = 2400; // Increased from 1200 to make blocks larger -const TOTAL_TIME_BLOCKS = 96; -const TIME_BLOCK_WIDTH = 25; // Increased from implicit ~12.5px to 25px - -interface TimelineGridProps { - timezone: string; - currentTimeZone?: string; - showBusinessHours?: boolean; - entity?: 'usa' | 'europe' | 'asia'; - children?: React.ReactNode; - className?: string; -} - -export const TimelineGrid: React.FC = ({ - timezone, - currentTimeZone, - showBusinessHours = false, - entity = 'usa', - children, - className -}) => { - // Generate hour markers for 24-hour period - const hourMarkers = Array.from({ length: 24 }, (_, index) => ({ - hour: index, - label: formatTimeForDisplay(index), - position: index * 4 // Each hour spans 4 blocks - })); - - return ( -
- {/* Grid container with fixed labels and scrollable timeline */} -
- {/* Fixed Labels Column */} -
- {/* Schedule header */} -
- Schedule -
- {/* Track labels container - will be filled by TimelineTrack components */} -
- {/* Labels will be absolutely positioned here by TimelineTrack */} -
-
- - {/* Scrollable Timeline */} -
-
- {/* Hour markers header */} -
- {hourMarkers.map(({ hour, label, position }) => ( -
- - {formatTimeForDisplay(hour)} - -
- ))} -
- - {/* Timeline content */} -
- {/* Grid background with 15-minute lines */} -
- {Array.from({ length: TOTAL_TIME_BLOCKS }).map((_, index) => ( -
- ))} -
- - {/* Business hours overlay */} - {showBusinessHours && entity && ( - - )} - - {/* Current time indicator - use currentTimeZone if provided, otherwise fall back to timezone */} - - - {/* Timeline tracks content */} -
- {children} -
-
-
-
-
-
- ); -}; diff --git a/visual-schedule-manager-portfolio/components/schedules/TimelineScheduler.tsx b/visual-schedule-manager-portfolio/components/schedules/TimelineScheduler.tsx deleted file mode 100644 index 1778127..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/TimelineScheduler.tsx +++ /dev/null @@ -1,743 +0,0 @@ -'use client'; - -import React, { useState, useEffect } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; -import { Button } from '../ui/button'; -import { Badge } from '../ui/badge'; -import { Clock, Settings, Calendar } from 'lucide-react'; -import { toast } from 'sonner'; -import { formatInTimeZone } from 'date-fns-tz'; - -// Components -import { TimelineGrid } from './TimelineGrid'; -import { TimelineTrack } from './TimelineTrack'; -import { TimezoneSelector } from './TimezoneSelector'; -import { AutoScheduleDialog } from './AutoScheduleDialog'; -import { BusinessHoursModal } from './BusinessHoursModal'; -import { DraggableJobBadge } from './DraggableJobBadge'; - -// API -import { api } from '../../lib/api/client'; - -interface Schedule { - id: number; - title: string; - emoji: string; - position: number | null; - startTimeUTC: string | null; - duration: number; - enabled: boolean; - frequency: string; - entity: string; - syncType: string; - description?: string; - stackIndex?: number; -} - -interface TimelineSchedulerProps { - className?: string; -} - -export const TimelineScheduler: React.FC = ({ className }) => { - const [schedules, setSchedules] = useState([]); - const [loading, setLoading] = useState(true); - const [autoScheduleOpen, setAutoScheduleOpen] = useState(false); - const [businessHoursOpen, setBusinessHoursOpen] = useState(false); - const [selectedTimezone, setSelectedTimezone] = useState('America/New_York'); - const [dropTarget, setDropTarget] = useState<{ position: number | null }>({ position: null }); - const [draggedScheduleId, setDraggedScheduleId] = useState(null); - - // Auto-detect user's timezone on mount - useEffect(() => { - const detected = Intl.DateTimeFormat().resolvedOptions().timeZone; - if (detected) { - setSelectedTimezone(detected); - } - }, []); - - // Mock data for disabled syncs (for demonstration) - const getMockDisabledSyncs = (entity: string): Schedule[] => { - // Use negative IDs for mock data to avoid conflicts - const baseId = entity === 'usa' ? -1000 : entity === 'europe' ? -2000 : -3000; - return [ - { - id: baseId - 1, - title: 'User Sync', - entity: entity, - syncType: 'user_sync', - emoji: '👤', - enabled: false, - frequency: 'daily', - position: null, - startTimeUTC: null, - duration: 60, - description: 'Mock disabled user sync', - }, - { - id: baseId - 2, - title: 'Project Sync', - entity: entity, - syncType: 'project_sync', - emoji: '📁', - enabled: false, - frequency: 'daily', - position: null, - startTimeUTC: null, - duration: 60, - description: 'Mock disabled project sync', - } - ]; - }; - - // Load schedules - const loadSchedules = async () => { - try { - setLoading(true); - const response = await api.getVisualSchedules({ timezone: selectedTimezone }); - - // Ensure response is an array - if (response.data && Array.isArray(response.data)) { - let schedulesData = response.data.map(s => ({ - ...s, - position: s.position ?? null, - startTimeUTC: s.startTimeUTC ?? null - })); - - // Auto-schedule enabled syncs that don't have positions - schedulesData = autoScheduleEnabledSyncs(schedulesData); - - // For mockup: ensure we have disabled syncs for each entity - const entities = ['usa', 'europe', 'asia']; - entities.forEach(entity => { - const hasDisabledSyncs = schedulesData.some(s => - s.entity === entity && !s.enabled - ); - if (!hasDisabledSyncs) { - // Add mock disabled syncs - schedulesData.push(...getMockDisabledSyncs(entity)); - } - }); - - setSchedules(schedulesData); - } else { - // For mockup: create default disabled syncs - const mockSchedules = [ - ...getMockDisabledSyncs('usa'), - ...getMockDisabledSyncs('europe'), - ...getMockDisabledSyncs('asia') - ]; - setSchedules(mockSchedules); - } - } catch (error: any) { - console.error('Load schedules error:', error); - - // For mockup: use mock data on error - const mockSchedules = [ - ...getMockDisabledSyncs('usa'), - ...getMockDisabledSyncs('europe'), - ...getMockDisabledSyncs('asia') - ]; - setSchedules(mockSchedules); - } finally { - setLoading(false); - } - }; - - // Auto-schedule enabled syncs starting at 2am local time - const autoScheduleEnabledSyncs = (scheduleList: Schedule[]) => { - // Separate schedules by entity - const usaSchedules = scheduleList.filter(s => s.entity === 'usa'); - const europeSchedules = scheduleList.filter(s => s.entity === 'europe'); - const asiaSchedules = scheduleList.filter(s => s.entity === 'asia'); - - // Auto-schedule function for a group of schedules - const scheduleGroup = (group: Schedule[]) => { - return group.map(schedule => { - // Only auto-schedule if enabled and no position set - if (schedule.enabled && schedule.position === null) { - // User Sync at 2am, Project Sync at 3am - const hour = schedule.syncType === 'user_sync' ? 2 : 3; - const newSchedule = { ...schedule, position: hour * 4 }; // Convert hour to position - return newSchedule; - } - return schedule; - }); - }; - - // Schedule syncs for each region - const scheduledUsa = scheduleGroup(usaSchedules); - const scheduledEurope = scheduleGroup(europeSchedules); - const scheduledAsia = scheduleGroup(asiaSchedules); - - return [...scheduledUsa, ...scheduledEurope, ...scheduledAsia]; - }; - - useEffect(() => { - loadSchedules(); - }, [selectedTimezone]); - - // Handle schedule time update - const handleScheduleUpdate = async (scheduleId: number, newPosition: number) => { - try { - const result = await api.updateVisualScheduleTime(scheduleId, { - position: newPosition, - timezone: selectedTimezone - }); - - if (result.data.success) { - toast.success(result.data.queue_info ? result.data.queue_info.message : "Schedule time updated successfully"); - loadSchedules(); - } - } catch (error) { - toast.error('Failed to update schedule'); - } - }; - - // Handle drop from drag and drop - const handleDrop = (trackId: string, position: number) => { - // Get the schedule ID from the drag event - const scheduleId = parseInt(draggedScheduleId || '0'); - if (scheduleId) { - // Enable the sync and set its position - handleEnableSync(scheduleId, position); - setDraggedScheduleId(null); - } - }; - - // Handle enabling a sync and setting its position - const handleEnableSync = async (scheduleId: number, position: number) => { - // For mock data (negative IDs), just update state directly - if (scheduleId < 0) { - setSchedules(prev => prev.map(s => - s.id === scheduleId ? { ...s, enabled: true, position: position } : s - )); - toast.success("Schedule enabled and time updated"); - return; - } - - try { - // First enable the sync, then update its position - const schedule = schedules.find(s => s.id === scheduleId); - if (schedule && !schedule.enabled) { - // Update to enable - await api.toggleVisualSchedule(scheduleId, true); - } - - // Then update position - const result = await api.updateVisualScheduleTime(scheduleId, { - position: position, - timezone: selectedTimezone - }); - - if (result.data.success) { - toast.success("Schedule enabled and time updated"); - loadSchedules(); - } - } catch (error) { - toast.error('Failed to enable schedule'); - } - }; - - // Handle disabling a sync - const handleDisableSync = async (scheduleId: number) => { - // For mock data (negative IDs), just update state directly - if (scheduleId < 0) { - setSchedules(prev => prev.map(s => - s.id === scheduleId ? { ...s, enabled: false, position: null } : s - )); - toast.success("Schedule disabled"); - return; - } - - try { - const result = await api.toggleVisualSchedule(scheduleId, false); - - if (result.data.success) { - toast.success("Schedule disabled"); - loadSchedules(); - } - } catch (error) { - toast.error('Failed to disable schedule'); - } - }; - - // Get current time in different timezones - const getCurrentTimeForTimezone = (tz: string) => { - try { - return formatInTimeZone(new Date(), tz, 'h:mm a zzz'); - } catch { - return 'Invalid timezone'; - } - }; - - // Separate schedules by entity - const usaSchedules = schedules.filter(s => s.entity === 'usa'); - const europeSchedules = schedules.filter(s => s.entity === 'europe'); - const asiaSchedules = schedules.filter(s => s.entity === 'asia'); - - // Calculate stack indices for overlapping schedules - const calculateStackIndices = (scheduleList: Schedule[]) => { - // Only calculate for enabled schedules - const enabledSchedules = scheduleList.filter(s => s.enabled); - - // Sort by position - const sorted = [...enabledSchedules].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)); - const schedulesWithStack = []; - - for (const schedule of sorted) { - let stackIndex = 0; - - // Check for overlaps with already processed schedules - for (const existing of schedulesWithStack) { - const schedStart = schedule.position ?? 0; - const schedEnd = schedStart + 4; // 1 hour duration - const existStart = existing.position ?? 0; - const existEnd = existStart + 4; - - // Check if they overlap and are on the same stack level - if (schedStart < existEnd && schedEnd > existStart && existing.stackIndex === stackIndex) { - stackIndex++; - } - } - - schedulesWithStack.push({ ...schedule, stackIndex }); - } - - return schedulesWithStack; - }; - - const usaSchedulesWithStack = calculateStackIndices(usaSchedules); - const europeSchedulesWithStack = calculateStackIndices(europeSchedules); - const asiaSchedulesWithStack = calculateStackIndices(asiaSchedules); - - // Calculate max stack depth for track height - const getMaxStackDepth = (scheduleList: any[]) => { - return Math.max(0, ...scheduleList.map(s => s.stackIndex || 0)); - }; - - const usaMaxStack = getMaxStackDepth(usaSchedulesWithStack); - const europeMaxStack = getMaxStackDepth(europeSchedulesWithStack); - const asiaMaxStack = getMaxStackDepth(asiaSchedulesWithStack); - - if (loading) { - return ( -
-
-
- ); - } - - // Show message if no schedules exist - if (schedules.length === 0 && !loading) { - return ( -
-
-
-

Visual Schedule Manager

-

- No schedules found. Click Auto-Schedule to create default schedules. -

-
- - -
- - - -

No Schedules Configured

-

- Get started by using auto-schedule to create optimal sync timings for all regions. -

-
- - { - setAutoScheduleOpen(open); - if (!open) { - loadSchedules(); - } - }} - /> -
- ); - } - - return ( -
- {/* Header Controls */} -
-
-

Visual Schedule Manager

-

- Drag and drop to schedule sync jobs. Times shown in your selected timezone. -

-

- ✓ Syncs on the timeline are enabled and will run at the scheduled time -

-
- -
- - - -
-
- - {/* USA Timeline */} - - -
-
-
- 🇺🇸 - North America Schedule -
- - {/* Disabled USA syncs */} -
{ - e.preventDefault(); - e.currentTarget.classList.add('border-red-400', 'bg-red-50'); - }} - onDragLeave={(e) => { - e.currentTarget.classList.remove('border-red-400', 'bg-red-50'); - }} - onDrop={(e) => { - e.preventDefault(); - e.currentTarget.classList.remove('border-red-400', 'bg-red-50'); - const scheduleId = parseInt(draggedScheduleId || '0'); - if (scheduleId) { - handleDisableSync(scheduleId); - setDraggedScheduleId(null); - } - }} - > - {usaSchedules.filter(s => !s.enabled).map(schedule => ( - { setDraggedScheduleId(id); }} - onDrop={() => {}} - isPositioned={false} - /> - ))} - {usaSchedules.filter(s => !s.enabled).length === 0 && ( -
- - - - Drag syncs here to disable -
- )} -
-
- - - - {getCurrentTimeForTimezone('America/New_York')} - -
-
- - - handleDrop('usa-syncs', position)} - > - {/* Render only enabled USA schedules on the timeline */} - {usaSchedulesWithStack.filter(s => s.enabled).map(schedule => ( - { setDraggedScheduleId(id); }} - onDrop={(id, pos) => handleScheduleUpdate(parseInt(id), pos)} - /> - ))} - - - -
- - {/* Europe Timeline */} - - -
-
-
- 🇬🇧 - Europe Schedule -
- - {/* Disabled Europe syncs */} -
{ - e.preventDefault(); - e.currentTarget.classList.add('border-red-400', 'bg-red-50'); - }} - onDragLeave={(e) => { - e.currentTarget.classList.remove('border-red-400', 'bg-red-50'); - }} - onDrop={(e) => { - e.preventDefault(); - e.currentTarget.classList.remove('border-red-400', 'bg-red-50'); - const scheduleId = parseInt(draggedScheduleId || '0'); - if (scheduleId) { - handleDisableSync(scheduleId); - setDraggedScheduleId(null); - } - }} - > - {europeSchedules.filter((s: Schedule) => !s.enabled).map((schedule: Schedule) => ( - { setDraggedScheduleId(id); }} - onDrop={() => {}} - isPositioned={false} - /> - ))} - {europeSchedules.filter((s: Schedule) => !s.enabled).length === 0 && ( -
- - - - Drag syncs here to disable -
- )} -
-
- - - - {getCurrentTimeForTimezone('Europe/London')} - -
-
- - - handleDrop('europe-syncs', position)} - > - {/* Render only enabled Europe schedules on the timeline */} - {europeSchedulesWithStack.filter((s: any) => s.enabled).map((schedule: any) => ( - { setDraggedScheduleId(id); }} - onDrop={(id, pos) => handleScheduleUpdate(parseInt(id), pos)} - /> - ))} - - - -
- - {/* Asia Timeline */} - - -
-
-
- 🇯🇵 - Asia Schedule -
- - {/* Disabled Asia syncs */} -
{ - e.preventDefault(); - e.currentTarget.classList.add('border-red-400', 'bg-red-50'); - }} - onDragLeave={(e) => { - e.currentTarget.classList.remove('border-red-400', 'bg-red-50'); - }} - onDrop={(e) => { - e.preventDefault(); - e.currentTarget.classList.remove('border-red-400', 'bg-red-50'); - const scheduleId = parseInt(draggedScheduleId || '0'); - if (scheduleId) { - handleDisableSync(scheduleId); - setDraggedScheduleId(null); - } - }} - > - {asiaSchedules.filter((s: Schedule) => !s.enabled).map((schedule: Schedule) => ( - { setDraggedScheduleId(id); }} - onDrop={() => {}} - isPositioned={false} - /> - ))} - {asiaSchedules.filter((s: Schedule) => !s.enabled).length === 0 && ( -
- - - - Drag syncs here to disable -
- )} -
-
- - - - {getCurrentTimeForTimezone('Asia/Tokyo')} - -
-
- - - handleDrop('asia-syncs', position)} - > - {/* Render only enabled Asia schedules on the timeline */} - {asiaSchedulesWithStack.filter((s: any) => s.enabled).map((schedule: any) => ( - { setDraggedScheduleId(id); }} - onDrop={(id, pos) => handleScheduleUpdate(parseInt(id), pos)} - /> - ))} - - - -
- - {/* Modals */} - { - setAutoScheduleOpen(open); - if (!open) { - // Reload schedules when dialog closes - loadSchedules(); - } - }} - /> - - -
- ); -}; \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/schedules/TimelineTrack.tsx b/visual-schedule-manager-portfolio/components/schedules/TimelineTrack.tsx deleted file mode 100644 index eee04ed..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/TimelineTrack.tsx +++ /dev/null @@ -1,102 +0,0 @@ -"use client" - -import React, { useEffect, useRef } from 'react'; -import { cn } from '../../lib/utils'; -import { TIME_BLOCK_WIDTH } from '../../utils/timeline-helpers'; - -const TRACK_LABEL_WIDTH = 96; // Reduced since we removed labels - -interface TimelineTrackProps { - label: string; - emoji: string; - trackId: string; - children?: React.ReactNode; - className?: string; - maxStackDepth?: number; - onDrop?: (position: number) => void; -} - -export const TimelineTrack: React.FC = ({ - label, - emoji, - trackId, - children, - className, - maxStackDepth = 0, - onDrop -}) => { - const trackRef = useRef(null); - const labelRef = useRef(null); - - // Calculate height based on stack depth - // Base height is 40px (32px badge + 8px padding), add 36px for each additional stack level - const trackHeight = 40 + (maxStackDepth * 36); - const minHeight = `${trackHeight}px`; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - - if (!trackRef.current || !onDrop) return; - - // Get the position where the item was dropped - const rect = trackRef.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const position = Math.floor(x / TIME_BLOCK_WIDTH); - - // Clamp position to valid range (0-95) - const clampedPosition = Math.max(0, Math.min(95, position)); - - onDrop(clampedPosition); - }; - - useEffect(() => { - // Position the label in the fixed column - if (trackRef.current && labelRef.current) { - const trackElement = trackRef.current; - const labelElement = labelRef.current; - - // Find the labels container - const labelsContainer = trackElement.closest('.grid')?.querySelector('.timeline-track-labels'); - if (labelsContainer && !labelsContainer.contains(labelElement)) { - labelsContainer.appendChild(labelElement); - } - } - }, []); - - return ( - <> - {/* Track label - rendered into the fixed column */} -
-
- {emoji && {emoji}} - {label && {label}} -
-
- - {/* Track content - rendered in the scrollable area */} -
- {children} -
- - ); -}; diff --git a/visual-schedule-manager-portfolio/components/schedules/TimezoneSelector.tsx b/visual-schedule-manager-portfolio/components/schedules/TimezoneSelector.tsx deleted file mode 100644 index 64ef775..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/TimezoneSelector.tsx +++ /dev/null @@ -1,144 +0,0 @@ -'use client'; - -import { useMemo } from 'react'; -import { Clock } from 'lucide-react'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGroup, SelectLabel } from '../ui/select'; -import { Badge } from '../ui/badge'; -import { cn } from '../../lib/utils'; - -export interface TimezoneSelectorProps { - value: string; - onChange: (timezone: string) => void; - className?: string; -} - -interface TimezoneOption { - value: string; - label: string; - abbreviation: string; - region: 'usa' | 'europe' | 'asia'; -} - -const TIMEZONE_GROUPS = { - usa: [ - { value: 'America/New_York', label: 'Eastern Time', abbreviation: 'EST/EDT' }, - { value: 'America/Chicago', label: 'Central Time', abbreviation: 'CST/CDT' }, - { value: 'America/Los_Angeles', label: 'Pacific Time', abbreviation: 'PST/PDT' }, - ], - europe: [ - { value: 'Europe/London', label: 'London', abbreviation: 'GMT/BST' }, - ], - asia: [ - { value: 'Asia/Tokyo', label: 'Tokyo', abbreviation: 'JST' }, - ], -}; - -export const TimezoneSelector = ({ - value, - onChange, - className -}: TimezoneSelectorProps) => { - - // Build the timezone options list with regions - const timezoneOptions = useMemo(() => { - const options: TimezoneOption[] = []; - - // Add USA timezones - TIMEZONE_GROUPS.usa.forEach(option => { - options.push({ - ...option, - region: 'usa' - }); - }); - - // Add Europe timezones - TIMEZONE_GROUPS.europe.forEach(option => { - options.push({ - ...option, - region: 'europe' - }); - }); - - // Add Asia timezones - TIMEZONE_GROUPS.asia.forEach(option => { - options.push({ - ...option, - region: 'asia' - }); - }); - - return options; - }, []); - - // Group options by region - const groupedOptions = useMemo(() => { - const groups: Record = { - usa: [], - europe: [], - asia: [] - }; - - timezoneOptions.forEach(option => { - groups[option.region].push(option); - }); - - return groups; - }, [timezoneOptions]); - - // Get current timezone display info - const currentTimezone = useMemo(() => { - return timezoneOptions.find(option => option.value === value); - }, [timezoneOptions, value]); - - const getCategoryLabel = (category: string) => { - switch (category) { - case 'usa': - return '🇺🇸 North America'; - case 'europe': - return '🇬🇧 Europe'; - case 'asia': - return '🇯🇵 Asia'; - default: - return category; - } - }; - - return ( -
- - -
- ); -}; diff --git a/visual-schedule-manager-portfolio/components/schedules/index.ts b/visual-schedule-manager-portfolio/components/schedules/index.ts deleted file mode 100644 index 55d2200..0000000 --- a/visual-schedule-manager-portfolio/components/schedules/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Export all schedule-related components -export { TimelineScheduler } from './TimelineScheduler'; -export { TimelineGrid } from './TimelineGrid'; -export { TimelineTrack } from './TimelineTrack'; -export { DraggableJobBadge } from './DraggableJobBadge'; -export { DroppableTimeSlot } from './DroppableTimeSlot'; -export { CurrentTimeIndicator } from './CurrentTimeIndicator'; -export { BusinessHoursOverlay } from './BusinessHoursOverlay'; -export { TimezoneSelector } from './TimezoneSelector'; -export { AutoScheduleDialog } from './AutoScheduleDialog'; -export { ScheduleDetailsModal } from './ScheduleDetailsModal'; -export { DragPreview } from './DragPreview'; -export { SchedulerErrorBoundary } from './SchedulerErrorBoundary'; -export { BusinessHoursModal } from './BusinessHoursModal'; - -// Export types if needed -export type { TimezoneSelectorProps } from './TimezoneSelector'; diff --git a/visual-schedule-manager-portfolio/components/ui/alert.tsx b/visual-schedule-manager-portfolio/components/ui/alert.tsx deleted file mode 100644 index 8ee604f..0000000 --- a/visual-schedule-manager-portfolio/components/ui/alert.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" - -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" - -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" - -export { Alert, AlertTitle, AlertDescription } \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/ui/badge.tsx b/visual-schedule-manager-portfolio/components/ui/badge.tsx deleted file mode 100644 index 0205413..0000000 --- a/visual-schedule-manager-portfolio/components/ui/badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" - - return ( - - ) -} - -export { Badge, badgeVariants } diff --git a/visual-schedule-manager-portfolio/components/ui/button.tsx b/visual-schedule-manager-portfolio/components/ui/button.tsx deleted file mode 100644 index a2df8dc..0000000 --- a/visual-schedule-manager-portfolio/components/ui/button.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { - const Comp = asChild ? Slot : "button" - - return ( - - ) -} - -export { Button, buttonVariants } diff --git a/visual-schedule-manager-portfolio/components/ui/card.tsx b/visual-schedule-manager-portfolio/components/ui/card.tsx deleted file mode 100644 index d05bbc6..0000000 --- a/visual-schedule-manager-portfolio/components/ui/card.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -function Card({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} diff --git a/visual-schedule-manager-portfolio/components/ui/dialog.tsx b/visual-schedule-manager-portfolio/components/ui/dialog.tsx deleted file mode 100644 index d9ccec9..0000000 --- a/visual-schedule-manager-portfolio/components/ui/dialog.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client" - -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Dialog({ - ...props -}: React.ComponentProps) { - return -} - -function DialogTrigger({ - ...props -}: React.ComponentProps) { - return -} - -function DialogPortal({ - ...props -}: React.ComponentProps) { - return -} - -function DialogClose({ - ...props -}: React.ComponentProps) { - return -} - -function DialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: React.ComponentProps & { - showCloseButton?: boolean -}) { - return ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - - ) -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function DialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function DialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -} diff --git a/visual-schedule-manager-portfolio/components/ui/label.tsx b/visual-schedule-manager-portfolio/components/ui/label.tsx deleted file mode 100644 index 0f258cf..0000000 --- a/visual-schedule-manager-portfolio/components/ui/label.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client" - -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" -) - -const Label = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, ...props }, ref) => ( - -)) -Label.displayName = LabelPrimitive.Root.displayName - -export { Label } \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/ui/scroll-area.tsx b/visual-schedule-manager-portfolio/components/ui/scroll-area.tsx deleted file mode 100644 index 8e4fa13..0000000 --- a/visual-schedule-manager-portfolio/components/ui/scroll-area.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" - -import * as React from "react" -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" - -import { cn } from "@/lib/utils" - -function ScrollArea({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - {children} - - - - - ) -} - -function ScrollBar({ - className, - orientation = "vertical", - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -export { ScrollArea, ScrollBar } diff --git a/visual-schedule-manager-portfolio/components/ui/select.tsx b/visual-schedule-manager-portfolio/components/ui/select.tsx deleted file mode 100644 index dcbbc0c..0000000 --- a/visual-schedule-manager-portfolio/components/ui/select.tsx +++ /dev/null @@ -1,185 +0,0 @@ -"use client" - -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Select({ - ...props -}: React.ComponentProps) { - return -} - -function SelectGroup({ - ...props -}: React.ComponentProps) { - return -} - -function SelectValue({ - ...props -}: React.ComponentProps) { - return -} - -function SelectTrigger({ - className, - size = "default", - children, - ...props -}: React.ComponentProps & { - size?: "sm" | "default" -}) { - return ( - - {children} - - - - - ) -} - -function SelectContent({ - className, - children, - position = "popper", - ...props -}: React.ComponentProps) { - return ( - - - - - {children} - - - - - ) -} - -function SelectLabel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function SelectItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ) -} - -function SelectSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -} diff --git a/visual-schedule-manager-portfolio/components/ui/separator.tsx b/visual-schedule-manager-portfolio/components/ui/separator.tsx deleted file mode 100644 index 275381c..0000000 --- a/visual-schedule-manager-portfolio/components/ui/separator.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client" - -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" - -import { cn } from "@/lib/utils" - -function Separator({ - className, - orientation = "horizontal", - decorative = true, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { Separator } diff --git a/visual-schedule-manager-portfolio/components/ui/switch.tsx b/visual-schedule-manager-portfolio/components/ui/switch.tsx deleted file mode 100644 index bc4a038..0000000 --- a/visual-schedule-manager-portfolio/components/ui/switch.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client" - -import * as React from "react" -import * as SwitchPrimitives from "@radix-ui/react-switch" - -import { cn } from "@/lib/utils" - -const Switch = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)) -Switch.displayName = SwitchPrimitives.Root.displayName - -export { Switch } \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/components/ui/tooltip.tsx b/visual-schedule-manager-portfolio/components/ui/tooltip.tsx deleted file mode 100644 index 4ee26b3..0000000 --- a/visual-schedule-manager-portfolio/components/ui/tooltip.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client" - -import * as React from "react" -import * as TooltipPrimitive from "@radix-ui/react-tooltip" - -import { cn } from "@/lib/utils" - -function TooltipProvider({ - delayDuration = 0, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function Tooltip({ - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -function TooltipTrigger({ - ...props -}: React.ComponentProps) { - return -} - -function TooltipContent({ - className, - sideOffset = 0, - children, - ...props -}: React.ComponentProps) { - return ( - - - {children} - - - - ) -} - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/visual-schedule-manager-portfolio/demo.tsx b/visual-schedule-manager-portfolio/demo.tsx deleted file mode 100644 index 3f477c6..0000000 --- a/visual-schedule-manager-portfolio/demo.tsx +++ /dev/null @@ -1,84 +0,0 @@ -'use client'; - -import React from 'react'; -import { TimelineScheduler } from './components/schedules/TimelineScheduler'; -import { SchedulerErrorBoundary } from './components/schedules/SchedulerErrorBoundary'; - -export default function VisualScheduleManagerDemo() { - return ( -
-
- {/* Header */} -
-

- Visual Schedule Manager -

-

- Drag-and-drop interface for managing global sync schedules across timezones -

-
- - {/* Feature highlights */} -
-
-
- 🌍 -

Multi-Region Support

-
-

- Manage schedules for USA, Europe, and Asia with automatic timezone conversion -

-
- -
-
- ✨ -

Drag & Drop

-
-

- Intuitive interface to enable, disable, and reschedule sync jobs -

-
- -
-
- 🤖 -

Auto-Schedule

-
-

- Intelligent optimization to avoid business hours and prevent conflicts -

-
-
- - {/* Main scheduler component */} -
- - - -
- - {/* Demo instructions */} -
-

- Demo Instructions -

-
    -
  • • Drag sync jobs from the disabled area onto the timeline to schedule them
  • -
  • • Drag scheduled jobs back to the disabled area to disable them
  • -
  • • Drag scheduled jobs to different time slots to reschedule
  • -
  • • Use the timezone selector to view schedules in different timezones
  • -
  • • Click "Auto-Schedule" to optimize all sync timings automatically
  • -
  • • Toggle "Business Hours" to see work hours overlay for each region
  • -
-
- - {/* Technical details */} -
-

Built with React, TypeScript, Tailwind CSS, and Framer Motion

-

Features: Drag & Drop, Timezone Management, Collision Detection, Auto-scheduling

-
-
-
- ); -} \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/hooks/useDragAndDrop.ts b/visual-schedule-manager-portfolio/hooks/useDragAndDrop.ts deleted file mode 100644 index 841987e..0000000 --- a/visual-schedule-manager-portfolio/hooks/useDragAndDrop.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { useState, useCallback } from 'react'; -import { - DndContext, - DragEndEvent, - DragStartEvent, - DragOverEvent, - useSensor, - useSensors, - PointerSensor, - KeyboardSensor, - closestCenter, - DragOverlay, -} from '@dnd-kit/core'; -import { - restrictToHorizontalAxis, -} from '@dnd-kit/modifiers'; - -export interface DragItem { - id: string; - title: string; - emoji: string; - color: string; - position: number; - enabled: boolean; - hasConflict?: boolean; -} - -export interface DragEndResult { - itemId: string; - oldPosition: number; - newPosition: number; - willQueue: boolean; - queuedAfter?: string[]; -} - -interface DragAndDropConfig { - onDragStart?: (item: DragItem) => void; - onDragEnd?: (result: DragEndResult) => void; - snapToGrid?: boolean; - gridSize?: number; // Width of one 15-minute block in pixels -} - -export const useDragAndDrop = (config: DragAndDropConfig) => { - const [activeId, setActiveId] = useState(null); - const [activeItem, setActiveItem] = useState(null); - const [dragOverPosition, setDragOverPosition] = useState(null); - - // Configure sensors for different input methods - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, // Require 8px movement before starting drag - }, - }), - useSensor(KeyboardSensor, { - // Enable keyboard navigation for accessibility - }) - ); - - const handleDragStart = useCallback((event: DragStartEvent) => { - const { active } = event; - const draggedItem = active.data.current as DragItem; - - setActiveId(active.id as string); - setActiveItem(draggedItem); - - config.onDragStart?.(draggedItem); - }, [config]); - - const handleDragOver = useCallback((event: DragOverEvent) => { - const { over } = event; - - if (over) { - // Extract position from droppable slot ID (format: "slot-{position}") - const position = parseInt(over.id.toString().replace('slot-', ''), 10); - setDragOverPosition(position); - } else { - setDragOverPosition(null); - } - }, []); - - const handleDragEnd = useCallback((event: DragEndEvent) => { - const { active, over } = event; - - if (over && activeItem) { - const newPosition = parseInt(over.id.toString().replace('slot-', ''), 10); - const oldPosition = activeItem.position; - - if (newPosition !== oldPosition) { - const result: DragEndResult = { - itemId: active.id as string, - oldPosition, - newPosition, - willQueue: false, // This will be calculated by the consumer - }; - - config.onDragEnd?.(result); - } - } - - // Reset drag state - setActiveId(null); - setActiveItem(null); - setDragOverPosition(null); - }, [activeItem, config]); - - const snapToGrid = useCallback((position: { x: number; y: number }) => { - if (!config.snapToGrid || !config.gridSize) { - return position; - } - - const snappedX = Math.round(position.x / config.gridSize) * config.gridSize; - return { - x: snappedX, - y: position.y, // Keep Y position unchanged - }; - }, [config.snapToGrid, config.gridSize]); - - const isDragging = activeId !== null; - - const dndContextProps = { - sensors, - collisionDetection: closestCenter, - modifiers: [restrictToHorizontalAxis], - onDragStart: handleDragStart, - onDragOver: handleDragOver, - onDragEnd: handleDragEnd, - }; - - return { - // State - activeId, - activeItem, - isDragging, - dragOverPosition, - - // Props for DndContext - dndContextProps, - - // Helper functions - snapToGrid, - - // Components (re-export for convenience) - DndContext, - DragOverlay, - }; -}; \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/hooks/useScheduleTimezone.ts b/visual-schedule-manager-portfolio/hooks/useScheduleTimezone.ts deleted file mode 100644 index c0df306..0000000 --- a/visual-schedule-manager-portfolio/hooks/useScheduleTimezone.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { useMemo, useCallback } from 'react'; -import { useScheduleStore, ScheduleWithPosition, ConflictInfo } from '../stores/schedule-store'; -import { - convertUTCToPosition, - convertPositionToUTC, - formatTimeForDisplay, - getCurrentTimePosition, - getBusinessHours -} from '../utils/timezone-conversion'; - -export const useScheduleTimezone = () => { - const { - selectedTimezone, - schedules, - businessHoursVisible - } = useScheduleStore(); - - // Convert all schedules to display timezone with positions - const schedulesInTimezone = useMemo(() => { - return schedules.map(schedule => { - const position = convertUTCToPosition(schedule.startTimeUTC, selectedTimezone); - const displayTime = formatTimeForDisplay(schedule.startTimeUTC, selectedTimezone); - - return { - ...schedule, - position, - displayTime - } as ScheduleWithPosition; - }); - }, [schedules, selectedTimezone]); - - // Get current time position for the selected timezone - const currentTimePosition = useMemo(() => { - return getCurrentTimePosition(selectedTimezone); - }, [selectedTimezone]); - - // Get business hours for entities in the selected timezone - const businessHours = useMemo(() => { - return { - usa: getBusinessHours('usa', selectedTimezone), - adgm: getBusinessHours('adgm', selectedTimezone) - }; - }, [selectedTimezone]); - - // Helper to convert UTC time to local display time - const convertTimeToLocal = useCallback((timeUTC: string) => { - return formatTimeForDisplay(timeUTC, selectedTimezone); - }, [selectedTimezone]); - - // Helper to convert position to UTC time - const convertPositionToTime = useCallback((position: number) => { - return convertPositionToUTC(position, selectedTimezone); - }, [selectedTimezone]); - - // Helper to convert UTC time to position - const convertTimeToPosition = useCallback((timeUTC: string) => { - return convertUTCToPosition(timeUTC, selectedTimezone); - }, [selectedTimezone]); - - // Get schedules by entity with timezone conversion - const getSchedulesByEntityInTimezone = useCallback((entity?: 'usa' | 'adgm') => { - return schedulesInTimezone.filter(schedule => - !entity || schedule.entity === entity - ); - }, [schedulesInTimezone]); - - // Get enabled schedules in timezone - const enabledSchedulesInTimezone = useMemo(() => { - return schedulesInTimezone.filter(schedule => schedule.enabled); - }, [schedulesInTimezone]); - - // Get conflicts with detailed information - const conflictsInTimezone = useMemo(() => { - const conflicts: ConflictInfo[] = []; - const positionGroups: { [position: number]: ScheduleWithPosition[] } = {}; - - // Group enabled schedules by position - enabledSchedulesInTimezone.forEach(schedule => { - const position = schedule.position; - if (!positionGroups[position]) { - positionGroups[position] = []; - } - positionGroups[position].push(schedule); - }); - - // Identify conflicts - Object.entries(positionGroups).forEach(([, schedules]) => { - if (schedules.length > 1) { - // Sort by priority (users first, then by entity) - const sortedSchedules = [...schedules].sort((a, b) => { - if (a.syncType !== b.syncType) { - return a.syncType === 'user' ? -1 : 1; - } - return a.entity.localeCompare(b.entity); - }); - - sortedSchedules.forEach((schedule, index) => { - if (index > 0) { // First schedule runs immediately, others are queued - conflicts.push({ - scheduleId: schedule.id, - conflictsWith: sortedSchedules.slice(0, index).map(s => s.id), - queuePosition: index + 1, - estimatedDelay: sortedSchedules.slice(0, index).reduce((sum, s) => sum + s.duration, 0) - }); - } - }); - } - }); - - return conflicts; - }, [enabledSchedulesInTimezone]); - - // Check if a specific time position has conflicts - const hasConflictAtPosition = useCallback((position: number, excludeId?: string) => { - const schedulesAtPosition = enabledSchedulesInTimezone.filter( - schedule => schedule.position === position && schedule.id !== excludeId - ); - return schedulesAtPosition.length > 0; - }, [enabledSchedulesInTimezone]); - - // Get queue information for a position - const getQueueInfoForPosition = useCallback((position: number, excludeId?: string) => { - const schedulesAtPosition = enabledSchedulesInTimezone.filter( - schedule => schedule.position === position && schedule.id !== excludeId - ); - - if (schedulesAtPosition.length === 0) { - return { willQueue: false }; - } - - return { - willQueue: true, - queuedAfter: schedulesAtPosition.map(s => s.title), - estimatedDelay: schedulesAtPosition.reduce((sum, s) => sum + s.duration, 0), - queuePosition: schedulesAtPosition.length + 1 - }; - }, [enabledSchedulesInTimezone]); - - // Check if a time is during business hours - const isBusinessHours = useCallback((position: number, entity: 'usa' | 'adgm') => { - const entityBusinessHours = businessHours[entity]; - return position >= entityBusinessHours.start && position <= entityBusinessHours.end; - }, [businessHours]); - - // Get next available position after a given position - const getNextAvailablePosition = useCallback((startPosition: number, entity?: 'usa' | 'adgm') => { - const position = startPosition; - - // Check up to 24 hours ahead - for (let i = 0; i < 96; i++) { - const checkPosition = (position + i) % 96; - - if (!hasConflictAtPosition(checkPosition)) { - // If entity is specified, prefer non-business hours - if (entity && isBusinessHours(checkPosition, entity)) { - continue; - } - return checkPosition; - } - } - - // Fallback: return a position even if it has conflicts - return (startPosition + 1) % 96; - }, [hasConflictAtPosition, isBusinessHours]); - - // Format time for display with timezone - const formatScheduleTime = useCallback((schedule: ScheduleWithPosition) => { - return { - utc: schedule.startTimeUTC, - local: schedule.displayTime, - position: schedule.position - }; - }, []); - - // Get timeline metadata for rendering - const timelineMetadata = useMemo(() => { - return { - timezone: selectedTimezone, - currentTimePosition, - businessHours, - businessHoursVisible, - totalSchedules: schedules.length, - enabledSchedules: enabledSchedulesInTimezone.length, - conflicts: conflictsInTimezone.length - }; - }, [ - selectedTimezone, - currentTimePosition, - businessHours, - businessHoursVisible, - schedules.length, - enabledSchedulesInTimezone.length, - conflictsInTimezone.length - ]); - - return { - // Core data - timezone: selectedTimezone, - schedules: schedulesInTimezone, - enabledSchedules: enabledSchedulesInTimezone, - conflicts: conflictsInTimezone, - - // Metadata - currentTimePosition, - businessHours, - timelineMetadata, - - // Helper functions - convertTimeToLocal, - convertPositionToTime, - convertTimeToPosition, - getSchedulesByEntityInTimezone, - hasConflictAtPosition, - getQueueInfoForPosition, - isBusinessHours, - getNextAvailablePosition, - formatScheduleTime, - - // Computed properties - hasConflicts: conflictsInTimezone.length > 0, - hasEnabledSchedules: enabledSchedulesInTimezone.length > 0 - }; -}; diff --git a/visual-schedule-manager-portfolio/lib/api/client.ts b/visual-schedule-manager-portfolio/lib/api/client.ts deleted file mode 100644 index ec40515..0000000 --- a/visual-schedule-manager-portfolio/lib/api/client.ts +++ /dev/null @@ -1,181 +0,0 @@ -// Mock API client for portfolio demo -// Simulates API calls without making real network requests - -interface VisualSchedule { - id: number; - title: string; - emoji: string; - position: number | null; - startTimeUTC: string | null; - duration: number; - enabled: boolean; - frequency: string; - entity: string; - syncType: string; - description?: string; -} - -// Mock data storage -let mockSchedules: VisualSchedule[] = [ - // USA schedules - { - id: 1, - title: 'User Sync', - emoji: '👤', - entity: 'usa', - syncType: 'user_sync', - enabled: true, - frequency: 'daily', - position: 8, // 2 AM = 2 * 4 blocks - startTimeUTC: '2024-01-01T07:00:00Z', - duration: 60, - description: 'Sync users from OneLogin to Fyle', - }, - { - id: 2, - title: 'Project Sync', - emoji: '📁', - entity: 'usa', - syncType: 'project_sync', - enabled: true, - frequency: 'daily', - position: 12, // 3 AM = 3 * 4 blocks - startTimeUTC: '2024-01-01T08:00:00Z', - duration: 60, - description: 'Sync projects from Sage to Fyle', - }, - // Europe schedules - { - id: 3, - title: 'User Sync', - emoji: '👤', - entity: 'europe', - syncType: 'user_sync', - enabled: false, - frequency: 'daily', - position: null, - startTimeUTC: null, - duration: 60, - description: 'Sync users from OneLogin to Fyle', - }, - { - id: 4, - title: 'Project Sync', - emoji: '📁', - entity: 'europe', - syncType: 'project_sync', - enabled: false, - frequency: 'daily', - position: null, - startTimeUTC: null, - duration: 60, - description: 'Sync projects from Sage to Fyle', - }, - // Asia schedules - { - id: 5, - title: 'User Sync', - emoji: '👤', - entity: 'asia', - syncType: 'user_sync', - enabled: false, - frequency: 'daily', - position: null, - startTimeUTC: null, - duration: 60, - description: 'Sync users from OneLogin to Fyle', - }, - { - id: 6, - title: 'Project Sync', - emoji: '📁', - entity: 'asia', - syncType: 'project_sync', - enabled: false, - frequency: 'daily', - position: null, - startTimeUTC: null, - duration: 60, - description: 'Sync projects from Sage to Fyle', - }, -]; - -// Helper to simulate async delay -const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -// Mock API client -export const api = { - getVisualSchedules: async ({ timezone }: { timezone: string }) => { - await delay(300); // Simulate network delay - return { - data: [...mockSchedules], - success: true, - }; - }, - - updateVisualScheduleTime: async (scheduleId: number, data: { position: number; timezone: string }) => { - await delay(500); // Simulate network delay - - const schedule = mockSchedules.find(s => s.id === scheduleId); - if (schedule) { - schedule.position = data.position; - // Mock UTC time calculation - const hours = Math.floor(data.position / 4); - const minutes = (data.position % 4) * 15; - schedule.startTimeUTC = `2024-01-01T${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00Z`; - } - - return { - data: { - success: true, - schedule, - queue_info: { - message: `Schedule updated to ${Math.floor(data.position / 4)}:${((data.position % 4) * 15).toString().padStart(2, '0')}`, - }, - }, - }; - }, - - toggleVisualSchedule: async (scheduleId: number, enabled: boolean) => { - await delay(500); // Simulate network delay - - const schedule = mockSchedules.find(s => s.id === scheduleId); - if (schedule) { - schedule.enabled = enabled; - if (!enabled) { - schedule.position = null; - schedule.startTimeUTC = null; - } - } - - return { - data: { - success: true, - schedule, - }, - }; - }, - - autoSchedule: async (data: { timezone: string }) => { - await delay(1000); // Simulate processing time - - // Auto-schedule all syncs - mockSchedules.forEach(schedule => { - if (!schedule.enabled) { - schedule.enabled = true; - // User syncs at 2 AM, project syncs at 3 AM - const hour = schedule.syncType === 'user_sync' ? 2 : 3; - schedule.position = hour * 4; - schedule.startTimeUTC = `2024-01-01T${(hour + 5).toString().padStart(2, '0')}:00:00Z`; // Mock UTC - } - }); - - return { - data: { - success: true, - message: 'All schedules optimized successfully', - schedules: [...mockSchedules], - }, - }; - }, -}; \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/lib/utils.ts b/visual-schedule-manager-portfolio/lib/utils.ts deleted file mode 100644 index fcb6366..0000000 --- a/visual-schedule-manager-portfolio/lib/utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { type ClassValue, clsx } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} - -export function formatUserName(createdBy: string): { display: string; tooltip: string } { - // Handle "Auto" case - if (createdBy === "Auto") { - return { display: "Auto", tooltip: "Automated sync" }; - } - - // Extract name and email from "Name (email)" format - const match = createdBy.match(/^(.+?)\s*\((.+?)\)$/); - - if (match) { - const [, fullName, email] = match; - const nameParts = fullName.trim().split(' '); - - if (nameParts.length >= 2) { - // Get first name and first initial of last name - const firstName = nameParts[0]; - const lastInitial = nameParts[nameParts.length - 1][0]; - return { - display: `${firstName} ${lastInitial}.`, - tooltip: `${fullName} (${email})` - }; - } else { - // Single name - return { - display: fullName, - tooltip: `${fullName} (${email})` - }; - } - } - - // Handle plain name without email - const nameParts = createdBy.trim().split(' '); - if (nameParts.length >= 2) { - const firstName = nameParts[0]; - const lastInitial = nameParts[nameParts.length - 1][0]; - return { - display: `${firstName} ${lastInitial}.`, - tooltip: createdBy - }; - } - - // Fallback - return { display: createdBy, tooltip: createdBy }; -} diff --git a/visual-schedule-manager-portfolio/package.json b/visual-schedule-manager-portfolio/package.json deleted file mode 100644 index 870e722..0000000 --- a/visual-schedule-manager-portfolio/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "name": "visual-schedule-manager", - "version": "1.0.0", - "description": "A drag-and-drop visual scheduler for managing global sync schedules across timezones", - "main": "demo.tsx", - "author": "Your Name", - "license": "MIT", - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "^16.0.0", - "typescript": "^5.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@types/node": "^24.0.0", - "tailwindcss": "^4.0.0", - "clsx": "^2.0.0", - "tailwind-merge": "^3.0.0", - "framer-motion": "^12.0.0", - "lucide-react": "^0.559.0", - "date-fns": "^4.0.0", - "date-fns-tz": "^3.0.0", - "zustand": "^5.0.0", - "sonner": "^2.0.0", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-separator": "^1.0.3", - "@radix-ui/react-scroll-area": "^1.0.5", - "@radix-ui/react-tooltip": "^1.0.7", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-slot": "^1.0.2", - "class-variance-authority": "^0.7.0" - }, - "devDependencies": { - "autoprefixer": "^10.4.16", - "postcss": "^8.4.31" - } -} \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/stores/schedule-store.ts b/visual-schedule-manager-portfolio/stores/schedule-store.ts deleted file mode 100644 index 4b0a6df..0000000 --- a/visual-schedule-manager-portfolio/stores/schedule-store.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { create } from 'zustand'; -import { persist, createJSONStorage } from 'zustand/middleware'; -import { convertUTCToPosition, getBusinessHours } from '../utils/timezone-conversion'; - -export interface Schedule { - id: string; - title: string; - entity: 'usa' | 'adgm'; - syncType: 'project' | 'user'; - emoji: string; - startTimeUTC: string; // HH:mm format - duration: number; // minutes - enabled: boolean; - frequency: 'daily' | 'manual'; -} - -export interface ScheduleWithPosition extends Schedule { - position: number; // 0-95 - displayTime: string; -} - -export interface ConflictInfo { - scheduleId: string; - conflictsWith: string[]; - queuePosition: number; - estimatedDelay: number; -} - -interface ScheduleStore { - // State - schedules: Schedule[]; - selectedTimezone: string; - businessHoursVisible: boolean; - recentTimezones: string[]; - - // Actions - setSchedules: (schedules: Schedule[]) => void; - addSchedule: (schedule: Schedule) => void; - updateSchedule: (id: string, updates: Partial) => void; - updateScheduleTime: (id: string, newTimeUTC: string) => void; - deleteSchedule: (id: string) => void; - setTimezone: (timezone: string) => void; - toggleBusinessHours: () => void; - addRecentTimezone: (timezone: string) => void; - - // Computed - getSchedulePosition: (schedule: Schedule) => number; - getSchedulesWithPositions: () => ScheduleWithPosition[]; - getConflicts: () => ConflictInfo[]; - getSchedulesByEntity: (entity?: 'usa' | 'adgm') => ScheduleWithPosition[]; - getEnabledSchedules: () => ScheduleWithPosition[]; -} - -// Auto-detect user's timezone -const getUserTimezone = (): string => { - try { - return Intl.DateTimeFormat().resolvedOptions().timeZone; - } catch { - return 'UTC'; - } -}; - -export const useScheduleStore = create()( - persist( - (set, get) => ({ - // Initial state - schedules: [], - selectedTimezone: getUserTimezone(), - businessHoursVisible: true, - recentTimezones: [], - - // Actions - setSchedules: (schedules) => set({ schedules }), - - addSchedule: (schedule) => set((state) => ({ - schedules: [...state.schedules, schedule] - })), - - updateSchedule: (id, updates) => set((state) => ({ - schedules: state.schedules.map(schedule => - schedule.id === id ? { ...schedule, ...updates } : schedule - ) - })), - - updateScheduleTime: (id, newTimeUTC) => set((state) => ({ - schedules: state.schedules.map(schedule => - schedule.id === id ? { ...schedule, startTimeUTC: newTimeUTC } : schedule - ) - })), - - deleteSchedule: (id) => set((state) => ({ - schedules: state.schedules.filter(schedule => schedule.id !== id) - })), - - setTimezone: (timezone) => { - const { addRecentTimezone } = get(); - addRecentTimezone(timezone); - set({ selectedTimezone: timezone }); - }, - - toggleBusinessHours: () => set((state) => ({ - businessHoursVisible: !state.businessHoursVisible - })), - - addRecentTimezone: (timezone) => set((state) => { - // Valid timezones for our system - const validTimezones = [ - 'America/New_York', - 'America/Chicago', - 'America/Denver', - 'America/Los_Angeles', - 'Asia/Dubai' - ]; - - // Only add if it's a valid timezone - if (!validTimezones.includes(timezone)) { - return state; - } - - const recentTimezones = [ - timezone, - ...state.recentTimezones.filter(tz => tz !== timezone && validTimezones.includes(tz)) - ].slice(0, 3); // Keep only 3 most recent and valid - - return { recentTimezones }; - }), - - // Computed functions - getSchedulePosition: (schedule) => { - const { selectedTimezone } = get(); - return convertUTCToPosition(schedule.startTimeUTC, selectedTimezone); - }, - - getSchedulesWithPositions: () => { - const { schedules, selectedTimezone } = get(); - return schedules.map(schedule => ({ - ...schedule, - position: convertUTCToPosition(schedule.startTimeUTC, selectedTimezone), - displayTime: schedule.startTimeUTC // This will be formatted by the component - })); - }, - - getConflicts: () => { - const schedulesWithPositions = get().getSchedulesWithPositions(); - const conflicts: ConflictInfo[] = []; - - // Group schedules by position - const positionGroups: { [position: number]: ScheduleWithPosition[] } = {}; - - schedulesWithPositions.forEach(schedule => { - if (!schedule.enabled) return; - - const position = schedule.position; - if (!positionGroups[position]) { - positionGroups[position] = []; - } - positionGroups[position].push(schedule); - }); - - // Find conflicts - Object.entries(positionGroups).forEach(([, schedules]) => { - if (schedules.length > 1) { - schedules.forEach((schedule, index) => { - if (index > 0) { // First schedule runs immediately, others are queued - conflicts.push({ - scheduleId: schedule.id, - conflictsWith: schedules.slice(0, index).map(s => s.id), - queuePosition: index + 1, - estimatedDelay: schedules.slice(0, index).reduce((sum, s) => sum + s.duration, 0) - }); - } - }); - } - }); - - return conflicts; - }, - - getSchedulesByEntity: (entity) => { - const schedulesWithPositions = get().getSchedulesWithPositions(); - if (!entity) return schedulesWithPositions; - return schedulesWithPositions.filter(schedule => schedule.entity === entity); - }, - - getEnabledSchedules: () => { - const schedulesWithPositions = get().getSchedulesWithPositions(); - return schedulesWithPositions.filter(schedule => schedule.enabled); - } - }), - { - name: 'schedule-store', - storage: createJSONStorage(() => localStorage), - // Only persist certain fields - partialize: (state) => ({ - schedules: state.schedules, - selectedTimezone: state.selectedTimezone, - businessHoursVisible: state.businessHoursVisible, - recentTimezones: state.recentTimezones, - }), - } - ) -); - -// Helper function to create a new schedule -export const createSchedule = ( - entity: 'usa' | 'adgm', - syncType: 'project' | 'user', - startTimeUTC?: string -): Omit => ({ - title: `${entity.toUpperCase()} ${syncType === 'project' ? 'Projects' : 'Users'}`, - entity, - syncType, - emoji: entity === 'usa' ? '🇺🇸' : '🇦🇪', - startTimeUTC: startTimeUTC || '02:00', // Default to 2 AM UTC - duration: 5, // Default 5 minutes - enabled: false, - frequency: 'daily' -}); - -// Helper to validate schedule time -export const validateScheduleTime = ( - timeUTC: string, - entity: 'usa' | 'adgm', - displayTimezone: string -): { valid: boolean; warning?: string } => { - try { - const position = convertUTCToPosition(timeUTC, displayTimezone); - const businessHours = getBusinessHours(entity, displayTimezone); - - // Check if the time falls within business hours - const isInBusinessHours = position >= businessHours.start && position <= businessHours.end; - - if (isInBusinessHours) { - return { - valid: true, - warning: 'Scheduling during business hours is not recommended as it may impact users.' - }; - } - - return { valid: true }; - } catch { - return { - valid: false, - warning: 'Invalid time format or timezone.' - }; - } -}; diff --git a/visual-schedule-manager-portfolio/tailwind.config.js b/visual-schedule-manager-portfolio/tailwind.config.js deleted file mode 100644 index 3818e5b..0000000 --- a/visual-schedule-manager-portfolio/tailwind.config.js +++ /dev/null @@ -1,77 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -module.exports = { - darkMode: ["class"], - content: [ - './pages/**/*.{ts,tsx}', - './components/**/*.{ts,tsx}', - './app/**/*.{ts,tsx}', - './src/**/*.{ts,tsx}', - './*.{ts,tsx}', - ], - theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, - }, - plugins: [require("tailwindcss-animate")], -} \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/tsconfig.json b/visual-schedule-manager-portfolio/tsconfig.json deleted file mode 100644 index ecf65ce..0000000 --- a/visual-schedule-manager-portfolio/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "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, - "paths": { - "@/*": ["./*"] - } - }, - "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules"] -} \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/utils/auto-schedule.ts b/visual-schedule-manager-portfolio/utils/auto-schedule.ts deleted file mode 100644 index deb98d1..0000000 --- a/visual-schedule-manager-portfolio/utils/auto-schedule.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { Schedule } from '../stores/schedule-store'; -import { convertPositionToUTC, convertUTCToPosition } from './timezone-conversion'; - -export interface AutoScheduleOptions { - baseHour: number; // 2 for 2 AM - staggerMinutes: number; // 15 - syncOrder: ('user' | 'project')[]; -} - -export interface ScheduleUpdate { - scheduleId: string; - newTimeUTC: string; - displayTime: string; - timezone: string; - entity: 'usa' | 'adgm'; - syncType: 'project' | 'user'; -} - -export const generateAutoSchedule = ( - unscheduledSyncs: Schedule[], - options: AutoScheduleOptions = { - baseHour: 2, - staggerMinutes: 15, - syncOrder: ['user', 'project'] - } -): ScheduleUpdate[] => { - const scheduleUpdates: ScheduleUpdate[] = []; - - // Group syncs by entity - const syncsByEntity = unscheduledSyncs.reduce((groups, sync) => { - if (!groups[sync.entity]) { - groups[sync.entity] = {}; - } - if (!groups[sync.entity][sync.syncType]) { - groups[sync.entity][sync.syncType] = []; - } - groups[sync.entity][sync.syncType].push(sync); - return groups; - }, {} as Record>); - - // Process each entity - Object.entries(syncsByEntity).forEach(([entity, syncTypes]) => { - const entityTimezone = getLocalTimezone(entity as 'usa' | 'adgm'); - let currentMinuteOffset = 0; - - // Process sync types in order (users first, then projects) - options.syncOrder.forEach(syncType => { - if (syncTypes[syncType]) { - syncTypes[syncType].forEach(sync => { - // Calculate the scheduled time - const totalMinutes = (options.baseHour * 60) + currentMinuteOffset; - const hours = Math.floor(totalMinutes / 60); - const minutes = totalMinutes % 60; - - // Create local time and convert to UTC - const localTimeString = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; - const position = convertUTCToPosition(localTimeString, entityTimezone); - const utcTimeString = convertPositionToUTC(position, entityTimezone); - - scheduleUpdates.push({ - scheduleId: sync.id, - newTimeUTC: utcTimeString, - displayTime: localTimeString, - timezone: entityTimezone, - entity: sync.entity, - syncType: sync.syncType - }); - - // Increment for next sync - currentMinuteOffset += options.staggerMinutes; - }); - } - }); - }); - - return scheduleUpdates; -}; - -export const getLocalTimezone = (entity: 'usa' | 'adgm'): string => { - switch (entity) { - case 'usa': - return 'America/New_York'; // EST/EDT - case 'adgm': - return 'Asia/Dubai'; // GST - default: - return 'UTC'; - } -}; - -export const validateScheduleTime = ( - timeUTC: string, - entity: 'usa' | 'adgm' -): { valid: boolean; warning?: string } => { - try { - const entityTimezone = getLocalTimezone(entity); - const position = convertUTCToPosition(timeUTC, entityTimezone); - - // Convert position back to hours for readability - const hours = Math.floor(position / 4); - - // Define business hours for each entity - const businessHours = { - usa: { start: 8, end: 20 }, // 8 AM to 8 PM - adgm: { start: 8, end: 18 } // 8 AM to 6 PM - }; - - const entityHours = businessHours[entity]; - const isInBusinessHours = hours >= entityHours.start && hours < entityHours.end; - - if (isInBusinessHours) { - return { - valid: true, - warning: `Scheduling during business hours (${entityHours.start}:00-${entityHours.end}:00 local time) may impact users.` - }; - } - - return { valid: true }; - } catch { - return { - valid: false, - warning: 'Invalid time format or timezone configuration.' - }; - } -}; - -export const calculateOptimalStartTime = ( - entity: 'usa' | 'adgm', - existingSchedules: Schedule[] = [] -): string => { - const entityTimezone = getLocalTimezone(entity); - const baseHour = 2; // 2 AM local time - - // Find occupied time slots - const occupiedPositions = existingSchedules - .filter(schedule => schedule.entity === entity && schedule.enabled) - .map(schedule => convertUTCToPosition(schedule.startTimeUTC, entityTimezone)); - - // Start from 2 AM and find first available slot - let currentPosition = baseHour * 4; // 2 AM = position 8 - - while (occupiedPositions.includes(currentPosition)) { - currentPosition += 1; // Move to next 15-minute slot - - // If we've gone past 6 AM, wrap to next day or return original time - if (currentPosition >= 24 * 4) { - break; - } - } - - return convertPositionToUTC(currentPosition, entityTimezone); -}; - -export const getRecommendedScheduleTimes = (entity: 'usa' | 'adgm'): string[] => { - const entityTimezone = getLocalTimezone(entity); - const recommendedHours = [2, 3, 4, 5]; // 2 AM to 5 AM local time - - return recommendedHours.map(hour => { - const position = hour * 4; // Convert hour to position - return convertPositionToUTC(position, entityTimezone); - }); -}; - -export const analyzeScheduleDistribution = (schedules: Schedule[]): { - usa: { user: number; project: number }; - adgm: { user: number; project: number }; - total: number; - unscheduled: number; -} => { - const analysis = { - usa: { user: 0, project: 0 }, - adgm: { user: 0, project: 0 }, - total: schedules.length, - unscheduled: 0 - }; - - schedules.forEach(schedule => { - if (!schedule.startTimeUTC || !schedule.enabled) { - analysis.unscheduled++; - } else { - analysis[schedule.entity][schedule.syncType]++; - } - }); - - return analysis; -}; - -export const generateSchedulePreview = ( - updates: ScheduleUpdate[] -): { - summary: string; - timeline: { time: string; entity: string; syncType: string; timezone: string }[]; - conflicts: string[]; -} => { - const timeline = updates - .map(update => ({ - time: update.displayTime, - entity: update.entity, - syncType: update.syncType, - timezone: update.timezone - })) - .sort((a, b) => a.time.localeCompare(b.time)); - - // Check for potential conflicts (same time slots) - const timeSlots: Record = {}; - timeline.forEach(item => { - const key = `${item.time}-${item.timezone}`; - if (!timeSlots[key]) timeSlots[key] = []; - timeSlots[key].push(item); - }); - - const conflicts = Object.entries(timeSlots) - .filter(([, items]) => items.length > 1) - .map(([time, items]) => - `${time}: ${items.map(i => `${i.entity} ${i.syncType}`).join(', ')}` - ); - - const summary = `Scheduling ${updates.length} sync jobs starting at 2:00 AM local time, staggered by 15 minutes.`; - - return { summary, timeline, conflicts }; -}; \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/utils/collision-detection.ts b/visual-schedule-manager-portfolio/utils/collision-detection.ts deleted file mode 100644 index 95bd532..0000000 --- a/visual-schedule-manager-portfolio/utils/collision-detection.ts +++ /dev/null @@ -1,110 +0,0 @@ -export interface TimeSlot { - id: string; - position: number; - duration: number; // in 15-minute blocks (usually 1) -} - -export const checkCollision = ( - newPosition: number, - duration: number, - existingSlots: TimeSlot[], - excludeId?: string -): boolean => { - // Check if the new position overlaps with existing slots - const newEndPosition = newPosition + duration - 1; - - return existingSlots.some(slot => { - if (excludeId && slot.id === excludeId) { - return false; // Exclude the item being dragged - } - - const slotEndPosition = slot.position + slot.duration - 1; - - // Check for overlap: new slot starts before existing slot ends - // and existing slot starts before new slot ends - return ( - newPosition <= slotEndPosition && - slot.position <= newEndPosition - ); - }); -}; - -export const findNextAvailableSlot = ( - startPosition: number, - duration: number, - existingSlots: TimeSlot[] -): number => { - // Find the next available position after startPosition - let currentPosition = startPosition; - const maxPosition = 96 - duration; // 96 total blocks minus duration - - while (currentPosition <= maxPosition) { - if (!checkCollision(currentPosition, duration, existingSlots)) { - return currentPosition; - } - currentPosition++; - } - - // If no slot available after start position, try from beginning - currentPosition = 0; - while (currentPosition < startPosition) { - if (!checkCollision(currentPosition, duration, existingSlots)) { - return currentPosition; - } - currentPosition++; - } - - // Return the original position if no available slot found - return startPosition; -}; - -export const getQueueInfo = ( - position: number, - existingSlots: TimeSlot[] -): { willQueue: boolean; queuedAfter?: string[] } => { - // Determine if this position will cause queuing - const conflictingSlots = existingSlots.filter(slot => { - const slotEndPosition = slot.position + slot.duration - 1; - return position >= slot.position && position <= slotEndPosition; - }); - - if (conflictingSlots.length === 0) { - return { willQueue: false }; - } - - // Sort conflicting slots by position to determine queue order - const sortedConflicts = conflictingSlots.sort((a, b) => a.position - b.position); - - return { - willQueue: true, - queuedAfter: sortedConflicts.map(slot => slot.id) - }; -}; - -export const calculateEstimatedStartTime = ( - position: number, - duration: number, - existingSlots: TimeSlot[] -): number => { - // Calculate when the job will actually start considering queue - const queueInfo = getQueueInfo(position, existingSlots); - - if (!queueInfo.willQueue) { - return position; - } - - // Find the latest end time of conflicting slots - const conflictingSlots = existingSlots.filter(slot => - queueInfo.queuedAfter?.includes(slot.id) - ); - - if (conflictingSlots.length === 0) { - return position; - } - - const latestEndTime = Math.max( - ...conflictingSlots.map(slot => slot.position + slot.duration) - ); - - return latestEndTime; -}; \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/utils/date.ts b/visual-schedule-manager-portfolio/utils/date.ts deleted file mode 100644 index dd43897..0000000 --- a/visual-schedule-manager-portfolio/utils/date.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Date formatting utilities for the frontend application - */ - -/** - * Format a date string to local time with consistent formatting - */ -export const formatLocalTime = ( - dateString: string, - options?: Intl.DateTimeFormatOptions -): string => { - const defaultOptions: Intl.DateTimeFormatOptions = { - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true - }; - - // Ensure UTC string format - const utcString = dateString.endsWith('Z') ? dateString : `${dateString}Z`; - const date = new Date(utcString); - - // Validate date - if (isNaN(date.getTime())) { - console.error(`Invalid date string: ${dateString}`); - return 'Invalid date'; - } - - return date.toLocaleString('en-US', options || defaultOptions); -}; - -/** - * Format a date string to local time with extended options (includes year and timezone) - */ -export const formatLocalTimeExtended = (dateString: string): string => { - return formatLocalTime(dateString, { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true, - timeZoneName: 'short' - }); -}; - -/** - * Format relative time (e.g., "2 hours ago", "just now") - */ -export const formatRelativeTime = (dateString: string): string => { - const date = new Date(dateString); - - // Validate date - if (isNaN(date.getTime())) { - console.error(`Invalid date string: ${dateString}`); - return 'Invalid date'; - } - - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - - // Handle future dates - if (diffMs < 0) { - return formatLocalTime(dateString); - } - - const diffSecs = Math.floor(diffMs / 1000); - const diffMins = Math.floor(diffSecs / 60); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffSecs < 60) return 'just now'; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - - return formatLocalTime(dateString); -}; - -/** - * Format duration in seconds to human-readable string - * Examples: 45 → "45s", 90 → "1m 30s", 3665 → "1h 1m" - */ -export const formatDuration = (seconds: number): string => { - // Validate input - if (seconds < 0 || !Number.isInteger(seconds)) { - console.error(`Invalid duration: ${seconds}. Duration must be a non-negative integer.`); - return '0s'; - } - - if (seconds < 60) return `${seconds}s`; - - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - - if (minutes < 60) { - return remainingSeconds > 0 - ? `${minutes}m ${remainingSeconds}s` - : `${minutes}m`; - } - - const hours = Math.floor(minutes / 60); - const remainingMinutes = minutes % 60; - - return remainingMinutes > 0 - ? `${hours}h ${remainingMinutes}m` - : `${hours}h`; -}; \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/utils/timeline-helpers.ts b/visual-schedule-manager-portfolio/utils/timeline-helpers.ts deleted file mode 100644 index 3960ee5..0000000 --- a/visual-schedule-manager-portfolio/utils/timeline-helpers.ts +++ /dev/null @@ -1,74 +0,0 @@ -interface BusinessHoursConfig { - [key: string]: { start: number; end: number }; -} - -const DEFAULT_BUSINESS_HOURS: BusinessHoursConfig = { - usa: { start: 8, end: 18 }, - europe: { start: 8, end: 18 }, - asia: { start: 9, end: 18 } -}; - -export const TIME_BLOCK_WIDTH = 25; // Width of each 15-minute block in pixels - -export const getTimelinePosition = (hours: number, minutes: number): number => { - // Validate inputs - if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { - throw new Error('Invalid time: hours must be 0-23, minutes must be 0-59'); - } - - // Convert time to position (0-95) - // Each hour has 4 blocks (15-minute each) - // Clamp minutes to nearest 15-minute block - const minuteBlock = Math.floor(minutes / 15); - return hours * 4 + minuteBlock; -}; - -export function formatTimeForDisplay(hour: number, minute: number = 0): string { - const period = hour >= 12 ? 'PM' : 'AM'; - const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour; - - if (minute === 0) { - return `${displayHour} ${period}`; - } - - return `${displayHour}:${minute.toString().padStart(2, '0')} ${period}`; -} - -export const getGridColumns = (): string => { - // CSS grid template columns for 96 time blocks - return `repeat(96, ${TIME_BLOCK_WIDTH}px)`; -}; - -export const getTimeFromPosition = (position: number): { hours: number; minutes: number } => { - // Validate input - if (position < 0 || position > 95) { - throw new Error('Invalid position: must be between 0 and 95'); - } - - // Convert position back to hours and minutes - const hours = Math.floor(position / 4); - const minutes = (position % 4) * 15; - return { hours, minutes }; -}; - -export const isWithinBusinessHours = ( - position: number, - entity: 'usa' | 'europe' | 'asia', - businessHours: BusinessHoursConfig = DEFAULT_BUSINESS_HOURS -): boolean => { - // Check if position falls within business hours - // This is a simplified check - timezone-aware business hours - // will be handled by the BusinessHoursOverlay component - const { hours } = getTimeFromPosition(position); - - const config = businessHours[entity]; - return config ? hours >= config.start && hours < config.end : false; -}; - -export const calculateGridWidth = (blockWidth: number = TIME_BLOCK_WIDTH): number => { - // Calculate total grid width based on block width in pixels - return 96 * blockWidth; -}; - -// Export the business hours config for reuse -export { DEFAULT_BUSINESS_HOURS, type BusinessHoursConfig }; \ No newline at end of file diff --git a/visual-schedule-manager-portfolio/utils/timezone-conversion.ts b/visual-schedule-manager-portfolio/utils/timezone-conversion.ts deleted file mode 100644 index f16b8d3..0000000 --- a/visual-schedule-manager-portfolio/utils/timezone-conversion.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { toZonedTime, fromZonedTime, format } from 'date-fns-tz'; - -export const convertUTCToPosition = ( - timeUTC: string, // "HH:mm" - displayTimezone: string -): number => { - try { - // Parse the UTC time - const [hours, minutes] = timeUTC.split(':').map(Number); - - // Create a date object in UTC for today with the specified time - const utcDate = new Date(); - utcDate.setUTCHours(hours, minutes, 0, 0); - - // Convert to display timezone - const zonedDate = toZonedTime(utcDate, displayTimezone); - - // Get local hours and minutes - const localHours = zonedDate.getHours(); - const localMinutes = zonedDate.getMinutes(); - - // Convert to position (0-95) - each position is 15 minutes - return localHours * 4 + Math.floor(localMinutes / 15); - } catch (error) { - console.error('Error converting UTC to position:', error); - return 0; - } -}; - -export const convertPositionToUTC = ( - position: number, // 0-95 - displayTimezone: string -): string => { - try { - // Calculate hours and minutes from position - const hours = Math.floor(position / 4); - const minutes = (position % 4) * 15; - - // Create a date object in the display timezone for today - const localDate = new Date(); - localDate.setHours(hours, minutes, 0, 0); - - // Convert to UTC - const utcDate = fromZonedTime(localDate, displayTimezone); - - // Return in HH:mm format - return format(utcDate, 'HH:mm', { timeZone: 'UTC' }); - } catch (error) { - console.error('Error converting position to UTC:', error); - return '00:00'; - } -}; - -export const formatTimeForDisplay = ( - timeUTC: string, - displayTimezone: string -): string => { - try { - const [hours, minutes] = timeUTC.split(':').map(Number); - const utcDate = new Date(); - utcDate.setUTCHours(hours, minutes, 0, 0); - - const zonedDate = toZonedTime(utcDate, displayTimezone); - - // Format as "2:00 AM EST" - return format(zonedDate, 'h:mm a zzz', { timeZone: displayTimezone }); - } catch (error) { - console.error('Error formatting time for display:', error); - return timeUTC; - } -}; - -export const getTimezoneOffset = (timezone: string): number => { - try { - const now = new Date(); - const utcTime = now.getTime() + (now.getTimezoneOffset() * 60000); - const targetTime = new Date(utcTime + (getTimezoneOffsetInMs(timezone))); - return (targetTime.getTime() - now.getTime()) / (1000 * 60 * 60); - } catch (error) { - console.error('Error getting timezone offset:', error); - return 0; - } -}; - -const getTimezoneOffsetInMs = (timezone: string): number => { - const now = new Date(); - const zonedTime = toZonedTime(now, timezone); - const utcTime = fromZonedTime(zonedTime, 'UTC'); - return zonedTime.getTime() - utcTime.getTime(); -}; - -export const getBusinessHours = ( - entity: 'usa' | 'adgm', - displayTimezone: string -): { start: number; end: number } => { - try { - if (entity === 'usa') { - // USA: 8 AM EST to 8 PM PST - // Convert 8 AM EST to display timezone - const estStartUTC = convertPositionToUTC(32, 'America/New_York'); // 8 AM EST = position 32 - const pstEndUTC = convertPositionToUTC(80, 'America/Los_Angeles'); // 8 PM PST = position 80 - - const startPosition = convertUTCToPosition(estStartUTC, displayTimezone); - const endPosition = convertUTCToPosition(pstEndUTC, displayTimezone); - - return { start: startPosition, end: endPosition }; - } else { - // ADGM: 8 AM to 6 PM GST - const gstStartUTC = convertPositionToUTC(32, 'Asia/Dubai'); // 8 AM GST = position 32 - const gstEndUTC = convertPositionToUTC(72, 'Asia/Dubai'); // 6 PM GST = position 72 - - const startPosition = convertUTCToPosition(gstStartUTC, displayTimezone); - const endPosition = convertUTCToPosition(gstEndUTC, displayTimezone); - - return { start: startPosition, end: endPosition }; - } - } catch (error) { - console.error('Error calculating business hours:', error); - return { start: 32, end: 72 }; // Default 8 AM to 6 PM - } -}; - -export const getCurrentTimePosition = (timezone: string): number => { - try { - const now = new Date(); - const zonedNow = toZonedTime(now, timezone); - const hours = zonedNow.getHours(); - const minutes = zonedNow.getMinutes(); - - // Convert to position with sub-minute precision - return hours * 4 + (minutes / 15); - } catch (error) { - console.error('Error getting current time position:', error); - return 0; - } -}; - -export const getTimelinePosition = (hours: number, minutes: number): number => { - return hours * 4 + Math.floor(minutes / 15); -}; - -export const formatTimeMarker = (hour: number): string => { - return `${hour.toString().padStart(2, '0')}:00`; -}; - -export const getGridColumns = (): string => { - return 'repeat(96, 1fr)'; -}; \ No newline at end of file From 0d2f14214acf69719811e8c53bd3540cde146e60 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:46:58 -0400 Subject: [PATCH 6/7] docs(roadmap): record the 2026-10-25 status and retire the superseded 3D items - docs(roadmap): add a where-things-stand section covering the security patch, CI hardening and repo cleanup in flight - docs(roadmap): mark the WebGL fallback superseded by the Catalogue redesign and park the 3D asset set - docs(roadmap): record the PostHog verification of $pathname and $pageleave --- ROADMAP.md | 106 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 5f36304..e7f1162 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,35 +3,57 @@ Loose, personal roadmap for the CodesWhat homepage. Not a ticket tracker (that's work). Just the ideas worth not forgetting. -## Progressive-enhancement fallback for the 3D layer +## Where things stand (2026-10-25) + +The Catalogue redesign, the `/support` page and the analytics events shipped on +2026-10-22 (#69, #70, #71). Production is `main@68520ee`. Two things are in +flight: -The homepage renders its coin, mascot balloons, and the `@` marble live in WebGL -(three.js + react-three-fiber). That buys real interactivity: the balloons track -scroll and dodge the cursor, the marble orbits and flees. The cost isn't mainly -bandwidth (the three/fiber/drei bundle is ~300KB gzipped, loaded once and cached, -and the texture PNGs load either way). The cost is compute: WebGL burns CPU/GPU -and battery and adds parse + hydration time, which hurts on cheap phones. +- [ ] Dependency security patch (Next 16.4.3, dompurify, sharp): PR #72 is green + on CI and preview and waiting on a delivered review before it lands in `dev`, + then a promotion to `main`. +- [ ] CI hardening (support tests in `verify`, no duplicate promotion runs, + fork-safe skip, no persisted checkout credential, actions pinned to + commits): finished and tested locally, queued behind #72. +- [ ] Remove the stray `visual-schedule-manager-portfolio/` app and root + `start.sh`; nothing references either. +- [ ] Clean up `test/posthog.test.ts`, which imports a posthog-js internal path, + then bump `posthog-js` and add `test:posthog:browser` to CI. +- [ ] Close #58 (`.claude/` is already ignored on `main`). #57 is partly done; + the remaining items are tracked privately. + +## Progressive-enhancement fallback for the 3D layer -Baked assets (the WebP loops in `exports/`) are the opposite: nearly free to -render, but dumb. They can't track scroll or react to the pointer. +**Superseded 2026-10-22.** The Catalogue redesign replaced the WebGL homepage +with CSS and pixel-sprite specimen plates (`components/catalogue/sprites.ts`). +three.js, react-three-fiber, drei and rapier are now only imported under +`components/studio/`, which backs the dev-only `/studio/*` capture routes. There +is no 3D layer on the shipped homepage to gate or fall back from, so the original +checklist below is moot and kept for the reasoning only. -So the move for a lighter homepage isn't ripping WebGL out. It's a fallback: -serve flat assets by default and upgrade to WebGL when the device can handle it, -or when `prefers-reduced-motion` is off. That's more code, not less, but it gives -weak devices and reduced-motion users a cheap, static homepage while everyone -else still gets the interactive scene. +The original idea: the homepage rendered its coin, mascot balloons and the `@` +marble live in WebGL. That bought real interactivity (balloons tracking scroll and +dodging the cursor, the marble orbiting and fleeing) at the cost of compute, +battery and hydration time on cheap phones. Baked WebP loops were nearly free to +render but couldn't react to anything. The plan was to serve flat assets by +default and upgrade to WebGL behind `prefers-reduced-motion` and a capability +check, lazy-loading the 3D bundle only on the upgrade path, and to measure First +Load JS and TTI for both paths rather than guess. -- [ ] Static fallback layer (coin WebP + balloon stills positioned to match the scene) -- [ ] Gate WebGL behind `prefers-reduced-motion` + a capability/perf check -- [ ] Lazy-load the 3D bundle only when the upgrade path is taken -- [ ] Measure: real First Load JS + TTI for both paths (build, don't guess) +- [ ] Decide whether the three/fiber/drei/rapier dependencies stay for the + `/studio` capture rig or go. They're dev-only but still in `package.json` + and the lockfile, so they show up in audits (`fflate` moderate via drei). ## Coherent 3D brand-asset / icon set -`scripts/capture-assets.mjs` mints transparent assets straight from the live scene -(dev-only `/studio/*` routes → headless Chrome → ffmpeg/img2webp). It already does -the coin (spin) and any mascot balloon (square avatar still + spin). Make the whole -icon/image story coherent off the back of it: +**Parked 2026-10-25.** The Catalogue uses hand-drawn pixel sprites for all eight +projects plus the coin, matched to the real project logos, so the 3D-minted +icon set isn't on the critical path any more. `scripts/capture-assets.mjs` and +the `/studio/*` routes still work and still mint the coin spin and any mascot +balloon (square avatar still + spin). What exists today: `exports/coin/` +(light and dark spin WebP) and `exports/portkey/` (avatar PNG + spin WebP), plus +static balloon PNGs for drydock, portkey, portwing, sockguard and scott in +`frontend/public/logos/`. - [ ] Avatar + spin for every mascot (Sockguard, Drydock, Portwing, Portkey), not just Portkey - [ ] Decide where committed assets live and how they're named (currently `exports//`, WebP + still PNG tracked, GIF/APNG gitignored) @@ -40,26 +62,25 @@ icon/image story coherent off the back of it: ## Web-analytics table coverage (2026-08-28, ops X37) -Production events carry no `$pathname` and no `$pageleave`, so PostHog's Page / -Entry / Exit tables render empty and every session counts as a zero-duration -bounce. The wiring for both landed on dev as PR #60 (sanitizer allowlists -`$pageleave` and stamps `$pathname` from the already-sanitized `path`) and -ships with the next promotion — the items below are what's left after that. +Production events carried no `$pathname` and no `$pageleave`, so PostHog's Page / +Entry / Exit tables rendered empty and every session counted as a zero-duration +bounce. The wiring for both landed as PR #60 and has been in production since the +September promotions. -- [ ] Verify post-deploy that `$pathname` shows on fresh pageviews and the - Page/Entry/Exit tables populate (board: CodesWhat Sites Health, - project 558033, dashboard 2044260) -- [ ] `$pageleave` volume note (resolved 2026-08-28, standard `19a0af1`): a +- [x] Verified 2026-10-25 against PostHog project 558033 (host `codeswhat.com`, + last 30 days): 30 `$pageview`, all 30 carrying `$pathname`; 9 `$pageleave`; + 22 `$web_vitals`. The Page/Entry/Exit tables have data to render. +- [x] `$pageleave` volume note (resolved 2026-08-28, standard `19a0af1`): a pageleave-to-pageview ratio well under 100% is structural, not a bug — `$pageview` fires per client-side route change, `$pageleave` once per document lifetime. drydock measured the envelope: the sanitizer guard eats nothing. Don't "fix" the ratio here. -- [ ] Post-deploy canary caveat: posthog-js bot detection (`isLikelyBot` - checks `navigator.webdriver` and `userAgentData.brands`) may silently - drop Playwright-driven visits before `before_send` runs; contested - (portwing's landed), so a Playwright canary's silence proves nothing - either way. Verify with a real browser or by reading the deployed chunk - for the property names. +- [x] Post-deploy canary caveat: moot now that real traffic confirms the + properties. For the record, posthog-js bot detection (`isLikelyBot` on + `navigator.webdriver` and `userAgentData.brands`) may silently drop + Playwright-driven visits before `before_send` runs, so a Playwright canary's + silence proves nothing either way. Verify with a real browser or by reading + the deployed chunk. - [x] Acquisition data: decided org-wide in `CodesWhat/ops` `standards/analytics.md` ("Acquisition data and consent", as of `c161ebc`) — that file is the authority. Landed: `save_campaign_params: @@ -74,10 +95,13 @@ ships with the next promotion — the items below are what's left after that. (cookieless strips the IP upstream, PostHog #48660). No banner needed. - [x] `$web_vitals` carries `$pathname` from the sanitized path - [x] `cta activated` for the project cards: `project_` ids, placement - `projects`. `cta activated` had zero rows in 120 days on codeswhat.com; - reproduced in a browser and the capture, sanitizer and send path all work, - so it was low traffic (76 pageviews) rather than a bug. Check it fires after - deploy. + `projects`. Shipped 2026-10-22 (#68/#70). Capture, sanitizer and send path + were reproduced in a browser before shipping. +- [ ] Production fire check for `cta activated`, `newsletter submitted` and + `client error`: none of the three has a row for host `codeswhat.com` in the + 30 days to 2026-10-25. With 30 pageviews in that window that's low traffic, + not evidence of a bug, so leave this open until one lands or a real-browser + click on production is checked in PostHog. - [x] `newsletter submitted` (`result`, plus `error_class` from invalid_email / rate_limited / unavailable / network on error) - [x] `client error` (`error_kind`, allowlisted `error_name`, sanitized path; From f3ea2d7e1eaf77833106bdd49ae9b20292163b1c Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:54:51 -0400 Subject: [PATCH 7/7] ci(website): give push runs a unique concurrency group A queued push run shares its group with the running one, so a third push to dev replaced the pending run and left that SHA with no verify result. The promotion skip assumes every dev SHA keeps its own run. Keying push runs by run_id removes the queue; PR runs still cancel superseded heads per PR. --- .github/workflows/website.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 35e1359..64deae4 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -14,7 +14,10 @@ permissions: contents: read concurrency: - group: website-${{ github.event.pull_request.number || github.ref }} + # PR runs share a group per PR so a superseded head is cancelled. Push runs get + # a unique group so a queued dev/main run is never replaced by a newer push; + # the promotion skip relies on every dev SHA keeping its own verify run. + group: website-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: